Merge branch 'master' of github.com:Budibase/budibase into screen-updates

This commit is contained in:
Andrew Kingston 2020-10-14 17:09:50 +01:00
commit 3de238c66a
61 changed files with 269 additions and 252 deletions

View File

@ -10,7 +10,7 @@
"eslint-plugin-svelte3": "^2.7.3",
"lerna": "3.14.1",
"prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0",
"prettier-plugin-svelte": "^1.4.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.28.0"

View File

@ -27,7 +27,6 @@ context("Create a Table", () => {
cy.get(".actions input")
.first()
.type("updated")
cy.get("select").select("Text")
cy.contains("Save Column").click()
cy.contains("nameupdated").should("have.text", "nameupdated")
})

View File

@ -53,6 +53,7 @@ export const getStore = () => {
store.saveScreen = saveScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.deleteScreens = deleteScreens(store)
store.setCurrentPage = setCurrentPage(store)
store.createScreen = createScreen(store)
store.addStylesheet = addStylesheet(store)
@ -185,6 +186,26 @@ const setCurrentScreen = store => screenName => {
})
}
const deleteScreens = store => (screens, pageName = null) => {
if (!(screens instanceof Array)) {
screens = [screens]
}
store.update(state => {
if (pageName == null) {
pageName = state.pages.main.name
}
for (let screen of screens) {
state.screens = state.screens.filter(c => c.name !== screen.name)
// Remove screen from current page as well
state.pages[pageName]._screens = state.pages[pageName]._screens.filter(
scr => scr.name !== screen.name
)
api.delete(`/_builder/api/pages/${pageName}/screens/${screen.name}`)
}
return state
})
}
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {

View File

@ -23,9 +23,7 @@
class="automation-block hoverable"
on:click={addBlockToAutomation}
data-cy={stepId}>
<div>
<i class={blockDefinition.icon} />
</div>
<div><i class={blockDefinition.icon} /></div>
<div class="automation-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>

View File

@ -62,10 +62,7 @@
{#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
{:else if $automationStore.selectedAutomation}
<div class="block-label">
Automation
<b>{automation.name}</b>
</div>
<div class="block-label">Automation <b>{automation.name}</b></div>
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
{/if}
</div>

View File

@ -108,7 +108,8 @@
<div
class:link={row[header] && row[header].length}
on:click={() => selectRelationship(row, header)}>
{row[header] ? row[header].length : 0} related row(s)
{row[header] ? row[header].length : 0}
related row(s)
</div>
{:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />

View File

@ -49,9 +49,7 @@
<button class:selected={currentPage === 0} on:click={() => selectPage(0)}>
1
</button>
{#if currentPage > 3}
<button disabled>...</button>
{/if}
{#if currentPage > 3}<button disabled>...</button>{/if}
{#each pagesAroundCurrent as idx}
<button
class:selected={idx === currentPage}
@ -59,9 +57,7 @@
{idx + 1}
</button>
{/each}
{#if currentPage < numPages - 4}
<button disabled>...</button>
{/if}
{#if currentPage < numPages - 4}<button disabled>...</button>{/if}
<button
class:selected={currentPage === numPages - 1}
on:click={() => selectPage(numPages - 1)}>
@ -77,8 +73,13 @@
<p>
{#if numPages > 1}
Showing {ITEMS_PER_PAGE * currentPage + 1} - {ITEMS_PER_PAGE * currentPage + pageItemCount}
of {data.length} rows
Showing
{ITEMS_PER_PAGE * currentPage + 1}
-
{ITEMS_PER_PAGE * currentPage + pageItemCount}
of
{data.length}
rows
{:else if numPages === 1}Showing all {data.length} row(s){/if}
</p>
</div>

View File

@ -65,16 +65,18 @@
<div class="actions">
<Input label="Name" thin bind:value={field.name} />
<Select
secondary
thin
label="Type"
on:change={handleFieldConstraints}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
</Select>
{#if !originalName}
<Select
secondary
thin
label="Type"
on:change={handleFieldConstraints}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
</Select>
{/if}
{#if field.type !== 'link'}
<Toggle

View File

@ -20,10 +20,21 @@
let modal
let name
let dataImport
let error = ""
function resetState() {
name = ""
dataImport = undefined
error = ""
}
function checkValid(evt) {
const tableName = evt.target.value
if ($backendUiStore.models?.some(model => model.name === tableName)) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveTable() {
@ -61,12 +72,14 @@
title="Create Table"
confirmText="Create"
onConfirm={saveTable}
disabled={!name || (dataImport && !dataImport.valid)}>
disabled={error || !name || (dataImport && !dataImport.valid)}>
<Input
data-cy="table-name-input"
thin
label="Table Name"
bind:value={name} />
on:input={checkValid}
bind:value={name}
{error} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />

View File

@ -1,9 +1,11 @@
<script>
import { backendUiStore } from "builderStore"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import screenTemplates from "builderStore/store/screenTemplates"
import api from "builderStore/api"
export let table
@ -11,6 +13,10 @@
let dropdown
let editing
let confirmDeleteDialog
let error = ""
let originalName = table.name
let templateScreens
let willBeDeleted
$: fields = Object.keys(table.schema)
@ -24,12 +30,18 @@
}
function showModal() {
const screens = $store.allScreens
templateScreens = screens.filter(screen => screen.props.table === table._id)
willBeDeleted = ["All table data"].concat(
templateScreens.map(screen => `Screen ${screen.props._instanceName}`)
)
hideEditor()
confirmDeleteDialog.show()
}
async function deleteTable() {
await backendUiStore.actions.tables.delete(table)
store.deleteScreens(templateScreens)
notifier.success("Table deleted")
hideEditor()
}
@ -39,6 +51,18 @@
notifier.success("Table renamed successfully")
hideEditor()
}
function checkValid(evt) {
const tableName = evt.target.value
if (
originalName !== tableName &&
$backendUiStore.models?.some(model => model.name === tableName)
) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
</script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
@ -48,7 +72,12 @@
{#if editing}
<div class="actions">
<h5>Edit Table</h5>
<Input label="Table Name" thin bind:value={table.name} />
<Input
label="Table Name"
thin
bind:value={table.name}
on:input={checkValid}
{error} />
<Select
label="Primary Display Column"
thin
@ -61,7 +90,7 @@
</Select>
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary on:click={save}>Save</Button>
<Button primary disabled={error} on:click={save}>Save</Button>
</footer>
</div>
{:else}
@ -79,10 +108,21 @@
</DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete Table"
onOk={deleteTable}
title="Confirm Delete" />
title="Confirm Delete">
Are you sure you wish to delete the table
<i>{table.name}?</i>
The following will also be deleted:
<b>
<div class="delete-items">
{#each willBeDeleted as item}
<div>{item}</div>
{/each}
</div>
</b>
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
@ -96,6 +136,17 @@
font-size: 16px;
}
div.delete-items {
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
}
div.delete-items div {
margin-top: 4px;
font-weight: 500;
}
.actions {
padding: var(--spacing-xl);
display: grid;

View File

@ -39,7 +39,6 @@
transition: 0.2s ease transform, 0.2s ease background-color,
0.2s ease box-shadow;
overflow: hidden;
z-index: 1;
border-radius: 4px;
}

View File

@ -20,7 +20,10 @@
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<div class="body">{body}</div>
<div class="body">
{body}
<slot />
</div>
</ModalContent>
</Modal>

View File

@ -31,9 +31,7 @@
style="background: {themes[notification.type]};"
transition:fly={{ y: -30 }}>
<div class="content">{notification.message}</div>
{#if notification.icon}
<i class={notification.icon} />
{/if}
{#if notification.icon}<i class={notification.icon} />{/if}
</div>
{/each}
</div>

View File

@ -12,15 +12,12 @@
</script>
<div class="root">
<div class="switcher">
{#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab}
</button>
{/each}
</div>
<div class="panel">
@ -34,7 +31,6 @@
<slot name="3" />
{/if}
</div>
</div>
<style>

View File

@ -5,9 +5,7 @@
<div class="container">
<div class="content">
<div class="img">
<img src="https://picsum.photos/60/60" alt="zoom" />
</div>
<div class="img"><img src="https://picsum.photos/60/60" alt="zoom" /></div>
<div class="body">
<div class="title">Zoom</div>
<div class="description">

View File

@ -58,6 +58,7 @@
<Input thin bind:value={username} name="Name" placeholder="Username" />
<Input
thin
type="password"
bind:value={password}
name="Password"
placeholder="Password" />

View File

@ -10,7 +10,9 @@
<Spacer medium />
<div class="card-footer">
<TextButton text medium blue href="/_builder/{_id}">
Open {name}
Open
{name}
</TextButton>
</div>
</div>

View File

@ -28,12 +28,11 @@
<Spacer small />
<Body medium grey>{template.category}</Body>
<Body lh small black>{template.description}</Body>
<div>
<img src={template.image} width="100%" />
</div>
<div><img src={template.image} width="100%" /></div>
<div class="card-footer">
<Button secondary on:click={() => onSelect(template)}>
Create {template.name}
Create
{template.name}
</Button>
</div>
</div>

View File

@ -13,7 +13,6 @@
{category.name}
</li>
{/each}
</div>
<style>

View File

@ -99,9 +99,7 @@
</script>
<div bind:this={anchor} on:click|stopPropagation={() => {}}>
<div class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<div class="icon" on:click={dropdown.show}><i class="ri-more-line" /></div>
</div>
<DropdownMenu
bind:this={dropdown}

View File

@ -1,21 +1,10 @@
<script>
import { setContext, onMount } from "svelte"
import { store } from "builderStore"
import {
LayoutIcon,
PaintIcon,
TerminalIcon,
CircleIndicator,
EventsIcon,
} from "components/common/Icons/"
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte"
let current_view = "design"
let codeEditor
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
{ value: "settings", name: "Settings" },
@ -23,7 +12,6 @@
]
let selectedCategory = categories[0]
$: components = $store.components
$: componentInstance =
$store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo }
@ -76,7 +64,6 @@
</script>
<div class="root">
<CategoryTab
onClick={category => (selectedCategory = category)}
{categories}
@ -99,9 +86,7 @@
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if}
</div>
</div>
<style>

View File

@ -1,33 +1,13 @@
<script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import { store } from "builderStore"
import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CategoryTab from "./CategoryTab.svelte"
import {
find,
sortBy,
groupBy,
values,
filter,
map,
uniqBy,
flatten,
} from "lodash/fp"
import { pipe } from "components/common/core"
import Tab from "./ItemTab/Tab.svelte"
import { store } from "builderStore"
export let toggleTab
let selectTemplateDialog
let selectedTemplateInstance
let templateInstances = []
let selectedComponent = null
const categories = components.categories
let selectedCategory = categories[0]
@ -45,7 +25,6 @@
</script>
<div class="root">
<CategoryTab
onClick={category => (selectedCategory = category)}
{selectedCategory}

View File

@ -38,13 +38,11 @@
</script>
<div class="root">
{#each screens as screen}
<div
class="budibase__nav-item screen-header-row"
class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}>
@ -69,7 +67,6 @@
{dragDropStore} />
{/if}
{/each}
</div>
<style>

View File

@ -129,7 +129,6 @@
<ul>
{#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
<div
on:drop={drop}

View File

@ -20,7 +20,6 @@
<div class="root">
{#if $store.currentFrontEndType === 'page' || $store.screens.length}
<div class="switcher">
<button
class:selected={selected === COMPONENT_SELECTION_TAB}
on:click={() => selectTab(COMPONENT_SELECTION_TAB)}>
@ -32,7 +31,6 @@
on:click={() => selectTab(PROPERTIES_TAB)}>
Edit
</button>
</div>
<div class="panel">
@ -43,10 +41,8 @@
{#if selected === COMPONENT_SELECTION_TAB}
<ComponentSelectionList {toggleTab} />
{/if}
</div>
{/if}
</div>
<style>

View File

@ -28,7 +28,6 @@
</script>
<div class="design-view-container">
<div class="design-view-state-categories">
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div>

View File

@ -18,9 +18,7 @@
</script>
<div class="handler-option">
{#if parameter.name === 'automation'}
<span>{parameter.name}</span>
{/if}
{#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if}
{#if parameter.name === 'automation'}
<Select on:change bind:value={parameter.value}>
<option value="" />

View File

@ -48,7 +48,6 @@
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>

View File

@ -109,7 +109,6 @@
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>

View File

@ -107,7 +107,6 @@
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>

View File

@ -126,9 +126,7 @@
on:click={() => switchLetter(letter)}>
{letter}
</span>
{#if idx !== alphabet.length - 1}
<span>-</span>
{/if}
{#if idx !== alphabet.length - 1}<span>-</span>{/if}
{/each}
</div>
<div class="search-input">

View File

@ -3,9 +3,7 @@
</script>
<div data-cy={item.name} class="item-item" on:click>
<div class="item-icon">
<i class={item.icon} />
</div>
<div class="item-icon"><i class={item.icon} /></div>
<div class="item-text">
<div class="item-name">{item.name}</div>
</div>

View File

@ -1,8 +1,10 @@
<script>
import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import Item from "./Item.svelte"
const dispatch = createEventDispatcher()
import Item from "./Item.svelte"
export let list
let category = list
@ -21,10 +23,12 @@
</script>
{#if !list.isCategory}
<button class="back-button" on:click={() => (list = category)}>Back</button>
<button class="back-button" on:click={goBack}>Back</button>
{/if}
{#each list.children as item}
<Item {item} on:click={() => handleClick(item)} />
{#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)}
<Item {item} on:click={() => handleClick(item)} />
{/if}
{/each}
<style>

View File

@ -89,7 +89,6 @@
</script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Select
label="Choose a Template"
bind:value={templateIndex}
@ -109,5 +108,4 @@
error={routeError}
bind:value={route}
on:change={routeChanged} />
</ModalContent>

View File

@ -17,26 +17,13 @@
}
const deleteScreen = () => {
store.deleteScreens(screen, $store.currentPageName)
// update the page if required
store.update(state => {
// Remove screen from screens
const screens = state.screens.filter(c => c.name !== screen.name)
state.screens = screens
// Remove screen from current page as well
const pageScreens = state.pages[state.currentPageName]._screens.filter(
scr => scr.name !== screen.name
)
state.pages[state.currentPageName]._screens = pageScreens
if (state.currentPreviewItem.name === screen.name) {
store.setCurrentPage($store.currentPageName)
$goto(`./:page/page-layout`)
}
api.delete(
`/_builder/api/pages/${state.currentPageName}/screens/${screen.name}`
)
return state
})
}

View File

@ -1165,14 +1165,6 @@ export default {
},
children: [],
},
// {
// name: "Map",
// _component: "@budibase/standard-components/datamap",
// description: "Shiny map",
// icon: "ri-map-pin-line",
// properties: { design: { ...all } },
// children: [],
// },
],
},
{
@ -1208,6 +1200,7 @@ export default {
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line",
children: [],
showOnPages: ["unauthenticated"],
properties: {
design: { ...all },
settings: [

View File

@ -68,7 +68,10 @@
<span
class:active={false}
class="topnavitemright"
on:click={() => window.open(`/${application}`)}>
on:click={() => {
document.cookie = 'budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
window.open(`/${application}`)
}}>
<PreviewIcon />
</span>
</div>
@ -77,7 +80,7 @@
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then _}
{:then results}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>

View File

@ -7,9 +7,7 @@
{#if $backendUiStore.selectedDatabase._id && selectedTable.name}
<TableDataTable />
{:else}
<i>Create your first table to start building</i>
{/if}
{:else}<i>Create your first table to start building</i>{/if}
<style>
i {

View File

@ -23,9 +23,7 @@
{#if $backendUiStore.tables.length === 0}
<i>Create your first table to start building</i>
{:else}
<i>Select a table to edit</i>
{/if}
{:else}<i>Select a table to edit</i>{/if}
<style>
i {

View File

@ -7,9 +7,7 @@
{#if $backendUiStore.selectedDatabase._id && selectedView}
<ViewDataTable view={selectedView} />
{:else}
<i>Create your first table to start building</i>
{/if}
{:else}<i>Create your first table to start building</i>{/if}
<style>
i {

View File

@ -3,7 +3,7 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
const env = require("../../environment")
const instanceController = require("./instance")
const { copy, exists, readFile, writeFile } = require("fs-extra")
const { copy, existsSync, readFile, writeFile } = require("fs-extra")
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const sqrl = require("squirrelly")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
@ -116,6 +116,12 @@ exports.delete = async function(ctx) {
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
const app = await db.get(ctx.params.applicationId)
const result = await db.remove(app)
for (let instance of app.instances) {
const instanceDb = new CouchDB(instance._id)
await instanceDb.destroy()
}
// remove top level directory
await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), {
recursive: true,
})
@ -137,7 +143,7 @@ const createEmptyAppPackage = async (ctx, app) => {
const appsFolder = budibaseAppsDir()
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
if (existsSync(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
}

View File

@ -41,7 +41,7 @@ exports.authenticate = async ctx => {
dbUser = await instanceDb.get(generateUserID(username))
} catch (_) {
// do not want to throw a 404 - as this could be
// used to dtermine valid usernames
// used to determine valid usernames
ctx.throw(401, "Invalid Credentials")
}

View File

@ -73,6 +73,12 @@ exports.save = async function(ctx) {
let row = ctx.request.body
row.tableId = ctx.params.tableId
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
return
}
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
}
@ -348,3 +354,25 @@ const TYPE_TRANSFORM_MAP = {
false: false,
},
}
async function bulkDelete(ctx) {
const instanceId = ctx.user.instanceId
const { rows } = ctx.request.body
const db = new CouchDB(instanceId)
const linkUpdates = rows.map(row =>
linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
)
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
await Promise.all(linkUpdates)
rows.forEach(row => {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, instanceId, row)
})
}

View File

@ -0,0 +1,17 @@
/**
* This controller is not currently fully implemented. Screens are
* currently managed as part of the pages API, please look in api/routes/page.js
* for routes and controllers.
*/
exports.fetch = async ctx => {
ctx.throw(501)
}
exports.save = async ctx => {
ctx.throw(501)
}
exports.destroy = async ctx => {
ctx.throw(501)
}

View File

@ -1,6 +1,5 @@
const send = require("koa-send")
const { resolve, join } = require("../../utilities/centralPath")
const jwt = require("jsonwebtoken")
const fetch = require("node-fetch")
const fs = require("fs-extra")
const uuid = require("uuid")
@ -13,8 +12,8 @@ const {
} = require("../../utilities/budibaseDir")
const CouchDB = require("../../db")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
const fileProcessor = require("../../utilities/fileProcessor")
const { AuthTypes } = require("../../constants")
exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../builder")
@ -136,7 +135,8 @@ exports.performLocalFileProcessing = async function(ctx) {
}
exports.serveApp = async function(ctx) {
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
const mainOrAuth =
ctx.auth.authenticated === AuthTypes.APP ? "main" : "unauthenticated"
// default to homedir
const appPath = resolve(
@ -146,26 +146,7 @@ exports.serveApp = async function(ctx) {
mainOrAuth
)
let appId = ctx.params.appId
if (process.env.CLOUD) {
appId = ctx.subdomains[1]
}
// only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit
const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId)
if (looksLikeAppId && !ctx.auth.authenticated) {
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
appId,
}
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
ctx.cookies.set("budibase:token", anonToken, {
path: "/",
httpOnly: false,
})
}
const appId = ctx.user.appId
if (process.env.CLOUD) {
const S3_URL = `https://${appId}.app.budi.live/assets/${appId}/${mainOrAuth}/${ctx.file ||
@ -200,7 +181,8 @@ exports.serveAttachment = async function(ctx) {
exports.serveAppAsset = async function(ctx) {
// default to homedir
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
const mainOrAuth =
ctx.auth.authenticated === AuthTypes.APP ? "main" : "unauthenticated"
const appPath = resolve(
budibaseAppsDir(),

View File

@ -0,0 +1,7 @@
const AuthTypes = {
APP: "app",
BUILDER: "builder",
EXTERNAL: "external",
}
exports.AuthTypes = AuthTypes

View File

@ -7,6 +7,8 @@ const {
BUILDER_LEVEL_ID,
ANON_LEVEL_ID,
} = require("../utilities/accessLevels")
const environment = require("../environment")
const { AuthTypes } = require("../constants")
module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") {
@ -17,36 +19,32 @@ module.exports = async (ctx, next) => {
const appToken = ctx.cookies.get("budibase:token")
const builderToken = ctx.cookies.get("builder:token")
if (builderToken) {
try {
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
ctx.auth = {
apiKey: jwtPayload.apiKey,
authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID,
}
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
jwtPayload.instanceId,
jwtPayload.accessLevelId
),
}
} catch (_) {
// empty: do nothing
}
await next()
return
let token
// if running locally in the builder itself
if (!environment.CLOUD && !appToken) {
token = builderToken
ctx.auth.authenticated = AuthTypes.BUILDER
} else {
token = appToken
ctx.auth.authenticated = AuthTypes.APP
}
if (!appToken) {
if (!token) {
ctx.auth.authenticated = false
const appId = process.env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
ctx.user = {
// if appId can't be determined from path param or subdomain
appId: appId || ctx.referer.split("/").pop(),
}
await next()
return
}
try {
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.auth.apiKey = jwtPayload.apiKey
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
@ -54,10 +52,6 @@ module.exports = async (ctx, next) => {
jwtPayload.accessLevelId
),
}
ctx.auth = {
authenticated: ctx.user.accessLevelId !== ANON_LEVEL_ID,
apiKey: jwtPayload.apiKey,
}
} catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
}

View File

@ -7,6 +7,7 @@ const {
} = require("../utilities/accessLevels")
const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants")
module.exports = (permName, getItemId) => async (ctx, next) => {
if (
@ -21,8 +22,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
if (apiKeyInfo) {
ctx.auth = {
authenticated: true,
external: true,
authenticated: AuthTypes.EXTERNAL,
apiKey: ctx.headers["x-api-key"],
}
ctx.user = {
@ -34,6 +34,9 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "API key invalid")
}
// don't expose builder endpoints in the cloud
if (environment.CLOUD && permName === BUILDER) return
if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated")
}
@ -42,6 +45,10 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "User not found")
}
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
return next()
}
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
return next()
}
@ -53,10 +60,6 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
return next()
}
const thisPermissionId = permissionId({
name: permName,
itemId: getItemId && getItemId(ctx),

View File

@ -21,7 +21,6 @@ module.exports.PRETTY_ACCESS_LEVELS = {
[module.exports.ADMIN_LEVEL_ID]: "Admin",
[module.exports.POWERUSER_LEVEL_ID]: "Power user",
[module.exports.BUILDER_LEVEL_ID]: "Builder",
[module.exports.ANON_LEVEL_ID]: "Anonymous",
}
module.exports.adminPermissions = [
{

View File

@ -7,6 +7,7 @@ const VALIDATORS = {
}
const PARSERS = {
number: attribute => Number(attribute),
datetime: attribute => new Date(attribute).toISOString(),
}
@ -24,7 +25,7 @@ function parse(path, parsers) {
}
}
})
result.fromFile(path).subscribe(row => {
result.subscribe(row => {
// For each CSV row parse all the columns that need parsed
for (let key in parsers) {
if (!schema[key] || schema[key].success) {

View File

@ -10,7 +10,6 @@ async function processImage(file) {
const imgMeta = await sharp(file.path)
.resize(300)
.toFile(file.outputPath)
return {
...file,
...imgMeta,

View File

@ -1,4 +1,4 @@
const { exists, readFile, writeFile, ensureDir } = require("fs-extra")
const { existsSync, readFile, writeFile, ensureDir } = require("fs-extra")
const { join, resolve } = require("./centralPath")
const Sqrl = require("squirrelly")
const uuid = require("uuid")
@ -28,7 +28,7 @@ const setCouchDbUrl = async opts => {
const createDevEnvFile = async opts => {
const destConfigFile = join(opts.dir, "./.env")
let createConfig = !(await exists(destConfigFile)) || opts.quiet
let createConfig = !existsSync(destConfigFile) || opts.quiet
if (createConfig) {
const template = await readFile(
resolve(__dirname, "..", "..", ".env.template"),

View File

@ -23,9 +23,7 @@
</script>
<div use:cssVars={cssVariables} class="container">
{#if showImage}
<img class="image" src={imageUrl} alt="" />
{/if}
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<div class="content">
<h2 class="heading">{heading}</h2>
<h4 class="text">{description}</h4>

View File

@ -26,9 +26,7 @@
</script>
<div use:cssVars={cssVariables} class="container">
{#if showImage}
<img class="image" src={imageUrl} alt="" />
{/if}
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<div class="content">
<main>
<h2 class="heading">{heading}</h2>

View File

@ -27,15 +27,4 @@
on:newRow={() => dispatch('newRow')} />
</DropdownMenu> -->
<!-- <style>
div {
display: grid;
grid-template-columns: auto auto;
place-items: start center;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style> -->
<!--<style ✂prettier:content✂="CiAgZGl2IHsKICAgIGRpc3BsYXk6IGdyaWQ7CiAgICBncmlkLXRlbXBsYXRlLWNvbHVtbnM6IGF1dG8gYXV0bzsKICAgIHBsYWNlLWl0ZW1zOiBzdGFydCBjZW50ZXI7CiAgfQogIGg1IHsKICAgIHBhZGRpbmc6IHZhcigtLXNwYWNpbmcteGwpIDAgMCB2YXIoLS1zcGFjaW5nLXhsKTsKICAgIG1hcmdpbjogMDsKICAgIGZvbnQtd2VpZ2h0OiA1MDA7CiAgfQo=" ✂prettier:content✂="" ✂prettier:content✂="" ✂prettier:content✂="" ✂prettier:content✂=""></style>-->

View File

@ -135,7 +135,9 @@
{#if selectedRows.length > 0}
<DeleteButton text small on:click={deleteRows}>
<Icon name="addrow" />
Delete {selectedRows.length} row(s)
Delete
{selectedRows.length}
row(s)
</DeleteButton>
{/if}
</div>

View File

@ -42,9 +42,7 @@
<div class="root">
<div class="content">
{#if logo}
<div class="logo-container">
<img src={logo} alt="logo" />
</div>
<div class="logo-container"><img src={logo} alt="logo" /></div>
{/if}
{#if title}

View File

@ -33,6 +33,4 @@
<sub class={className}>{text}</sub>
{:else if isTag('sup')}
<sup class={className}>{text}</sup>
{:else}
<span>{text}</span>
{/if}
{:else}<span>{text}</span>{/if}

View File

@ -12,9 +12,7 @@
<div class="file">
{#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())}
<img {width} {height} src={file.url} alt="preview of {file.name}" />
{:else}
<i class="far fa-file" />
{/if}
{:else}<i class="far fa-file" />{/if}
</div>
<span>{file.name}</span>
</a>

View File

@ -3690,11 +3690,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier-plugin-svelte@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-0.7.0.tgz#5ac0b9f194e0450c88ff1e167cbf3b32d2642df2"
dependencies:
tslib "^1.9.3"
prettier-plugin-svelte@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-1.4.0.tgz#bb992759fb77ec2c3545d454a7c60f7a258cb745"
integrity sha512-KXO2He7Kql0Lz4DdlzVli1j2JTDUR9jPV/DqyfnJmY1pCeSV1qZkxgdsyYma35W6OLrCAr/G6yKdmzo+75u2Ng==
prettier@^1.19.1:
version "1.19.1"
@ -4529,7 +4528,7 @@ trim-off-newlines@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
tslib@^1.9.0, tslib@^1.9.3:
tslib@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"