Merge branch 'master' into feature/add-grid-to-standard-components

This commit is contained in:
kevmodrome 2020-10-12 18:43:22 +02:00
commit 277ab59647
21 changed files with 198 additions and 63 deletions

View File

@ -90,6 +90,8 @@ const contextToBindables = (models, walkResult) => context => {
runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
// model / view info
model: context.model,
})
// see ModelViewSelect.svelte for the format of context.model

View File

@ -36,7 +36,8 @@ export const saveCurrentPreviewItem = s =>
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
const page = s.pages[s.currentPageName]
const pageName = s.currentPageName || "main"
const page = s.pages[pageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,

View File

@ -79,7 +79,7 @@
}
function fieldOptions(field) {
return viewModel.schema[field].type === "string"
return viewModel.schema[field].type === "options"
? viewModel.schema[field].constraints.inclusion
: [true, false]
}

View File

@ -41,7 +41,6 @@
.map(template => template.create())
for (let screen of screens) {
console.log(JSON.stringify(screen))
try {
await store.createScreen(screen)
} catch (_) {

View File

@ -1,6 +1,7 @@
<script>
import { Input } from "@budibase/bbui"
import { Input, Label } from "@budibase/bbui"
import api from "builderStore/api"
import { backendUiStore } from "builderStore"
import analytics from "analytics"
let keys = { budibase: "" }
@ -38,6 +39,10 @@
edit
value={keys.budibase}
label="Budibase API Key" />
<div>
<Label extraSmall grey>Instance ID (Webhooks)</Label>
<span>{$backendUiStore.selectedDatabase._id}</span>
</div>
</div>
<style>
@ -45,4 +50,9 @@
display: grid;
grid-gap: var(--spacing-xl);
}
span {
font-size: var(--font-size-xs);
font-weight: 500;
}
</style>

View File

@ -3,6 +3,7 @@
import { Input, Button, Spacer, Select, ModalContent } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import { some } from "lodash/fp"
import analytics from "analytics"
const CONTAINER = "@budibase/standard-components/container"
@ -29,7 +30,7 @@
const templateChanged = newTemplateIndex => {
if (newTemplateIndex === undefined) return
const template = templates[newTemplateIndex]
draftScreen = templates[newTemplateIndex].create()
if (draftScreen.props._instanceName) {
name = draftScreen.props._instanceName
@ -63,6 +64,13 @@
store.createScreen(draftScreen)
if (templateIndex !== undefined) {
const template = templates[templateIndex]
analytics.captureEvent("Screen Created", {
template: template.id || template.name,
})
}
finished()
}

View File

@ -1,18 +1,75 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
export let value = ""
$: urls = getUrls()
const handleBlur = () => dispatch("change", value)
// this will get urls of all screens, but only
// choose detail screens that are usable in the current context
// and substitute the :id param for the actual {{ ._id }} binding
const getUrls = () => {
const urls = [
...$store.screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.route,
sort: screen.props._component,
})),
]
const bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
components: $store.components,
screen: $store.currentPreviewItem,
models: $backendUiStore.models,
})
const detailScreens = $store.screens.filter(screen =>
screen.props._component.endsWith("/rowdetail")
)
for (let detailScreen of detailScreens) {
const idBinding = bindableProperties.find(p => {
if (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.model
) {
const modelId =
typeof p.model === "string" ? p.model : p.model.modelId
return modelId === detailScreen.props.model
}
return false
})
if (idBinding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.route.replace(
":id",
`{{ ${idBinding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
return urls
}
}
</script>
<DataList editable secondary on:blur={handleBlur} on:change bind:value>
<option value="" />
{#each $store.allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>

View File

@ -1,4 +1,4 @@
import { isString, isUndefined } from "lodash/fp"
import { isString, isUndefined, cloneDeep } from "lodash/fp"
import { TYPE_MAP } from "./types"
import { assign } from "lodash"
import { uuid } from "builderStore/uuid"
@ -83,13 +83,13 @@ const parsePropDef = propDef => {
if (isString(propDef)) {
if (!TYPE_MAP[propDef]) return error(`Type ${propDef} is not recognised.`)
return TYPE_MAP[propDef].default
return cloneDeep(TYPE_MAP[propDef].default)
}
const type = TYPE_MAP[propDef.type]
if (!type) return error(`Type ${propDef.type} is not recognised.`)
return propDef.default
return cloneDeep(propDef.default)
}
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>

View File

@ -10,7 +10,6 @@ export const TYPE_MAP = {
},
options: {
default: [],
options: [],
},
event: {
default: [],

View File

@ -361,19 +361,18 @@ export const typography = [
label: "Font",
key: "font-family",
control: OptionSelect,
defaultValue: "initial",
defaultValue: "Arial",
options: [
"initial",
"Arial",
"Arial Black",
"Cursive",
"Courier",
"Comic Sans MS",
"Helvetica",
"Helvetica Neue",
"Impact",
"Inter",
"Lucida Sans Unicode",
"Open Sans",
"Roboto",
"Roboto Mono",
"Times New Roman",
@ -467,9 +466,9 @@ export const background = [
label: "Gradient",
key: "background-image",
control: OptionSelect,
defaultValue: "None",
defaultValue: "",
options: [
{ label: "None", value: "None" },
{ label: "Select option", value: "" },
{
label: "Warm Flame",
value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
@ -518,9 +517,9 @@ export const background = [
},
{
label: "Image",
key: "background-image",
key: "background",
control: Input,
placeholder: "Src",
placeholder: "url",
},
]
@ -665,7 +664,7 @@ export const transitions = [
control: OptionSelect,
textAlign: "center",
placeholder: "sec",
options: ["0.2ms", "0.4ms", "0.8ms", "1s", "2s", "4s"],
options: ["0.4s", "0.6s", "0.8s", "1s", "2s", "4s"],
},
{
label: "Ease",

View File

@ -386,7 +386,7 @@ export default {
{
label: "destinationUrl",
key: "destinationUrl",
control: Input,
control: ScreenSelect,
placeholder: "/table/_id",
},
],
@ -435,7 +435,7 @@ export default {
{
label: "Link Url",
key: "linkUrl",
control: Input,
control: ScreenSelect,
placeholder: "Link URL",
},
{
@ -510,7 +510,7 @@ export default {
{
label: "Link Url",
key: "linkUrl",
control: Input,
control: ScreenSelect,
placeholder: "Link URL",
},
{

View File

@ -15,7 +15,7 @@ export const FIELDS = {
type: "options",
constraints: {
type: "string",
presence: { allowEmpty: true },
presence: false,
inclusion: [],
},
},
@ -67,7 +67,7 @@ export const FIELDS = {
type: "link",
constraints: {
type: "array",
presence: { allowEmpty: true },
presence: false,
},
},
}

View File

@ -33,6 +33,7 @@ exports.save = async function(ctx) {
views: {},
...rest,
}
let renameDocs = []
// if the model obj had an _id then it will have been retrieved
const oldModel = ctx.preExisting
@ -49,14 +50,11 @@ exports.save = async function(ctx) {
include_docs: true,
})
)
const docs = records.rows.map(({ doc }) => {
renameDocs = records.rows.map(({ doc }) => {
doc[_rename.updated] = doc[_rename.old]
delete doc[_rename.old]
return doc
})
await db.bulkDocs(docs)
delete modelToSave._rename
}
@ -69,9 +67,6 @@ exports.save = async function(ctx) {
modelView.schema = modelToSave.schema
}
const result = await db.post(modelToSave)
modelToSave._rev = result.rev
// update linked records
await linkRecords.updateLinks({
instanceId,
@ -82,6 +77,14 @@ exports.save = async function(ctx) {
oldModel: oldModel,
})
// don't perform any updates until relationships have been
// checked by the updateLinks function
if (renameDocs.length !== 0) {
await db.bulkDocs(renameDocs)
}
const result = await db.post(modelToSave)
modelToSave._rev = result.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
@ -105,11 +108,8 @@ exports.save = async function(ctx) {
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const modelToDelete = await db.get(ctx.params.modelId)
await db.remove(modelToDelete)
// Delete all records for that model
const records = await db.allDocs(
getRecordParams(ctx.params.modelId, null, {
@ -117,7 +117,7 @@ exports.destroy = async function(ctx) {
})
)
await db.bulkDocs(
records.rows.map(record => ({ _id: record.id, _deleted: true }))
records.rows.map(record => ({ ...record.doc, _deleted: true }))
)
// update linked records
@ -127,6 +127,9 @@ exports.destroy = async function(ctx) {
model: modelToDelete,
})
// don't remove the table itself until very end
await db.remove(modelToDelete)
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:delete`, instanceId, modelToDelete)
ctx.status = 200

View File

@ -136,7 +136,7 @@ exports.performLocalFileProcessing = async function(ctx) {
}
exports.serveApp = async function(ctx) {
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
// default to homedir
const appPath = resolve(
@ -154,7 +154,7 @@ exports.serveApp = async function(ctx) {
// 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.isAuthenticated) {
if (looksLikeAppId && !ctx.auth.authenticated) {
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
@ -200,7 +200,7 @@ exports.serveAttachment = async function(ctx) {
exports.serveAppAsset = async function(ctx) {
// default to homedir
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
const appPath = resolve(
budibaseAppsDir(),

View File

@ -24,6 +24,7 @@ app.use(
)
app.context.eventEmitter = eventEmitter
app.context.auth = {}
// api routes
app.use(api.routes())

View File

@ -161,7 +161,7 @@ class LinkController {
})
// now add the docs to be deleted to the bulk operation
operations.push(...toDeleteDocs)
// replace this field with a simple entry to denote there are links
// remove the field from this row, link doc will be added to record on way out
delete record[fieldName]
}
}
@ -234,8 +234,16 @@ class LinkController {
for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName]
if (field.type === "link") {
// handle this in a separate try catch, want
// the put to bubble up as an error, if can't update
// table for some reason
let linkedModel
try {
linkedModel = await this._db.get(field.modelId)
} catch (err) {
continue
}
// create the link field in the other model
const linkedModel = await this._db.get(field.modelId)
linkedModel.schema[field.fieldName] = {
name: field.fieldName,
type: "link",

View File

@ -42,6 +42,7 @@ exports.updateLinks = async function({
model,
oldModel,
}) {
const baseReturnObj = record == null ? model : record
if (instanceId == null) {
throw "Cannot operate without an instance ID."
}
@ -50,12 +51,16 @@ exports.updateLinks = async function({
arguments[0].modelId = model._id
}
let linkController = new LinkController(arguments[0])
if (
!(await linkController.doesModelHaveLinkedFields()) &&
(oldModel == null ||
!(await linkController.doesModelHaveLinkedFields(oldModel)))
) {
return record
try {
if (
!(await linkController.doesModelHaveLinkedFields()) &&
(oldModel == null ||
!(await linkController.doesModelHaveLinkedFields(oldModel)))
) {
return baseReturnObj
}
} catch (err) {
return baseReturnObj
}
switch (eventType) {
case EventType.RECORD_SAVE:

View File

@ -57,21 +57,26 @@ exports.generateModelID = () => {
/**
* Gets the DB allDocs/query params for retrieving a record.
* @param {string} modelId The model in which the records have been stored.
* @param {string|null} modelId The model in which the records have been stored.
* @param {string|null} recordId The ID of the record which is being specifically queried for. This can be
* left null to get all the records in the model.
* @param {object} otherProps Any other properties to add to the request.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
exports.getRecordParams = (modelId, recordId = null, otherProps = {}) => {
exports.getRecordParams = (
modelId = null,
recordId = null,
otherProps = {}
) => {
if (modelId == null) {
throw "Cannot build params for records without a model ID"
return getDocParams(DocumentTypes.RECORD, null, otherProps)
} else {
const endOfKey =
recordId == null
? `${modelId}${SEPARATOR}`
: `${modelId}${SEPARATOR}${recordId}`
return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
}
const endOfKey =
recordId == null
? `${modelId}${SEPARATOR}`
: `${modelId}${SEPARATOR}${recordId}`
return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
}
/**
@ -124,7 +129,14 @@ exports.generateAutomationID = () => {
* @returns {string} The new link doc ID which the automation doc can be stored under.
*/
exports.generateLinkID = (modelId1, modelId2, recordId1, recordId2) => {
return `${DocumentTypes.AUTOMATION}${SEPARATOR}${modelId1}${SEPARATOR}${modelId2}${SEPARATOR}${recordId1}${SEPARATOR}${recordId2}`
return `${DocumentTypes.LINK}${SEPARATOR}${modelId1}${SEPARATOR}${modelId2}${SEPARATOR}${recordId1}${SEPARATOR}${recordId2}`
}
/**
* Gets parameters for retrieving link docs, this is a utility function for the getDocParams function.
*/
exports.getLinkParams = (otherProps = {}) => {
return getDocParams(DocumentTypes.LINK, null, otherProps)
}
/**

View File

@ -20,8 +20,10 @@ module.exports = async (ctx, next) => {
if (builderToken) {
try {
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
ctx.apiKey = jwtPayload.apiKey
ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID
ctx.auth = {
apiKey: jwtPayload.apiKey,
authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID,
}
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
@ -38,14 +40,13 @@ module.exports = async (ctx, next) => {
}
if (!appToken) {
ctx.isAuthenticated = false
ctx.auth.authenticated = false
await next()
return
}
try {
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
ctx.apiKey = jwtPayload.apiKey
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
@ -53,7 +54,10 @@ module.exports = async (ctx, next) => {
jwtPayload.accessLevelId
),
}
ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID
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

@ -5,9 +5,36 @@ const {
BUILDER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient")
module.exports = (permName, getItemId) => async (ctx, next) => {
if (!ctx.isAuthenticated) {
if (
environment.CLOUD &&
ctx.headers["x-api-key"] &&
ctx.headers["x-instanceid"]
) {
// api key header passed by external webhook
const apiKeyInfo = await apiKeyTable.get({
primary: ctx.headers["x-api-key"],
})
if (apiKeyInfo) {
ctx.auth = {
authenticated: true,
external: true,
apiKey: ctx.headers["x-api-key"],
}
ctx.user = {
instanceId: ctx.headers["x-instanceid"],
}
return next()
}
ctx.throw(403, "API key invalid")
}
if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated")
}

View File

@ -55,7 +55,7 @@ module.exports = async (ctx, next) => {
return next()
}
try {
await usageQuota.update(ctx.apiKey, property, usage)
await usageQuota.update(ctx.auth.apiKey, property, usage)
return next()
} catch (err) {
ctx.throw(403, err)