Merge branch 'develop' into feature/oidc-support

This commit is contained in:
Rory Powell 2021-07-08 20:03:51 +01:00
commit 73c8dcf71b
126 changed files with 1856 additions and 1940 deletions

View File

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

3
packages/auth/cache.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
user: require("./src/cache/user"),
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.70", "version": "0.9.71",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -0,0 +1 @@
module.exports = require("./src/security/sessions")

21
packages/auth/src/cache/user.js vendored Normal file
View File

@ -0,0 +1,21 @@
const { getDB } = require("../db")
const { StaticDatabases } = require("../db/utils")
const redis = require("../redis/authRedis")
const EXPIRY_SECONDS = 3600
exports.getUser = async userId => {
const client = await redis.getUserClient()
// try cache
let user = await client.get(userId)
if (!user) {
user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
client.store(userId, user, EXPIRY_SECONDS)
}
return user
}
exports.invalidateUser = async userId => {
const client = await redis.getUserClient()
await client.delete(userId)
}

View File

@ -11,6 +11,7 @@ const {
auditLog, auditLog,
} = require("./middleware") } = require("./middleware")
const { setDB, getDB } = require("./db") const { setDB, getDB } = require("./db")
const userCache = require("./cache/user")
// Strategies // Strategies
passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new LocalStrategy(local.options, local.authenticate))
@ -55,6 +56,9 @@ module.exports = {
jwt: require("jsonwebtoken"), jwt: require("jsonwebtoken"),
auditLog, auditLog,
}, },
cache: {
user: userCache,
},
StaticDatabases, StaticDatabases,
constants: require("./constants"), constants: require("./constants"),
} }

View File

@ -1,7 +1,7 @@
const { Cookies } = require("../constants") const { Cookies } = require("../constants")
const database = require("../db")
const { getCookie, clearCookie } = require("../utils") const { getCookie, clearCookie } = require("../utils")
const { StaticDatabases } = require("../db/utils") const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const env = require("../environment") const env = require("../environment")
const PARAM_REGEX = /\/:(.*?)\//g const PARAM_REGEX = /\/:(.*?)\//g
@ -48,14 +48,27 @@ module.exports = (noAuthPatterns = [], opts) => {
user = null, user = null,
internal = false internal = false
if (authCookie) { if (authCookie) {
try { let error = null
const db = database.getDB(StaticDatabases.GLOBAL.name) const sessionId = authCookie.sessionId,
user = await db.get(authCookie.userId) userId = authCookie.userId
delete user.password const session = await getSession(userId, sessionId)
authenticated = true if (!session) {
} catch (err) { error = "No session found"
// remove the cookie as the use does not exist anymore } else {
try {
user = await getUser(userId)
delete user.password
authenticated = true
} catch (err) {
error = err
}
}
if (error) {
// remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookies.Auth)
} else {
// make sure we denote that the session is still in use
await updateSessionTTL(session)
} }
} }
const apiKey = ctx.request.headers["x-budibase-api-key"] const apiKey = ctx.request.headers["x-budibase-api-key"]

View File

@ -4,6 +4,8 @@ const { compare } = require("../../hashing")
const env = require("../../environment") const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils") const { getGlobalUserByEmail } = require("../../utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid Credentials"
@ -32,13 +34,16 @@ exports.authenticate = async function (email, password, done) {
// authenticate // authenticate
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const payload = { const sessionId = newid()
userId: dbUser._id, await createASession(dbUser._id, sessionId)
}
dbUser.token = jwt.sign(payload, env.JWT_SECRET, { dbUser.token = jwt.sign(
expiresIn: "1 day", {
}) userId: dbUser._id,
sessionId,
},
env.JWT_SECRET
)
// Remove users password in payload // Remove users password in payload
delete dbUser.password delete dbUser.password

View File

@ -110,7 +110,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
userInfoURL: body.userinfo_endpoint, userInfoURL: body.userinfo_endpoint,
clientID: clientId, clientID: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
callbackURL: callbackUrl callbackURL: callbackUrl,
}, },
authenticate authenticate
) )

View File

@ -7,6 +7,8 @@ const {
ViewNames, ViewNames,
} = require("../../db/utils") } = require("../../db/utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
/** /**
* Common authentication logic for third parties. e.g. OAuth, OIDC. * Common authentication logic for third parties. e.g. OAuth, OIDC.
@ -57,7 +59,7 @@ exports.authenticateThirdParty = async function (
} }
// exit early if there is still no user and auto creation is disabled // exit early if there is still no user and auto creation is disabled
if (!dbUser && requireLocalAccount ) { if (!dbUser && requireLocalAccount) {
if (requireLocalAccount) { if (requireLocalAccount) {
return authError( return authError(
done, done,
@ -82,15 +84,16 @@ exports.authenticateThirdParty = async function (
dbUser._rev = response.rev dbUser._rev = response.rev
// authenticate // authenticate
const payload = { const sessionId = newid()
userId: dbUser._id, await createASession(dbUser._id, sessionId)
builder: dbUser.builder,
email: dbUser.email,
}
dbUser.token = jwt.sign(payload, env.JWT_SECRET, { dbUser.token = jwt.sign(
expiresIn: "1 day", {
}) userId: dbUser._id,
sessionId,
},
env.JWT_SECRET
)
return done(null, dbUser) return done(null, dbUser)
} }
@ -120,7 +123,7 @@ function syncUser(user, thirdPartyUser) {
user.lastName = name.familyName user.lastName = name.familyName
} }
} }
// profile // profile
// @reviewers: Historically stored at the root level of the user // @reviewers: Historically stored at the root level of the user
// Nest to prevent conflicts with future fields // Nest to prevent conflicts with future fields

View File

@ -0,0 +1,29 @@
const Client = require("./index")
const utils = require("./utils")
let userClient, sessionClient
async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).init()
sessionClient = await new Client(utils.Databases.SESSIONS).init()
}
process.on("exit", async () => {
if (userClient) await userClient.finish()
if (sessionClient) await sessionClient.finish()
})
module.exports = {
getUserClient: async () => {
if (!userClient) {
await init()
}
return userClient
},
getSessionClient: async () => {
if (!sessionClient) {
await init()
}
return sessionClient
},
}

View File

@ -1,7 +1,12 @@
const env = require("../environment") const env = require("../environment")
// ioredis mock is all in memory // ioredis mock is all in memory
const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis")
const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils") const {
addDbPrefix,
removeDbPrefix,
getRedisOptions,
SEPARATOR,
} = require("./utils")
const RETRY_PERIOD_MS = 2000 const RETRY_PERIOD_MS = 2000
const STARTUP_TIMEOUT_MS = 5000 const STARTUP_TIMEOUT_MS = 5000
@ -143,14 +148,15 @@ class RedisWrapper {
CLIENT.disconnect() CLIENT.disconnect()
} }
async scan() { async scan(key = "") {
const db = this._db const db = this._db
key = `${db}${SEPARATOR}${key}`
let stream let stream
if (CLUSTERED) { if (CLUSTERED) {
let node = CLIENT.nodes("master") let node = CLIENT.nodes("master")
stream = node[0].scanStream({ match: db + "-*", count: 100 }) stream = node[0].scanStream({ match: key + "*", count: 100 })
} else { } else {
stream = CLIENT.scanStream({ match: db + "-*", count: 100 }) stream = CLIENT.scanStream({ match: key + "*", count: 100 })
} }
return promisifyStream(stream) return promisifyStream(stream)
} }
@ -182,6 +188,12 @@ class RedisWrapper {
} }
} }
async setExpiry(key, expirySeconds) {
const db = this._db
const prefixedKey = addDbPrefix(db, key)
await CLIENT.expire(prefixedKey, expirySeconds)
}
async delete(key) { async delete(key) {
const db = this._db const db = this._db
await CLIENT.del(addDbPrefix(db, key)) await CLIENT.del(addDbPrefix(db, key))

View File

@ -11,8 +11,12 @@ exports.Databases = {
INVITATIONS: "invitation", INVITATIONS: "invitation",
DEV_LOCKS: "devLocks", DEV_LOCKS: "devLocks",
DEBOUNCE: "debounce", DEBOUNCE: "debounce",
SESSIONS: "session",
USER_CACHE: "users",
} }
exports.SEPARATOR = SEPARATOR
exports.getRedisOptions = (clustered = false) => { exports.getRedisOptions = (clustered = false) => {
const [host, port] = REDIS_URL.split(":") const [host, port] = REDIS_URL.split(":")
const opts = { const opts = {

View File

@ -147,7 +147,7 @@ exports.getRole = async (appId, roleId) => {
*/ */
async function getAllUserRoles(appId, userRoleId) { async function getAllUserRoles(appId, userRoleId) {
if (!userRoleId) { if (!userRoleId) {
return [BUILTIN_IDS.PUBLIC] return [BUILTIN_IDS.BASIC]
} }
let currentRole = await exports.getRole(appId, userRoleId) let currentRole = await exports.getRole(appId, userRoleId)
let roles = currentRole ? [currentRole] : [] let roles = currentRole ? [currentRole] : []
@ -226,7 +226,7 @@ exports.getAllRoles = async appId => {
dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId
)[0] )[0]
if (dbBuiltin == null) { if (dbBuiltin == null) {
roles.push(builtinRole) roles.push(builtinRole || builtinRoles.BASIC)
} else { } else {
// remove role and all back after combining with the builtin // remove role and all back after combining with the builtin
roles = roles.filter(role => role._id !== dbBuiltin._id) roles = roles.filter(role => role._id !== dbBuiltin._id)

View File

@ -0,0 +1,69 @@
const redis = require("../redis/authRedis")
const EXPIRY_SECONDS = 86400
async function getSessionsForUser(userId) {
const client = await redis.getSessionClient()
const sessions = await client.scan(userId)
return sessions.map(session => session.value)
}
function makeSessionID(userId, sessionId) {
return `${userId}/${sessionId}`
}
exports.createASession = async (userId, sessionId) => {
const client = await redis.getSessionClient()
const session = {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
sessionId,
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
}
exports.invalidateSessions = async (userId, sessionId = null) => {
let sessions = []
if (sessionId) {
sessions.push({ key: makeSessionID(userId, sessionId) })
} else {
sessions = await getSessionsForUser(userId)
}
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
}
await Promise.all(promises)
}
exports.updateSessionTTL = async session => {
const client = await redis.getSessionClient()
const key = makeSessionID(session.userId, session.sessionId)
session.lastAccessedAt = new Date().toISOString()
await client.store(key, session, EXPIRY_SECONDS)
}
exports.endSession = async (userId, sessionId) => {
const client = await redis.getSessionClient()
await client.delete(makeSessionID(userId, sessionId))
}
exports.getUserSessions = getSessionsForUser
exports.getSession = async (userId, sessionId) => {
try {
const client = await redis.getSessionClient()
return client.get(makeSessionID(userId, sessionId))
} catch (err) {
// if can't get session don't error, just don't return anything
return null
}
}
exports.getAllSessions = async () => {
const client = await redis.getSessionClient()
const sessions = await client.scan()
return sessions.map(session => session.value)
}

View File

@ -64,23 +64,18 @@ exports.getCookie = (ctx, name) => {
} }
/** /**
* Store a cookie for the request, has a hardcoded expiry. * Store a cookie for the request - it will not expire.
* @param {object} ctx The request which is to be manipulated. * @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to set. * @param {string} name The name of the cookie to set.
* @param {string|object} value The value of cookie which will be set. * @param {string|object} value The value of cookie which will be set.
*/ */
exports.setCookie = (ctx, value, name = "builder") => { exports.setCookie = (ctx, value, name = "builder") => {
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!value) { if (!value) {
ctx.cookies.set(name) ctx.cookies.set(name)
} else { } else {
value = jwt.sign(value, options.secretOrKey, { value = jwt.sign(value, options.secretOrKey)
expiresIn: "1 day",
})
ctx.cookies.set(name, value, { ctx.cookies.set(name, value, {
expires, maxAge: Number.MAX_SAFE_INTEGER,
path: "/", path: "/",
httpOnly: false, httpOnly: false,
overwrite: true, overwrite: true,

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "0.9.70", "version": "0.9.71",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -9,10 +9,10 @@
export let value export let value
export let size = "M" export let size = "M"
export let spectrumTheme
let open = false let open = false
$: color = value || "transparent"
$: customValue = getCustomValue(value) $: customValue = getCustomValue(value)
$: checkColor = getCheckColor(value) $: checkColor = getCheckColor(value)
@ -21,7 +21,8 @@
{ {
label: "Grays", label: "Grays",
colors: [ colors: [
"white", "gray-50",
"gray-75",
"gray-100", "gray-100",
"gray-200", "gray-200",
"gray-300", "gray-300",
@ -31,7 +32,6 @@
"gray-700", "gray-700",
"gray-800", "gray-800",
"gray-900", "gray-900",
"black",
], ],
}, },
{ {
@ -86,7 +86,7 @@
return value return value
} }
let found = false let found = false
const comparisonValue = value.substring(35, value.length - 1) const comparisonValue = value.substring(28, value.length - 1)
for (let category of categories) { for (let category of categories) {
found = category.colors.includes(comparisonValue) found = category.colors.includes(comparisonValue)
if (found) { if (found) {
@ -102,17 +102,19 @@
const getCheckColor = value => { const getCheckColor = value => {
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value) return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value)
? "black" ? "var(--spectrum-global-color-gray-900)"
: "white" : "var(--spectrum-global-color-gray-50)"
} }
</script> </script>
<div class="container"> <div class="container">
<div <div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
class="preview size--{size || 'M'}" <div
style="background: {color};" class="fill {spectrumTheme || ''}"
on:click={() => (open = true)} style={value ? `background: ${value};` : ""}
/> class:placeholder={!value}
/>
</div>
{#if open} {#if open}
<div <div
use:clickOutside={() => (open = false)} use:clickOutside={() => (open = false)}
@ -126,15 +128,19 @@
{#each category.colors as color} {#each category.colors as color}
<div <div
on:click={() => { on:click={() => {
onChange(`var(--spectrum-global-color-static-${color})`) onChange(`var(--spectrum-global-color-${color})`)
}} }}
class="color" class="color"
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};"
title={prettyPrint(color)} title={prettyPrint(color)}
> >
{#if value === `var(--spectrum-global-color-static-${color})`} <div
<Icon name="Checkmark" size="S" /> class="fill {spectrumTheme || ''}"
{/if} style="background: var(--spectrum-global-color-{color}); color: {checkColor};"
>
{#if value === `var(--spectrum-global-color-${color})`}
<Icon name="Checkmark" size="S" />
{/if}
</div>
</div> </div>
{/each} {/each}
</div> </div>
@ -170,12 +176,43 @@
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 100%; border-radius: 100%;
position: relative;
transition: border-color 130ms ease-in-out; transition: border-color 130ms ease-in-out;
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-400);
} }
.preview:hover { .preview:hover {
cursor: pointer; cursor: pointer;
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-400);
}
.fill {
width: 100%;
height: 100%;
border-radius: 100%;
position: absolute;
top: 0;
left: 0;
display: grid;
place-items: center;
}
.fill.placeholder {
background-position: 0 0, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(
45deg,
#eee 25%,
transparent 25%,
transparent 75%,
#eee 75%,
#eee 100%
),
linear-gradient(
45deg,
#eee 25%,
white 25%,
white 75%,
#eee 75%,
#eee 100%
);
} }
.size--S { .size--S {
width: 20px; width: 20px;
@ -219,8 +256,7 @@
width: 16px; width: 16px;
border-radius: 100%; border-radius: 100%;
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
display: grid; position: relative;
place-items: center;
} }
.color:hover { .color:hover {
cursor: pointer; cursor: pointer;
@ -236,4 +272,8 @@
.category--custom .heading { .category--custom .heading {
margin-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);
} }
.spectrum-wrapper {
background-color: transparent;
}
</style> </style>

View File

@ -145,4 +145,7 @@
height: 100vh; height: 100vh;
z-index: 999; z-index: 999;
} }
:global(.flatpickr-calendar) {
font-family: "Source Sans Pro", sans-serif;
}
</style> </style>

View File

@ -15,8 +15,12 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: selected = $tab.title $: {
$: selected = dispatch("select", selected) if ($tab.title !== selected) {
selected = $tab.title
dispatch("select", selected)
}
}
let top, left, width, height let top, left, width, height
$: calculateIndicatorLength($tab) $: calculateIndicatorLength($tab)

View File

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 48 48" height="48px" viewBox="0 0 48 48" width="48px" xml:space="preserve">
<g>
<g>
<path d="M9,42H3c-0.552,0-1-0.449-1-1v-3.5C2,37.224,2.224,37,2.5,37S3,37.224,3,37.5V41h6 c0.276,0,0.5,0.224,0.5,0.5S9.276,42,9,42z"/>
<path d="M45,42h-6c-0.276,0-0.5-0.224-0.5-0.5S38.724,41,39,41h6V13H3v27c0,0.276-0.224,0.5-0.5,0.5S2,40.276,2,40 V12h44v29C46,41.551,45.552,42,45,42z"/>
<g>
<path d="M45.5,13h-43C2.224,13,2,12.776,2,12.5v-5C2,6.673,2.673,6,3.5,6h41C45.327,6,46,6.673,46,7.5v5 C46,12.776,45.776,13,45.5,13z M3,12h42V7.5C45,7.224,44.775,7,44.5,7h-41C3.225,7,3,7.224,3,7.5V12z"/>
</g>
<g>
<g>
<path d="M16.5,11c-0.827,0-1.5-0.673-1.5-1.5S15.673,8,16.5,8S18,8.673,18,9.5S17.327,11,16.5,11z M16.5,9 C16.225,9,16,9.224,16,9.5s0.225,0.5,0.5,0.5S17,9.776,17,9.5S16.775,9,16.5,9z"/>
</g>
<g>
<path d="M11.5,11c-0.827,0-1.5-0.673-1.5-1.5S10.673,8,11.5,8S13,8.673,13,9.5S12.327,11,11.5,11z M11.5,9 C11.225,9,11,9.224,11,9.5s0.225,0.5,0.5,0.5S12,9.776,12,9.5S11.775,9,11.5,9z"/>
</g>
<g>
<path d="M6.5,11C5.673,11,5,10.327,5,9.5S5.673,8,6.5,8S8,8.673,8,9.5S7.327,11,6.5,11z M6.5,9 C6.225,9,6,9.224,6,9.5S6.225,10,6.5,10S7,9.776,7,9.5S6.775,9,6.5,9z"/>
</g>
</g>
</g>
<g>
<g>
<path d="M35.696,44H12.304c-0.728,0-1.313-0.284-1.605-0.779c-0.289-0.489-0.259-1.126,0.084-1.749L22.58,19.996 c0.709-1.285,2.132-1.285,2.839,0l11.799,21.477v0c0.343,0.623,0.373,1.26,0.084,1.749C37.01,43.716,36.424,44,35.696,44z M24,20 c-0.176,0-0.379,0.179-0.544,0.478L11.659,41.954c-0.168,0.306-0.205,0.582-0.101,0.758C11.667,42.895,11.938,43,12.304,43 h23.393c0.365,0,0.637-0.105,0.745-0.288c0.104-0.177,0.067-0.453-0.101-0.758v0L24.543,20.478C24.379,20.179,24.176,20,24,20z"/>
</g>
<g>
<path d="M24,36L24,36c-0.225,0-0.421-0.15-0.481-0.366C23.456,35.412,22,30.169,22,28c0-1.103,0.897-2,2-2 s2,0.897,2,2c0,2.232-1.457,7.417-1.519,7.636C24.42,35.851,24.224,36,24,36z M24,27c-0.552,0-1,0.449-1,1 c0,1.266,0.569,3.793,1.002,5.531C24.435,31.806,25,29.301,25,28C25,27.449,24.552,27,24,27z"/>
</g>
<g>
<path d="M24,41c-1.103,0-2-0.897-2-2s0.897-2,2-2s2,0.897,2,2S25.103,41,24,41z M24,38c-0.552,0-1,0.449-1,1 s0.448,1,1,1s1-0.449,1-1S24.552,38,24,38z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -29,7 +29,7 @@ context("Create Bindings", () => {
// The builder preview pages don't have a real URL, so all we can do // The builder preview pages don't have a real URL, so all we can do
// is check that we were able to bind to the property, and that the // is check that we were able to bind to the property, and that the
// component exists on the page // component exists on the page
cy.getComponent(componentId).should("have.text", "Placeholder text") cy.getComponent(componentId).should("have.text", "New Paragraph")
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.70", "version": "0.9.71",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.70", "@budibase/bbui": "^0.9.71",
"@budibase/client": "^0.9.70", "@budibase/client": "^0.9.71",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.70", "@budibase/string-templates": "^0.9.71",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -8,7 +8,6 @@ import {
selectedComponent, selectedComponent,
selectedAccessRole, selectedAccessRole,
} from "builderStore" } from "builderStore"
// Backendstores
import { import {
datasources, datasources,
integrations, integrations,
@ -43,6 +42,7 @@ const INITIAL_FRONTEND_STATE = {
appId: "", appId: "",
routes: {}, routes: {},
clientLibPath: "", clientLibPath: "",
theme: "",
} }
export const getFrontendStore = () => { export const getFrontendStore = () => {
@ -62,6 +62,7 @@ export const getFrontendStore = () => {
url: application.url, url: application.url,
layouts, layouts,
screens, screens,
theme: application.theme,
hasAppPackage: true, hasAppPackage: true,
appInstance: application.instance, appInstance: application.instance,
clientLibPath, clientLibPath,
@ -79,6 +80,20 @@ export const getFrontendStore = () => {
database.set(application.instance) database.set(application.instance)
tables.init() tables.init()
}, },
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
}
},
},
routing: { routing: {
fetch: async () => { fetch: async () => {
const response = await api.get("/api/routing") const response = await api.get("/api/routing")
@ -122,6 +137,9 @@ export const getFrontendStore = () => {
save: async screen => { save: async screen => {
const creatingNewScreen = screen._id === undefined const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen) const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json() screen = await response.json()
await store.actions.routing.fetch() await store.actions.routing.fetch()
@ -196,6 +214,11 @@ export const getFrontendStore = () => {
const response = await api.post(`/api/layouts`, layoutToSave) const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json() const savedLayout = await response.json()
// Abort if saving failed
if (response.status !== 200) {
return
}
store.update(state => { store.update(state => {
const layoutIdx = state.layouts.findIndex( const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === savedLayout._id stateLayout => stateLayout._id === savedLayout._id
@ -313,16 +336,6 @@ export const getFrontendStore = () => {
create: async (componentName, presetProps) => { create: async (componentName, presetProps) => {
const selected = get(selectedComponent) const selected = get(selectedComponent)
const asset = get(currentAsset) const asset = get(currentAsset)
const state = get(store)
// Only allow one screen slot, and in the layout
if (componentName.endsWith("screenslot")) {
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
const slot = findComponentType(asset.props, componentName)
if (!isLayout || slot != null) {
return
}
}
// Create new component // Create new component
const componentInstance = store.actions.components.createInstance( const componentInstance = store.actions.components.createInstance(

View File

@ -38,8 +38,6 @@ const createScreen = table => {
.instanceName("Form") .instanceName("Form")
.customProps({ .customProps({
actionType: "Create", actionType: "Create",
theme: "spectrum--lightest",
size: "spectrum--medium",
dataSource: { dataSource: {
label: table.name, label: table.name,
tableId: table._id, tableId: table._id,

View File

@ -8,7 +8,6 @@ import {
makeTitleContainer, makeTitleContainer,
makeSaveButton, makeSaveButton,
makeMainForm, makeMainForm,
spectrumColor,
makeDatasourceFormComponents, makeDatasourceFormComponents,
} from "./utils/commonComponents" } from "./utils/commonComponents"
@ -26,36 +25,13 @@ export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
function generateTitleContainer(table, title, formId, repeaterId) { function generateTitleContainer(table, title, formId, repeaterId) {
// have to override style for this, its missing margin const saveButton = makeSaveButton(table, formId)
const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-weight": "600",
"font-size": "14px",
})
const deleteButton = new Component("@budibase/standard-components/button") const deleteButton = new Component("@budibase/standard-components/button")
.normalStyle({
background: "transparent",
"border-width": "0",
"border-style": "None",
color: "#9e9e9e",
"font-weight": "600",
"font-size": "14px",
"margin-right": "8px",
"margin-left": "16px",
})
.hoverStyle({
background: "transparent",
color: "#4285f4",
})
.customStyle(spectrumColor(700))
.text("Delete") .text("Delete")
.customProps({ .customProps({
className: "", type: "secondary",
disabled: false, quiet: true,
size: "M",
onClick: [ onClick: [
{ {
parameters: { parameters: {
@ -76,7 +52,19 @@ function generateTitleContainer(table, title, formId, repeaterId) {
}) })
.instanceName("Delete Button") .instanceName("Delete Button")
return makeTitleContainer(title).addChild(deleteButton).addChild(saveButton) const buttons = new Component("@budibase/standard-components/container")
.instanceName("Button Container")
.customProps({
direction: "row",
hAlign: "right",
vAlign: "middle",
size: "shrink",
gap: "M",
})
.addChild(deleteButton)
.addChild(saveButton)
return makeTitleContainer(title).addChild(buttons)
} }
const createScreen = table => { const createScreen = table => {
@ -98,6 +86,7 @@ const createScreen = table => {
valueType: "Binding", valueType: "Binding",
}, },
], ],
limit: table.type === "external" ? undefined : 1,
paginate: false, paginate: false,
}) })

View File

@ -19,21 +19,10 @@ export const rowListUrl = table => sanitizeUrl(`/${table.name}`)
function generateTitleContainer(table) { function generateTitleContainer(table) {
const newButton = new Component("@budibase/standard-components/button") const newButton = new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-weight": "600",
"font-size": "14px",
})
.hoverStyle({
background: "#4285f4",
})
.text("Create New") .text("Create New")
.customProps({ .customProps({
className: "", size: "M",
disabled: false, type: "primary",
onClick: [ onClick: [
{ {
parameters: { parameters: {
@ -46,12 +35,6 @@ function generateTitleContainer(table) {
.instanceName("New Button") .instanceName("New Button")
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/heading")
.normalStyle({
margin: "0px",
flex: "1 1 auto",
"text-transform": "capitalize",
})
.type("h2")
.instanceName("Title") .instanceName("Title")
.text(table.name) .text(table.name)
.customProps({ .customProps({
@ -60,14 +43,12 @@ function generateTitleContainer(table) {
}) })
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/container")
.normalStyle({
"margin-bottom": "32px",
})
.customProps({ .customProps({
direction: "row", direction: "row",
hAlign: "stretch", hAlign: "stretch",
vAlign: "middle", vAlign: "middle",
size: "shrink", size: "shrink",
gap: "M",
}) })
.instanceName("Title Container") .instanceName("Title Container")
.addChild(heading) .addChild(heading)
@ -91,68 +72,35 @@ const createScreen = table => {
const spectrumTable = new Component("@budibase/standard-components/table") const spectrumTable = new Component("@budibase/standard-components/table")
.customProps({ .customProps({
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`, dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
theme: "spectrum--lightest",
showAutoColumns: false, showAutoColumns: false,
quiet: true, quiet: false,
size: "spectrum--medium",
rowCount: 8, rowCount: 8,
}) })
.instanceName(`${table.name} Table`) .instanceName(`${table.name} Table`)
const safeTableId = makePropSafe(spectrumTable._json._id) const safeTableId = makePropSafe(spectrumTable._json._id)
const safeRowId = makePropSafe("_id") const safeRowId = makePropSafe("_id")
const viewButton = new Component("@budibase/standard-components/button") const viewLink = new Component("@budibase/standard-components/link")
.customProps({ .customProps({
text: "View", text: "View",
onClick: [ url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`,
{ size: "S",
"##eventHandlerType": "Navigate To", color: "var(--spectrum-global-color-gray-600)",
parameters: { align: "left",
url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`,
},
},
],
}) })
.instanceName("View Button")
.normalStyle({ .normalStyle({
background: "transparent", ["margin-left"]: "16px",
"font-weight": "600", ["margin-right"]: "16px",
color: "#888",
"border-width": "0",
})
.hoverStyle({
color: "#4285f4",
}) })
.instanceName("View Link")
spectrumTable.addChild(viewButton) spectrumTable.addChild(viewLink)
provider.addChild(spectrumTable) provider.addChild(spectrumTable)
const mainContainer = new Component("@budibase/standard-components/container")
.normalStyle({
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"border-width": "2px",
"border-color": "rgba(0, 0, 0, 0.1)",
"border-style": "None",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
})
.customProps({
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "shrink",
})
.instanceName("Container")
.addChild(generateTitleContainer(table))
.addChild(provider)
return new Screen() return new Screen()
.route(rowListUrl(table)) .route(rowListUrl(table))
.instanceName(`${table.name} - List`) .instanceName(`${table.name} - List`)
.addChild(mainContainer) .addChild(generateTitleContainer(table))
.addChild(provider)
.json() .json()
} }

View File

@ -8,23 +8,16 @@ export function spectrumColor(number) {
// God knows why. It seems to think optional chaining further down the // God knows why. It seems to think optional chaining further down the
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this // file is invalid if the word g-l-o-b-a-l is found - hence the reason this
// statement is split into parts. // statement is split into parts.
return "color: var(--spectrum-glo" + `bal-color-gray-${number});` return "var(--spectrum-glo" + `bal-color-gray-${number})`
} }
export function makeLinkComponent(tableName) { export function makeLinkComponent(tableName) {
return new Component("@budibase/standard-components/link") return new Component("@budibase/standard-components/link")
.normalStyle({
color: "#757575",
"text-transform": "capitalize",
})
.hoverStyle({
color: "#4285f4",
})
.customStyle(spectrumColor(700))
.text(tableName) .text(tableName)
.customProps({ .customProps({
url: `/${tableName.toLowerCase()}`, url: `/${tableName.toLowerCase()}`,
openInNewTab: false, openInNewTab: false,
color: spectrumColor(700),
size: "S", size: "S",
align: "left", align: "left",
}) })
@ -33,19 +26,12 @@ export function makeLinkComponent(tableName) {
export function makeMainForm() { export function makeMainForm() {
return new Component("@budibase/standard-components/form") return new Component("@budibase/standard-components/form")
.normalStyle({ .normalStyle({
width: "700px", width: "600px",
padding: "0px",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
}) })
.instanceName("Form") .instanceName("Form")
} }
export function makeBreadcrumbContainer(tableName, text, capitalise = false) { export function makeBreadcrumbContainer(tableName, text) {
const link = makeLinkComponent(tableName).instanceName("Back Link") const link = makeLinkComponent(tableName).instanceName("Back Link")
const arrowText = new Component("@budibase/standard-components/text") const arrowText = new Component("@budibase/standard-components/text")
@ -53,42 +39,27 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.normalStyle({ .normalStyle({
"margin-right": "4px", "margin-right": "4px",
"margin-left": "4px", "margin-left": "4px",
"margin-top": "0px",
"margin-bottom": "0px",
}) })
.customStyle(spectrumColor(700))
.text(">") .text(">")
.instanceName("Arrow") .instanceName("Arrow")
.customProps({ .customProps({
color: spectrumColor(700),
size: "S", size: "S",
align: "left", align: "left",
}) })
const textStyling = {
color: "#000000",
"margin-top": "0px",
"margin-bottom": "0px",
}
if (capitalise) {
textStyling["text-transform"] = "capitalize"
}
const identifierText = new Component("@budibase/standard-components/text") const identifierText = new Component("@budibase/standard-components/text")
.type("none")
.normalStyle(textStyling)
.customStyle(spectrumColor(700))
.text(text) .text(text)
.instanceName("Identifier") .instanceName("Identifier")
.customProps({ .customProps({
color: spectrumColor(700),
size: "S", size: "S",
align: "left", align: "left",
}) })
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/container")
.normalStyle({
"font-size": "14px",
color: "#757575",
})
.customProps({ .customProps({
gap: "N",
direction: "row", direction: "row",
hAlign: "left", hAlign: "left",
vAlign: "middle", vAlign: "middle",
@ -102,22 +73,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
export function makeSaveButton(table, formId) { export function makeSaveButton(table, formId) {
return new Component("@budibase/standard-components/button") return new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-weight": "600",
"font-size": "14px",
"margin-left": "16px",
})
.hoverStyle({
background: "#4285f4",
})
.text("Save") .text("Save")
.customProps({ .customProps({
className: "", type: "primary",
disabled: false, size: "M",
onClick: [ onClick: [
{ {
"##eventHandlerType": "Validate Form", "##eventHandlerType": "Validate Form",
@ -145,12 +104,6 @@ export function makeSaveButton(table, formId) {
export function makeTitleContainer(title) { export function makeTitleContainer(title) {
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/heading")
.normalStyle({
margin: "0px",
flex: "1 1 auto",
})
.customStyle(spectrumColor(900))
.type("h2")
.instanceName("Title") .instanceName("Title")
.text(title) .text(title)
.customProps({ .customProps({
@ -168,6 +121,7 @@ export function makeTitleContainer(title) {
hAlign: "stretch", hAlign: "stretch",
vAlign: "middle", vAlign: "middle",
size: "shrink", size: "shrink",
gap: "M",
}) })
.instanceName("Title Container") .instanceName("Title Container")
.addChild(heading) .addChild(heading)

View File

@ -24,7 +24,7 @@
name: $views.selected?.name, name: $views.selected?.name,
} }
$: type = $tables.selected?.type $: type = $tables.selected?.type
$: isInternal = type === "internal" $: isInternal = type !== "external"
// Fetch rows for specified table // Fetch rows for specified table
$: { $: {
@ -72,9 +72,7 @@
{#if isUsersTable} {#if isUsersTable}
<EditRolesButton /> <EditRolesButton />
{/if} {/if}
{#if isInternal} <HideAutocolumnButton bind:hideAutocolumns />
<HideAutocolumnButton bind:hideAutocolumns />
{/if}
<!-- always have the export last --> <!-- always have the export last -->
<ExportButton view={$tables.selected?._id} /> <ExportButton view={$tables.selected?._id} />
{/if} {/if}

View File

@ -10,8 +10,10 @@
let selectedRole = {} let selectedRole = {}
let errors = [] let errors = []
let builtInRoles = ["Admin", "Power", "Basic", "Public"] let builtInRoles = ["Admin", "Power", "Basic", "Public"]
// Don't allow editing of public role
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
$: selectedRoleId = selectedRole._id $: selectedRoleId = selectedRole._id
$: otherRoles = $roles.filter(role => role._id !== selectedRoleId) $: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
$: isCreating = selectedRoleId == null || selectedRoleId === "" $: isCreating = selectedRoleId == null || selectedRoleId === ""
const fetchBasePermissions = async () => { const fetchBasePermissions = async () => {
@ -96,7 +98,7 @@
label="Role" label="Role"
value={selectedRoleId} value={selectedRoleId}
on:change={changeRole} on:change={changeRole}
options={$roles} options={editableRoles}
placeholder="Create new role" placeholder="Create new role"
getOptionValue={role => role._id} getOptionValue={role => role._id}
getOptionLabel={role => role.name} getOptionLabel={role => role.name}

View File

@ -5,14 +5,17 @@
import ICONS from "../icons" import ICONS from "../icons"
export let integration = {} export let integration = {}
let integrations = [] let integrations = []
const INTERNAL = "BUDIBASE"
async function fetchIntegrations() { async function fetchIntegrations() {
const response = await api.get("/api/integrations") const response = await api.get("/api/integrations")
const json = await response.json() const json = await response.json()
integrations = json integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
}
return json return json
} }
@ -21,7 +24,7 @@
// build the schema // build the schema
const schema = {} const schema = {}
for (let key in selected.datasource) { for (let key of Object.keys(selected.datasource)) {
schema[key] = selected.datasource[key].default schema[key] = selected.datasource[key].default
} }
@ -39,7 +42,7 @@
<section> <section>
<div class="integration-list"> <div class="integration-list">
{#each Object.keys(integrations) as integrationType} {#each Object.entries(integrations) as [integrationType, schema]}
<div <div
class="integration hoverable" class="integration hoverable"
class:selected={integration.type === integrationType} class:selected={integration.type === integrationType}
@ -50,7 +53,7 @@
height="50" height="50"
width="50" width="50"
/> />
<Body size="XS">{integrationType}</Body> <Body size="XS">{schema.name || integrationType}</Body>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -2,15 +2,21 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { Input, Label, ModalContent } from "@budibase/bbui" import { Input, Label, ModalContent, Modal, Context } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte" import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import analytics from "analytics" import analytics from "analytics"
import { getContext } from "svelte"
let error = "" const modalContext = getContext(Context.Modal)
let tableModal
let name let name
let error = ""
let integration let integration
$: checkOpenModal(integration && integration.type === "BUDIBASE")
function checkValid(evt) { function checkValid(evt) {
const datasourceName = evt.target.value const datasourceName = evt.target.value
if ( if (
@ -22,6 +28,12 @@
error = "" error = ""
} }
function checkOpenModal(isInternal) {
if (isInternal) {
tableModal.show()
}
}
async function saveDatasource() { async function saveDatasource() {
const { type, plus, ...config } = integration const { type, plus, ...config } = integration
@ -40,6 +52,9 @@
} }
</script> </script>
<Modal bind:this={tableModal} on:hide={modalContext.hide}>
<CreateTableModal bind:name />
</Modal>
<ModalContent <ModalContent
title="Create Datasource" title="Create Datasource"
size="L" size="L"

View File

@ -1,5 +1,5 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto, url } from "@roxi/routify"
import { store } from "builderStore" import { store } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
@ -27,7 +27,7 @@
$: tableNames = $tables.list.map(table => table.name) $: tableNames = $tables.list.map(table => table.name)
let name export let name
let dataImport let dataImport
let error = "" let error = ""
let createAutoscreens = true let createAutoscreens = true
@ -91,7 +91,11 @@
} }
// Navigate to new table // Navigate to new table
$goto(`../../table/${table._id}`) const currentUrl = $url()
const path = currentUrl.endsWith("data")
? `./table/${table._id}`
: `../../table/${table._id}`
$goto(path)
} }
</script> </script>

View File

@ -0,0 +1,97 @@
<script>
import { Icon, Combobox, Drawer, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { createEventDispatcher } from "svelte"
export let panel = BindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
export let placeholder
export let label
export let disabled = false
export let options
const dispatch = createEventDispatcher()
let bindingDrawer
$: tempValue = Array.isArray(value) ? value : []
$: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
dispatch("change", readableToRuntimeBinding(bindings, value))
}
</script>
<div class="control">
<Combobox
{label}
{disabled}
value={readableValue}
on:change={event => onChange(event.detail)}
{placeholder}
{options}
/>
{#if !disabled}
<div class="icon" on:click={bindingDrawer.show}>
<Icon size="S" name="FlashOn" />
</div>
{/if}
</div>
<Drawer bind:this={bindingDrawer} {title}>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" on:click={handleClose}>Save</Button>
<svelte:component
this={panel}
slot="body"
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
bindableProperties={bindings}
/>
</Drawer>
<style>
.control {
flex: 1;
position: relative;
}
.icon {
right: 31px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-right: 1px solid var(--spectrum-alias-border-color);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
</style>

View File

@ -0,0 +1,38 @@
<script>
import { Select } from "@budibase/bbui"
import { store } from "builderStore"
const themeOptions = [
{
label: "Lightest",
value: "spectrum--lightest",
},
{
label: "Light",
value: "spectrum--light",
},
{
label: "Dark",
value: "spectrum--dark",
},
{
label: "Darkest",
value: "spectrum--darkest",
},
]
</script>
<div>
<Select
value={$store.theme || "spectrum--light"}
options={themeOptions}
placeholder={null}
on:change={e => store.actions.theme.save(e.detail)}
/>
</div>
<style>
div {
padding-right: 8px;
}
</style>

View File

@ -5,12 +5,16 @@
import { Screen } from "builderStore/store/screenTemplates/utils/Screen" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
import ErrorSVG from "assets/error.svg?raw"
let iframe let iframe
let layout let layout
let screen let screen
let confirmDeleteDialog let confirmDeleteDialog
let idToDelete let idToDelete
let loading = true
let error
// Create screen slot placeholder for use when a page is selected rather // Create screen slot placeholder for use when a page is selected rather
// than a screen // than a screen
@ -44,6 +48,7 @@
screen, screen,
selectedComponentId, selectedComponentId,
previewType: $store.currentFrontEndType, previewType: $store.currentFrontEndType,
theme: $store.theme,
} }
// Saving pages and screens to the DB causes them to have _revs. // Saving pages and screens to the DB causes them to have _revs.
@ -68,11 +73,28 @@
onMount(() => { onMount(() => {
// Initialise the app when mounted // Initialise the app when mounted
iframe.contentWindow.addEventListener( iframe.contentWindow.addEventListener(
"bb-ready", "ready",
() => refreshContent(strippedJson), () => refreshContent(strippedJson),
{ once: true } { once: true }
) )
// Display the client app once the iframe is initialised
iframe.contentWindow.addEventListener(
"iframe-loaded",
() => (loading = false),
{ once: true }
)
// Catch any app errors
iframe.contentWindow.addEventListener(
"error",
event => {
loading = false
error = event.detail || "An unknown error occurred"
},
{ once: true }
)
// Add listener for events sent by cliebt library in preview // Add listener for events sent by cliebt library in preview
iframe.contentWindow.addEventListener("bb-event", event => { iframe.contentWindow.addEventListener("bb-event", event => {
const { type, data } = event.detail const { type, data } = event.detail
@ -83,8 +105,14 @@
} else if (type === "delete-component" && data.id) { } else if (type === "delete-component" && data.id) {
idToDelete = data.id idToDelete = data.id
confirmDeleteDialog.show() confirmDeleteDialog.show()
} else if (type === "preview-loaded") {
// We can use this in future to delay displaying the preview
// until the client app has actually initialised.
// This makes a smoother loading experience, but is not backwards
// compatible with old client library versions.
// So do nothing with this for now.
} else { } else {
console.log(data) console.warning(`Client sent unknown event type: ${type}`)
} }
}) })
}) })
@ -99,11 +127,25 @@
</script> </script>
<div class="component-container"> <div class="component-container">
{#if loading}
<div class="center">
<ProgressCircle />
</div>
{:else if error}
<div class="center error">
<Layout justifyItems="center" gap="S">
{@html ErrorSVG}
<Heading size="L">App preview failed to load</Heading>
<Body size="S">{error}</Body>
</Layout>
</div>
{/if}
<iframe <iframe
style="height: 100%; width: 100%" style="height: 100%; width: 100%"
title="componentPreview" title="componentPreview"
bind:this={iframe} bind:this={iframe}
srcdoc={template} srcdoc={template}
class:hidden={loading || error}
/> />
</div> </div>
<ConfirmDialog <ConfirmDialog
@ -131,4 +173,32 @@
width: 100%; width: 100%;
background-color: transparent; background-color: transparent;
} }
.center {
position: absolute;
width: 100%;
height: 100%;
display: grid;
place-items: center;
z-index: 1;
}
.hidden {
opacity: 0;
}
.error :global(svg) {
fill: var(--spectrum-global-color-gray-500);
width: 80px;
height: 80px;
}
.error :global(h1),
.error :global(p) {
color: var(--spectrum-global-color-gray-800);
}
.error :global(p) {
font-style: italic;
margin-top: -0.5em;
}
.error :global(h1) {
font-weight: 400;
margin: 0;
}
</style> </style>

View File

@ -27,9 +27,7 @@
"name": "Card", "name": "Card",
"icon": "Card", "icon": "Card",
"children": [ "children": [
"stackedlist", "spectrumcard",
"card",
"cardhorizontal",
"cardstat" "cardstat"
] ]
}, },
@ -57,13 +55,6 @@
"icon", "icon",
"embed" "embed"
] ]
},
{
"name": "Other",
"icon": "More",
"children": [
"screenslot"
]
} }
] ]

View File

@ -25,7 +25,9 @@ export default `
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); }
html.loaded {
box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.1);
} }
body { body {
flex: 1 1 auto; flex: 1 1 auto;
@ -46,7 +48,14 @@ export default `
} }
// Extract data from message // Extract data from message
const { selectedComponentId, layout, screen, previewType, appId } = JSON.parse(event.data) const {
selectedComponentId,
layout,
screen,
previewType,
appId,
theme
} = JSON.parse(event.data)
// Set some flags so the app knows we're in the builder // Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true window["##BUDIBASE_IN_BUILDER##"] = true
@ -56,15 +65,24 @@ export default `
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
window["##BUDIBASE_PREVIEW_TYPE##"] = previewType window["##BUDIBASE_PREVIEW_TYPE##"] = previewType
window["##BUDIBASE_PREVIEW_THEME##"] = theme
// Initialise app // Initialise app
if (window.loadBudibase) { try {
loadBudibase() if (window.loadBudibase) {
window.loadBudibase()
document.documentElement.classList.add("loaded")
window.dispatchEvent(new Event("iframe-loaded"))
} else {
throw "The client library couldn't be loaded"
}
} catch (error) {
window.dispatchEvent(new CustomEvent("error", { detail: error }))
} }
} }
window.addEventListener("message", receiveMessage) window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event("bb-ready")) window.dispatchEvent(new Event("ready"))
</script> </script>
</head> </head>
<body/> <body/>

View File

@ -65,52 +65,56 @@
} }
</script> </script>
<ActionMenu> {#if definition.editable !== false}
<div slot="control" class="icon"> <ActionMenu>
<Icon size="S" hoverable name="MoreSmallList" /> <div slot="control" class="icon">
</div> <Icon size="S" hoverable name="MoreSmallList" />
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem> </div>
<MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}> <MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
Move up Delete
</MenuItem> </MenuItem>
<MenuItem noClose icon="ChevronDown" on:click={moveDownComponent}> <MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}>
Move down Move up
</MenuItem> </MenuItem>
<MenuItem noClose icon="Duplicate" on:click={duplicateComponent}> <MenuItem noClose icon="ChevronDown" on:click={moveDownComponent}>
Duplicate Move down
</MenuItem> </MenuItem>
<MenuItem icon="Cut" on:click={() => storeComponentForCopy(true)}> <MenuItem noClose icon="Duplicate" on:click={duplicateComponent}>
Cut Duplicate
</MenuItem> </MenuItem>
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}> <MenuItem icon="Cut" on:click={() => storeComponentForCopy(true)}>
Copy Cut
</MenuItem> </MenuItem>
<MenuItem <MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
icon="LayersBringToFront" Copy
on:click={() => pasteComponent("above")} </MenuItem>
disabled={noPaste} <MenuItem
> icon="LayersBringToFront"
Paste above on:click={() => pasteComponent("above")}
</MenuItem> disabled={noPaste}
<MenuItem >
icon="LayersSendToBack" Paste above
on:click={() => pasteComponent("below")} </MenuItem>
disabled={noPaste} <MenuItem
> icon="LayersSendToBack"
Paste below on:click={() => pasteComponent("below")}
</MenuItem> disabled={noPaste}
<MenuItem >
icon="ShowOneLayer" Paste below
on:click={() => pasteComponent("inside")} </MenuItem>
disabled={noPaste || noChildrenAllowed} <MenuItem
> icon="ShowOneLayer"
Paste inside on:click={() => pasteComponent("inside")}
</MenuItem> disabled={noPaste || noChildrenAllowed}
</ActionMenu> >
<ConfirmDialog Paste inside
bind:this={confirmDeleteDialog} </MenuItem>
title="Confirm Deletion" </ActionMenu>
body={`Are you sure you wish to delete this '${definition?.name}' component?`} <ConfirmDialog
okText="Delete Component" bind:this={confirmDeleteDialog}
onOk={deleteComponent} title="Confirm Deletion"
/> body={`Are you sure you wish to delete this '${definition?.name}' component?`}
okText="Delete Component"
onOk={deleteComponent}
/>
{/if}

View File

@ -3,6 +3,7 @@
import { DropEffect, DropPosition } from "./dragDropStore" import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte" import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers"
export let components = [] export let components = []
export let currentComponent export let currentComponent
@ -10,8 +11,6 @@
export let level = 0 export let level = 0
export let dragDropStore export let dragDropStore
const isScreenslot = name => name?.endsWith("screenslot")
const selectComponent = component => { const selectComponent = component => {
store.actions.components.select(component) store.actions.components.select(component)
} }
@ -42,6 +41,16 @@
return false return false
} }
const getComponentText = component => {
if (component._instanceName) {
return component._instanceName
}
const type =
component._component.replace("@budibase/standard-components/", "") ||
"component"
return capitalise(type)
}
</script> </script>
<ul> <ul>
@ -63,9 +72,7 @@
on:dragstart={dragstart(component)} on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:drop={dragDropStore.actions.drop} on:drop={dragDropStore.actions.drop}
text={isScreenslot(component._component) text={getComponentText(component)}
? "Screenslot"
: component._instanceName}
withArrow withArrow
indentLevel={level + 1} indentLevel={level + 1}
selected={$store.selectedComponentId === component._id} selected={$store.selectedComponentId === component._id}

View File

@ -1,12 +1,6 @@
<script> <script>
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
import { import { Checkbox, Input, Select, DetailSummary } from "@budibase/bbui"
Checkbox,
Input,
Select,
DetailSummary,
ColorPicker,
} from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import PropertyControl from "./PropertyControls/PropertyControl.svelte" import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
@ -31,6 +25,8 @@
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte" import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte" import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte" import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
import URLSelect from "./PropertyControls/URLSelect.svelte"
export let componentDefinition export let componentDefinition
export let componentInstance export let componentInstance
@ -66,6 +62,7 @@
section: SectionSelect, section: SectionSelect,
navigation: NavigationEditor, navigation: NavigationEditor,
filter: FilterEditor, filter: FilterEditor,
url: URLSelect,
"field/string": StringFieldSelect, "field/string": StringFieldSelect,
"field/number": NumberFieldSelect, "field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect, "field/options": OptionsFieldSelect,

View File

@ -35,17 +35,19 @@
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton> <ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
</div> </div>
</DetailSummary> </DetailSummary>
<Drawer bind:this={drawer} title="Custom CSS"> {#key componentInstance?._id}
<Button cta slot="buttons" on:click={save}>Save</Button> <Drawer bind:this={drawer} title="Custom CSS">
<DrawerContent slot="body"> <Button cta slot="buttons" on:click={save}>Save</Button>
<div class="content"> <DrawerContent slot="body">
<Layout gap="S"> <div class="content">
<Body size="S">Custom CSS overrides all other component styles.</Body> <Layout gap="S">
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." /> <Body size="S">Custom CSS overrides all other component styles.</Body>
</Layout> <TextArea bind:value={tempValue} placeholder="Enter some CSS..." />
</div> </Layout>
</DrawerContent> </div>
</Drawer> </DrawerContent>
</Drawer>
{/key}
<style> <style>
.content { .content {

View File

@ -1,42 +1,8 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { ColorPicker } from "@budibase/bbui"
import Colorpicker from "@budibase/colorpicker" import { store } from "builderStore"
const dispatch = createEventDispatcher()
export let value export let value
const WAIT = 150
function throttle(callback, wait, immediate = false) {
let timeout = null
let initialCall = true
return function () {
const callNow = immediate && initialCall
const next = () => {
callback.apply(this, arguments)
timeout = null
}
if (callNow) {
initialCall = false
next()
}
if (!timeout) {
timeout = setTimeout(next, wait)
}
}
}
const onChange = throttle(
e => {
dispatch("change", e.detail)
},
WAIT,
true
)
</script> </script>
<Colorpicker value={value || "#C4C4C4"} on:change={onChange} /> <ColorPicker {value} on:change spectrumTheme={$store.theme} />

View File

@ -51,7 +51,7 @@
} }
</script> </script>
<ActionButton on:click={drawer.show}>Define Actions</ActionButton> <ActionButton on:click={drawer.show}>Define actions</ActionButton>
<Drawer bind:this={drawer} title={"Actions"}> <Drawer bind:this={drawer} title={"Actions"}>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define what actions to run. Define what actions to run.

View File

@ -21,7 +21,7 @@
export let value = [] export let value = []
export let componentInstance export let componentInstance
let drawer let drawer
let tempValue = value let tempValue = value || []
$: numFilters = Array.isArray(tempValue) $: numFilters = Array.isArray(tempValue)
? tempValue.length ? tempValue.length
@ -31,15 +31,6 @@
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: internalTable = dataSource?.type === "table" $: internalTable = dataSource?.type === "table"
// Reset value if value is wrong type for the datasource.
// Lucene editor needs an array, and simple editor needs an object.
$: {
if (!Array.isArray(value)) {
tempValue = []
dispatch("change", [])
}
}
const saveFilter = async () => { const saveFilter = async () => {
dispatch("change", tempValue) dispatch("change", tempValue)
notifications.success("Filters saved.") notifications.success("Filters saved.")
@ -47,7 +38,7 @@
} }
</script> </script>
<ActionButton on:click={drawer.show}>Define Filters</ActionButton> <ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} title="Filtering"> <Drawer bind:this={drawer} title="Filtering">
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> <Button cta slot="buttons" on:click={saveFilter}>Save</Button>
<DrawerContent slot="body"> <DrawerContent slot="body">

View File

@ -107,7 +107,7 @@
loading = false loading = false
} }
$: displayValue = value ? value.substring(3) : "Pick Icon" $: displayValue = value ? value.substring(3) : "Pick icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage) $: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage $: pageEndIdx = maxIconsPerPage * currentPage

View File

@ -0,0 +1,12 @@
<script>
import { store } from "builderStore"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
export let value
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} on:change options={urlOptions} />

View File

@ -1,4 +1,5 @@
import { Input, Select, ColorPicker } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
export const margin = { export const margin = {
label: "Margin", label: "Margin",

View File

@ -7,7 +7,6 @@
let selected = "Sources" let selected = "Sources"
let modal let modal
$: isExternal = $: isExternal =
$params.selectedDatasource && $params.selectedDatasource &&
$params.selectedDatasource !== BUDIBASE_INTERNAL_DB $params.selectedDatasource !== BUDIBASE_INTERNAL_DB

View File

@ -1,8 +1,16 @@
<script> <script>
import { RelationshipTypes } from "constants/backend" import { RelationshipTypes } from "constants/backend"
import { Button, Input, ModalContent, Select, Detail } from "@budibase/bbui" import {
Button,
Input,
ModalContent,
Select,
Detail,
Body,
} from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
import { writable } from "svelte/store"
export let save export let save
export let datasource export let datasource
@ -14,16 +22,68 @@
let originalFromName = fromRelationship.name, let originalFromName = fromRelationship.name,
originalToName = toRelationship.name originalToName = toRelationship.name
function isValid(relationship) { function inSchema(table, prop, ogName) {
if ( if (!table || !prop || prop === ogName) {
relationship.relationshipType === RelationshipTypes.MANY_TO_MANY &&
!relationship.through
) {
return false return false
} }
return ( const keys = Object.keys(table.schema).map(key => key.toLowerCase())
relationship.name && relationship.tableId && relationship.relationshipType return keys.indexOf(prop.toLowerCase()) !== -1
) }
const touched = writable({})
function checkForErrors(
fromTable,
toTable,
throughTable,
fromRelate,
toRelate
) {
const isMany =
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
const tableNotSet = "Please specify a table"
const errors = {}
if ($touched.from && !fromTable) {
errors.from = tableNotSet
}
if ($touched.to && !toTable) {
errors.to = tableNotSet
}
if ($touched.through && isMany && !fromRelate.through) {
errors.through = tableNotSet
}
if ($touched.foreign && !isMany && !fromRelate.fieldName) {
errors.foreign = "Please pick the foreign key"
}
const colNotSet = "Please specify a column name"
if ($touched.fromCol && !fromRelate.name) {
errors.fromCol = colNotSet
}
if ($touched.toCol && !toRelate.name) {
errors.toCol = colNotSet
}
// currently don't support relationships back onto the table itself, needs to relate out
const tableError = "From/to/through tables must be different"
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
errors.from = tableError
}
if (toTable && (toTable === fromTable || toTable === throughTable)) {
errors.to = tableError
}
if (
throughTable &&
(throughTable === fromTable || throughTable === toTable)
) {
errors.through = tableError
}
const colError = "Column name cannot be an existing column"
if (inSchema(fromTable, fromRelate.name, originalFromName)) {
errors.fromCol = colError
}
if (inSchema(toTable, toRelate.name, originalToName)) {
errors.toCol = colError
}
return errors
} }
$: tableOptions = plusTables.map(table => ({ $: tableOptions = plusTables.map(table => ({
@ -33,7 +93,15 @@
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId) $: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId) $: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
$: through = plusTables.find(table => table._id === fromRelationship?.through) $: through = plusTables.find(table => table._id === fromRelationship?.through)
$: valid = toTable && fromTable && isValid(fromRelationship) $: errors = checkForErrors(
fromTable,
toTable,
through,
fromRelationship,
toRelationship
)
$: valid =
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
$: linkTable = through || toTable $: linkTable = through || toTable
$: relationshipTypes = [ $: relationshipTypes = [
{ {
@ -155,31 +223,55 @@
<Select <Select
label="Select from table" label="Select from table"
options={tableOptions} options={tableOptions}
on:change={() => ($touched.from = true)}
bind:error={errors.from}
bind:value={toRelationship.tableId} bind:value={toRelationship.tableId}
/> />
<Select <Select
label={"Select to table"} label={"Select to table"}
options={tableOptions} options={tableOptions}
on:change={() => ($touched.to = true)}
bind:error={errors.to}
bind:value={fromRelationship.tableId} bind:value={fromRelationship.tableId}
/> />
{#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY} {#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
<Select <Select
label={"Through"} label={"Through"}
options={tableOptions} options={tableOptions}
on:change={() => ($touched.through = true)}
bind:error={errors.through}
bind:value={fromRelationship.through} bind:value={fromRelationship.through}
/> />
{:else if toTable} {:else if fromRelationship?.relationshipType && toTable}
<Select <Select
label={`Foreign Key (${toTable?.name})`} label={`Foreign Key (${toTable?.name})`}
options={Object.keys(toTable?.schema)} options={Object.keys(toTable?.schema).filter(
field => toTable?.primary.indexOf(field) === -1
)}
on:change={() => ($touched.foreign = true)}
bind:error={errors.foreign}
bind:value={fromRelationship.fieldName} bind:value={fromRelationship.fieldName}
/> />
{/if} {/if}
<div class="headings"> <div class="headings">
<Detail>Column names</Detail> <Detail>Column names</Detail>
</div> </div>
<Input label="From table column" bind:value={fromRelationship.name} /> <Body>
<Input label="To table column" bind:value={toRelationship.name} /> Budibase manages SQL relationships as a new column in the table, please
provide a name for these columns.
</Body>
<Input
on:blur={() => ($touched.fromCol = true)}
bind:error={errors.fromCol}
label="From table column"
bind:value={fromRelationship.name}
/>
<Input
on:blur={() => ($touched.toCol = true)}
bind:error={errors.toCol}
label="To table column"
bind:value={toRelationship.name}
/>
<div slot="footer"> <div slot="footer">
{#if originalFromName != null} {#if originalFromName != null}
<Button warning text on:click={deleteRelationship}>Delete</Button> <Button warning text on:click={deleteRelationship}>Delete</Button>

View File

@ -13,6 +13,7 @@
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import { findComponent, findComponentPath } from "builderStore/storeUtils" import { findComponent, findComponentPath } from "builderStore/storeUtils"
import { get } from "svelte/store" import { get } from "svelte/store"
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
// Cache previous values so we don't update the URL more than necessary // Cache previous values so we don't update the URL more than necessary
let previousType let previousType
@ -147,7 +148,10 @@
<div class="preview-pane"> <div class="preview-pane">
{#if $currentAsset} {#if $currentAsset}
<ComponentSelectionList /> <div class="preview-header">
<ComponentSelectionList />
<AppThemeSelect />
</div>
<div class="preview-content"> <div class="preview-content">
<CurrentItemPreview /> <CurrentItemPreview />
</div> </div>
@ -193,6 +197,10 @@
gap: var(--spacing-m); gap: var(--spacing-m);
padding: var(--spacing-xl) 40px; padding: var(--spacing-xl) 40px;
} }
.preview-header {
display: grid;
grid-template-columns: 1fr 100px;
}
.preview-content { .preview-content {
flex: 1 1 auto; flex: 1 1 auto;
} }

View File

@ -28,12 +28,7 @@
onMount(async () => { onMount(async () => {
await organisation.init() await organisation.init()
await apps.load() await apps.load()
// Skip the portal if you only have one app loaded = true
if (!$auth.isBuilder && $apps.filter(publishedAppsOnly).length === 1) {
window.location = `/${publishedApps[0].prodId}`
} else {
loaded = true
}
}) })
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED

View File

@ -9,8 +9,6 @@
$redirect("../") $redirect("../")
} }
} }
$: console.log($page)
</script> </script>
{#if $auth.isAdmin} {#if $auth.isAdmin}

View File

@ -33,7 +33,7 @@
role: {}, role: {},
} }
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "" $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "BASIC"
// Merge the Apps list and the roles response to get something that makes sense for the table // Merge the Apps list and the roles response to get something that makes sense for the table
$: appList = Object.keys($apps?.data).map(id => { $: appList = Object.keys($apps?.data).map(id => {
const role = $userFetch?.data?.roles?.[id] || defaultRoleId const role = $userFetch?.data?.roles?.[id] || defaultRoleId

View File

@ -9,7 +9,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const roles = app.roles const roles = app.roles
let options = roles.map(role => ({ value: role._id, label: role.name })) let options = roles
.map(role => ({ value: role._id, label: role.name }))
.filter(role => role.value !== "PUBLIC")
let selectedRole = user?.roles?.[app?._id] let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() { async function updateUserRoles() {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.70", "version": "0.9.71",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.70", "version": "0.9.71",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -18,9 +18,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.70", "@budibase/bbui": "^0.9.71",
"@budibase/standard-components": "^0.9.70", "@budibase/standard-components": "^0.9.71",
"@budibase/string-templates": "^0.9.70", "@budibase/string-templates": "^0.9.71",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -43,10 +43,9 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
case 400: case 400:
return handleError(`${url}: Bad Request`) return handleError(`${url}: Bad Request`)
case 403: case 403:
// reload the page incase the token has expired notificationStore.danger(
if (!url.includes("self")) { "Your session has expired, or you don't have permission to access that data"
location.reload() )
}
return handleError(`${url}: Forbidden`) return handleError(`${url}: Forbidden`)
default: default:
if (response.status >= 200 && response.status < 400) { if (response.status >= 200 && response.status < 400) {

View File

@ -3,8 +3,8 @@ import API from "./api"
/** /**
* Fetches screen definition for an app. * Fetches screen definition for an app.
*/ */
export const fetchAppDefinition = async appId => { export const fetchAppPackage = async appId => {
return await API.get({ return await API.get({
url: `/api/applications/${appId}/definition`, url: `/api/applications/${appId}/appPackage`,
}) })
} }

View File

@ -24,7 +24,12 @@ export const logIn = async ({ email, password }) => {
export const fetchSelf = async () => { export const fetchSelf = async () => {
const user = await API.get({ url: "/api/self" }) const user = await API.get({ url: "/api/self" })
if (user?._id) { if (user?._id) {
return (await enrichRows([user], TableNames.USERS))[0] if (user.roleId === "PUBLIC") {
// Don't try to enrich a public user as it will 403
return user
} else {
return (await enrichRows([user], TableNames.USERS))[0]
}
} else { } else {
return null return null
} }

View File

@ -13,11 +13,14 @@
authStore, authStore,
routeStore, routeStore,
builderStore, builderStore,
appStore,
} from "../store" } from "../store"
import { TableNames, ActionTypes } from "../constants" import { TableNames, ActionTypes } from "../constants"
import SettingsBar from "./preview/SettingsBar.svelte" import SettingsBar from "./preview/SettingsBar.svelte"
import SelectionIndicator from "./preview/SelectionIndicator.svelte" import SelectionIndicator from "./preview/SelectionIndicator.svelte"
import HoverIndicator from "./preview/HoverIndicator.svelte" import HoverIndicator from "./preview/HoverIndicator.svelte"
import { Layout, Heading, Body } from "@budibase/bbui"
import ErrorSVG from "../../../builder/assets/error.svg"
// Provide contexts // Provide contexts
setContext("sdk", SDK) setContext("sdk", SDK)
@ -25,12 +28,16 @@
setContext("context", createContextStore()) setContext("context", createContextStore())
let dataLoaded = false let dataLoaded = false
let permissionError = false
// Load app config // Load app config
onMount(async () => { onMount(async () => {
await initialise() await initialise()
await authStore.actions.fetchUser() await authStore.actions.fetchUser()
dataLoaded = true dataLoaded = true
if ($builderStore.inBuilder) {
builderStore.actions.notifyLoaded()
}
}) })
// Register this as a refreshable datasource so that user changes cause // Register this as a refreshable datasource so that user changes cause
@ -43,50 +50,72 @@
}, },
] ]
// Redirect to home layout if no matching route // Handle no matching route - this is likely a permission error
$: { $: {
if (dataLoaded && $routeStore.routerLoaded && !$routeStore.activeRoute) { if (dataLoaded && $routeStore.routerLoaded && !$routeStore.activeRoute) {
if ($authStore) { if ($authStore) {
routeStore.actions.navigate("/") // There is a logged in user, so handle them
if ($screenStore.screens.length) {
// Screens exist so navigate back to the home screen
const firstRoute = $screenStore.screens[0].routing?.route ?? "/"
routeStore.actions.navigate(firstRoute)
} else {
// No screens likely means the user has no permissions to view this app
permissionError = true
}
} else { } else {
// The user is not logged in, redirect them to login
const returnUrl = `${window.location.pathname}${window.location.hash}` const returnUrl = `${window.location.pathname}${window.location.hash}`
const encodedUrl = encodeURIComponent(returnUrl) const encodedUrl = encodeURIComponent(returnUrl)
window.location = `/builder/auth/login?returnUrl=${encodedUrl}` window.location = `/builder/auth/login?returnUrl=${encodedUrl}`
} }
} }
} }
$: themeClass =
$builderStore.theme || $appStore.application?.theme || "spectrum--light"
</script> </script>
{#if dataLoaded && $screenStore.activeLayout} {#if dataLoaded}
<div <div
id="spectrum-root" id="spectrum-root"
lang="en" lang="en"
dir="ltr" dir="ltr"
class="spectrum spectrum--medium spectrum--light" class="spectrum spectrum--medium {themeClass}"
> >
<Provider key="user" data={$authStore} {actions}> {#if permissionError}
<div id="app-root"> <div class="error">
{#key $screenStore.activeLayout._id} <Layout justifyItems="center" gap="S">
<Component instance={$screenStore.activeLayout.props} /> {@html ErrorSVG}
{/key} <Heading size="L">You don't have permission to use this app</Heading>
<Body size="S">Ask your administrator to grant you access</Body>
</Layout>
</div> </div>
<NotificationDisplay /> {:else if $screenStore.activeLayout}
<ConfirmationDisplay /> <Provider key="user" data={$authStore} {actions}>
<!-- Key block needs to be outside the if statement or it breaks --> <div id="app-root">
{#key $builderStore.selectedComponentId} {#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
</div>
<NotificationDisplay />
<ConfirmationDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!--
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<SettingsBar /> <SelectionIndicator />
<HoverIndicator />
{/if} {/if}
{/key} </Provider>
<!-- {/if}
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
{/if}
</Provider>
</div> </div>
{/if} {/if}
@ -101,5 +130,54 @@
} }
#app-root { #app-root {
position: relative; position: relative;
border: 1px solid var(--spectrum-global-color-gray-300);
}
/* Custom scrollbars */
:global(::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
:global(::-webkit-scrollbar-track) {
background: var(--spectrum-alias-background-color-default);
}
:global(::-webkit-scrollbar-thumb) {
background-color: var(--spectrum-global-color-gray-400);
border-radius: 4px;
}
:global(::-webkit-scrollbar-corner) {
background: var(--spectrum-alias-background-color-default);
}
:global(*) {
scrollbar-width: thin;
scrollbar-color: var(--spectrum-global-color-gray-400)
var(--spectrum-alias-background-color-default);
}
.error {
position: absolute;
width: 100%;
height: 100%;
display: grid;
place-items: center;
z-index: 1;
text-align: center;
padding: 20px;
}
.error :global(svg) {
fill: var(--spectrum-global-color-gray-500);
width: 80px;
height: 80px;
}
.error :global(h1),
.error :global(p) {
color: var(--spectrum-global-color-gray-800);
}
.error :global(p) {
font-style: italic;
margin-top: -0.5em;
}
.error :global(h1) {
font-weight: 400;
} }
</style> </style>

View File

@ -29,4 +29,9 @@
}) })
</script> </script>
<IndicatorSet {componentId} color="rgb(120, 170, 244)" transition {zIndex} /> <IndicatorSet
{componentId}
color="var(--spectrum-global-color-static-blue-200)"
transition
{zIndex}
/>

View File

@ -5,7 +5,7 @@
<IndicatorSet <IndicatorSet
componentId={$builderStore.selectedComponentId} componentId={$builderStore.selectedComponentId}
color="rgb(66, 133, 244)" color="var(--spectrum-global-color-static-blue-600)"
zIndex="910" zIndex="910"
transition transition
/> />

View File

@ -138,11 +138,11 @@
padding: 6px 8px; padding: 6px 8px;
opacity: 0; opacity: 0;
flex-direction: row; flex-direction: row;
background: var(--background); background: var(--spectrum-alias-background-color-primary);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
gap: 2px; gap: 2px;
transition: opacity 0.13s ease-in-out; transition: opacity 0.13s ease-in-out;
} }
@ -155,4 +155,14 @@
margin: 0 4px; margin: 0 4px;
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
} }
/* Theme overrides */
:global(.spectrum--dark) .bar,
:global(.spectrum--darkest) .bar {
background: var(--spectrum-global-color-gray-200);
}
:global(.spectrum--dark) .divider,
:global(.spectrum--darkest) .divider {
background: var(--spectrum-global-color-gray-400);
}
</style> </style>

View File

@ -1,5 +1,5 @@
import ClientApp from "./components/ClientApp.svelte" import ClientApp from "./components/ClientApp.svelte"
import { builderStore } from "./store" import { builderStore, appStore } from "./store"
let app let app
@ -7,14 +7,18 @@ const loadBudibase = () => {
// Update builder store with any builder flags // Update builder store with any builder flags
builderStore.set({ builderStore.set({
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"], inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
appId: window["##BUDIBASE_APP_ID##"],
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"], layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
screen: window["##BUDIBASE_PREVIEW_SCREEN##"], screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"], selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
previewId: window["##BUDIBASE_PREVIEW_ID##"], previewId: window["##BUDIBASE_PREVIEW_ID##"],
previewType: window["##BUDIBASE_PREVIEW_TYPE##"], previewType: window["##BUDIBASE_PREVIEW_TYPE##"],
theme: window["##BUDIBASE_PREVIEW_THEME##"],
}) })
// Set app ID - this window flag is set by both the preview and the real
// server rendered app HTML
appStore.actions.setAppID(window["##BUDIBASE_APP_ID##"])
// Create app if one hasn't been created yet // Create app if one hasn't been created yet
if (!app) { if (!app) {
app = new ClientApp({ app = new ClientApp({

View File

@ -0,0 +1,27 @@
import * as API from "../api"
import { get, writable } from "svelte/store"
const createAppStore = () => {
const store = writable({})
// Fetches the app definition including screens, layouts and theme
const fetchAppDefinition = async () => {
const appDefinition = await API.fetchAppPackage(get(store).appId)
store.set(appDefinition)
}
// Sets the initial app ID
const setAppID = id => {
store.update(state => {
state.appId = id
return state
})
}
return {
subscribe: store.subscribe,
actions: { setAppID, fetchAppDefinition },
}
}
export const appStore = createAppStore()

View File

@ -1,26 +1,13 @@
import * as API from "../api" import * as API from "../api"
import { writable, get } from "svelte/store" import { writable } from "svelte/store"
import { builderStore } from "./builder"
import { TableNames } from "../constants"
const createAuthStore = () => { const createAuthStore = () => {
const store = writable(null) const store = writable(null)
// Fetches the user object if someone is logged in and has reloaded the page // Fetches the user object if someone is logged in and has reloaded the page
const fetchUser = async () => { const fetchUser = async () => {
// Fetch the first user if inside the builder const user = await API.fetchSelf()
if (get(builderStore).inBuilder) { store.set(user)
const users = await API.fetchTableData(TableNames.USERS)
if (!users.error && users[0] != null) {
store.set(users[0])
}
}
// Or fetch the current user from localstorage in a real app
else {
const user = await API.fetchSelf()
store.set(user)
}
} }
return { return {

View File

@ -1,7 +1,7 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import Manifest from "@budibase/standard-components/manifest.json" import Manifest from "@budibase/standard-components/manifest.json"
const dispatchEvent = (type, data) => { const dispatchEvent = (type, data = {}) => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("bb-event", { new CustomEvent("bb-event", {
detail: { type, data }, detail: { type, data },
@ -64,6 +64,9 @@ const createBuilderStore = () => {
deleteComponent: id => { deleteComponent: id => {
dispatchEvent("delete-component", { id }) dispatchEvent("delete-component", { id })
}, },
notifyLoaded: () => {
dispatchEvent("preview-loaded")
},
} }
return { return {
...writableStore, ...writableStore,

View File

@ -1,4 +1,5 @@
export { authStore } from "./auth" export { authStore } from "./auth"
export { appStore } from "./app"
export { notificationStore } from "./notification" export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"

View File

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

View File

@ -1,27 +1,26 @@
import { writable, derived, get } from "svelte/store" import { derived } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import * as API from "../api" import { appStore } from "./app"
const createScreenStore = () => { const createScreenStore = () => {
const config = writable({
screens: [],
layouts: [],
})
const store = derived( const store = derived(
[config, routeStore, builderStore], [appStore, routeStore, builderStore],
([$config, $routeStore, $builderStore]) => { ([$appStore, $routeStore, $builderStore]) => {
let activeLayout let activeLayout, activeScreen
let activeScreen let layouts, screens
if ($builderStore.inBuilder) { if ($builderStore.inBuilder) {
// Use builder defined definitions if inside the builder preview // Use builder defined definitions if inside the builder preview
activeLayout = $builderStore.layout activeLayout = $builderStore.layout
activeScreen = $builderStore.screen activeScreen = $builderStore.screen
layouts = [activeLayout]
screens = [activeScreen]
} else { } else {
activeLayout = { props: { _component: "screenslot" } } activeLayout = { props: { _component: "screenslot" } }
// Find the correct screen by matching the current route // Find the correct screen by matching the current route
const { screens, layouts } = $config screens = $appStore.screens
layouts = $appStore.layouts
if ($routeStore.activeRoute) { if ($routeStore.activeRoute) {
activeScreen = screens.find( activeScreen = screens.find(
screen => screen._id === $routeStore.activeRoute.screenId screen => screen._id === $routeStore.activeRoute.screenId
@ -33,21 +32,12 @@ const createScreenStore = () => {
) )
} }
} }
return { activeLayout, activeScreen } return { layouts, screens, activeLayout, activeScreen }
} }
) )
const fetchScreens = async () => {
const appDefinition = await API.fetchAppDefinition(get(builderStore).appId)
config.set({
screens: appDefinition.screens,
layouts: appDefinition.layouts,
})
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { fetchScreens },
} }
} }

View File

@ -29,7 +29,7 @@ export const styleable = (node, styles = {}) => {
// overridden by any user specified styles // overridden by any user specified styles
let baseStyles = {} let baseStyles = {}
if (newStyles.empty) { if (newStyles.empty) {
baseStyles.border = "2px dashed var(--grey-5)" baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
baseStyles.padding = "var(--spacing-l)" baseStyles.padding = "var(--spacing-l)"
baseStyles.overflow = "hidden" baseStyles.overflow = "hidden"
} }

View File

@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
"exec": "ts-node src/index.ts"
}

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.70", "version": "0.9.71",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -18,7 +18,7 @@
"dev:stack:up": "node scripts/dev/manage.js up", "dev:stack:up": "node scripts/dev/manage.js up",
"dev:stack:down": "node scripts/dev/manage.js down", "dev:stack:down": "node scripts/dev/manage.js down",
"dev:stack:nuke": "node scripts/dev/manage.js nuke", "dev:stack:nuke": "node scripts/dev/manage.js nuke",
"dev:builder": "yarn run dev:stack:up && ts-node src/index.ts", "dev:builder": "yarn run dev:stack:up && nodemon",
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write", "format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/", "lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint", "lint:fix": "yarn run format && yarn run lint",
@ -59,9 +59,9 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.70", "@budibase/auth": "^0.9.71",
"@budibase/client": "^0.9.70", "@budibase/client": "^0.9.71",
"@budibase/string-templates": "^0.9.70", "@budibase/string-templates": "^0.9.71",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1", "@sendgrid/mail": "7.1.1",
@ -114,7 +114,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4", "@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.70", "@budibase/standard-components": "^0.9.71",
"@jest/test-sequencer": "^24.8.0", "@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1", "@types/bull": "^3.15.1",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",

View File

@ -1,9 +1,22 @@
CREATE DATABASE IF NOT EXISTS main; CREATE DATABASE IF NOT EXISTS main;
USE main; USE main;
CREATE TABLE Persons ( CREATE TABLE Persons (
PersonID int NOT NULL PRIMARY KEY, PersonID int NOT NULL AUTO_INCREMENT,
LastName varchar(255), LastName varchar(255),
FirstName varchar(255), FirstName varchar(255),
Address varchar(255), Address varchar(255),
City varchar(255) City varchar(255),
PRIMARY KEY (PersonID)
); );
CREATE TABLE Tasks (
TaskID int NOT NULL AUTO_INCREMENT,
PersonID INT,
TaskName varchar(255),
PRIMARY KEY (TaskID),
CONSTRAINT fkPersons
FOREIGN KEY(PersonID)
REFERENCES Persons(PersonID)
);
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker-compose down
docker volume prune -f

View File

@ -1,14 +1,14 @@
SELECT 'CREATE DATABASE main' SELECT 'CREATE DATABASE main'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
CREATE TABLE Persons ( CREATE TABLE Persons (
PersonID INT NOT NULL PRIMARY KEY, PersonID SERIAL PRIMARY KEY,
LastName varchar(255), LastName varchar(255),
FirstName varchar(255), FirstName varchar(255),
Address varchar(255), Address varchar(255),
City varchar(255) City varchar(255) DEFAULT 'Belfast'
); );
CREATE TABLE Tasks ( CREATE TABLE Tasks (
TaskID INT NOT NULL PRIMARY KEY, TaskID SERIAL PRIMARY KEY,
PersonID INT, PersonID INT,
TaskName varchar(255), TaskName varchar(255),
CONSTRAINT fkPersons CONSTRAINT fkPersons
@ -16,7 +16,7 @@ CREATE TABLE Tasks (
REFERENCES Persons(PersonID) REFERENCES Persons(PersonID)
); );
CREATE TABLE Products ( CREATE TABLE Products (
ProductID INT NOT NULL PRIMARY KEY, ProductID SERIAL PRIMARY KEY,
ProductName varchar(255) ProductName varchar(255)
); );
CREATE TABLE Products_Tasks ( CREATE TABLE Products_Tasks (
@ -30,12 +30,12 @@ CREATE TABLE Products_Tasks (
REFERENCES Tasks(TaskID), REFERENCES Tasks(TaskID),
PRIMARY KEY (ProductID, TaskID) PRIMARY KEY (ProductID, TaskID)
); );
INSERT INTO Persons (PersonID, FirstName, LastName, Address, City) VALUES (1, 'Mike', 'Hughes', '123 Fake Street', 'Belfast'); INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (1, 1, 'assembling'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (2, 1, 'processing'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
INSERT INTO Products (ProductID, ProductName) VALUES (1, 'Computers'); INSERT INTO Products (ProductName) VALUES ('Computers');
INSERT INTO Products (ProductID, ProductName) VALUES (2, 'Laptops'); INSERT INTO Products (ProductName) VALUES ('Laptops');
INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs'); INSERT INTO Products (ProductName) VALUES ('Chairs');
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);

View File

@ -67,15 +67,18 @@ async function getAppUrlIfNotInUse(ctx) {
let url let url
if (ctx.request.body.url) { if (ctx.request.body.url) {
url = encodeURI(ctx.request.body.url) url = encodeURI(ctx.request.body.url)
} else { } else if (ctx.request.body.name) {
url = encodeURI(`${ctx.request.body.name}`) url = encodeURI(`${ctx.request.body.name}`)
} }
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase() if (url) {
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
}
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
return url return url
} }
const deployedApps = await getDeployedApps(ctx) const deployedApps = await getDeployedApps(ctx)
if ( if (
url &&
deployedApps[url] != null && deployedApps[url] != null &&
deployedApps[url].appId !== ctx.params.appId deployedApps[url].appId !== ctx.params.appId
) { ) {
@ -161,7 +164,15 @@ exports.fetchAppDefinition = async function (ctx) {
exports.fetchAppPackage = async function (ctx) { exports.fetchAppPackage = async function (ctx) {
const db = new CouchDB(ctx.params.appId) const db = new CouchDB(ctx.params.appId)
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)]) const layouts = await getLayouts(db)
let screens = await getScreens(db)
// Only filter screens if the user is not a builder
if (!(ctx.user.builder && ctx.user.builder.global)) {
const userRoleId = getUserRoleId(ctx)
const accessController = new AccessController(ctx.params.appId)
screens = await accessController.checkScreensAccess(screens, userRoleId)
}
ctx.body = { ctx.body = {
application, application,

View File

@ -162,7 +162,7 @@ module External {
manyRelationships: ManyRelationship[] = [] manyRelationships: ManyRelationship[] = []
for (let [key, field] of Object.entries(table.schema)) { for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it // if set already, or not set just skip it
if (!row[key] || newRow[key]) { if (!row[key] || newRow[key] || field.autocolumn) {
continue continue
} }
// if its not a link then just copy it over // if its not a link then just copy it over

View File

@ -34,13 +34,55 @@
*:after { *:after {
box-sizing: border-box; box-sizing: border-box;
} }
#error {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
display: none;
font-family: "Source Sans Pro", sans-serif;
flex-direction: column;
justify-content: center;
align-items: center;
background: #222;
text-align: center;
padding: 2rem;
gap: 2rem;
}
#error h1,
#error h2 {
margin: 0;
}
#error h1 {
color: #ccc;
font-size: 3rem;
font-weight: 600;
}
#error h2 {
color: #888;
font-weight: 400;
}
</style> </style>
</svelte:head> </svelte:head>
<body id="app"> <body id="app">
<div id="error">
<h1>There was an error loading your app</h1>
<h2>
The Budibase client library could not be loaded. Try republishing your
app.
</h2>
</div>
<script type="application/javascript" src={clientLibPath}> <script type="application/javascript" src={clientLibPath}>
</script> </script>
<script type="application/javascript"> <script type="application/javascript">
loadBudibase() if (window.loadBudibase) {
window.loadBudibase()
} else {
console.error("Failed to load the Budibase client")
document.getElementById("error").style.display = "flex"
}
</script> </script>
</body> </body>

View File

@ -9,9 +9,13 @@ jest.mock("../../../utilities/redis", () => ({
updateLock: jest.fn(), updateLock: jest.fn(),
setDebounce: jest.fn(), setDebounce: jest.fn(),
checkDebounce: jest.fn(), checkDebounce: jest.fn(),
shutdown: jest.fn(),
})) }))
const { clearAllApps, checkBuilderEndpoint } = require("./utilities/TestFunctions") const {
clearAllApps,
checkBuilderEndpoint,
} = require("./utilities/TestFunctions")
const setup = require("./utilities") const setup = require("./utilities")
const { AppStatus } = require("../../../db/utils") const { AppStatus } = require("../../../db/utils")
@ -32,7 +36,7 @@ describe("/applications", () => {
.post("/api/applications") .post("/api/applications")
.send({ name: "My App" }) .send({ name: "My App" })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body._id).toBeDefined() expect(res.body._id).toBeDefined()
}) })
@ -42,7 +46,7 @@ describe("/applications", () => {
config, config,
method: "POST", method: "POST",
url: `/api/applications`, url: `/api/applications`,
body: { name: "My App" } body: { name: "My App" },
}) })
}) })
}) })
@ -55,7 +59,7 @@ describe("/applications", () => {
const res = await request const res = await request
.get(`/api/applications?status=${AppStatus.DEV}`) .get(`/api/applications?status=${AppStatus.DEV}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
// two created apps + the inited app // two created apps + the inited app
@ -68,7 +72,7 @@ describe("/applications", () => {
const res = await request const res = await request
.get(`/api/applications/${config.getAppId()}/definition`) .get(`/api/applications/${config.getAppId()}/definition`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
// should have empty packages // should have empty packages
expect(res.body.screens.length).toEqual(1) expect(res.body.screens.length).toEqual(1)
@ -81,7 +85,7 @@ describe("/applications", () => {
const res = await request const res = await request
.get(`/api/applications/${config.getAppId()}/appPackage`) .get(`/api/applications/${config.getAppId()}/appPackage`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.application).toBeDefined() expect(res.body.application).toBeDefined()
expect(res.body.screens.length).toEqual(1) expect(res.body.screens.length).toEqual(1)
@ -94,10 +98,10 @@ describe("/applications", () => {
const res = await request const res = await request
.put(`/api/applications/${config.getAppId()}`) .put(`/api/applications/${config.getAppId()}`)
.send({ .send({
name: "TEST_APP" name: "TEST_APP",
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.rev).toBeDefined() expect(res.body.rev).toBeDefined()
}) })
@ -113,14 +117,14 @@ describe("/applications", () => {
name: "UPDATED_NAME", name: "UPDATED_NAME",
}) })
.set(headers) .set(headers)
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.rev).toBeDefined() expect(res.body.rev).toBeDefined()
// retrieve the app to check it // retrieve the app to check it
const getRes = await request const getRes = await request
.get(`/api/applications/${config.getAppId()}/appPackage`) .get(`/api/applications/${config.getAppId()}/appPackage`)
.set(headers) .set(headers)
.expect('Content-Type', /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(getRes.body.application.updatedAt).toBeDefined() expect(getRes.body.application.updatedAt).toBeDefined()
}) })

View File

@ -2,7 +2,6 @@ const setup = require("./utilities")
const { basicScreen } = setup.structures const { basicScreen } = setup.structures
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const workerRequests = require("../../../utilities/workerRequests")
const route = "/test" const route = "/test"

View File

@ -15,6 +15,7 @@ const EMPTY_LAYOUT = {
{ {
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", _id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
_component: "@budibase/standard-components/screenslot", _component: "@budibase/standard-components/screenslot",
_instanceName: "Screen slot",
_styles: { _styles: {
normal: { normal: {
flex: "1 1 auto", flex: "1 1 auto",
@ -63,6 +64,7 @@ const BASE_LAYOUTS = [
{ {
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", _id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
_component: "@budibase/standard-components/screenslot", _component: "@budibase/standard-components/screenslot",
_instanceName: "Screen slot",
_styles: { _styles: {
normal: { normal: {
flex: "1 1 auto", flex: "1 1 auto",
@ -84,6 +86,7 @@ const BASE_LAYOUTS = [
normal: {}, normal: {},
selected: {}, selected: {},
}, },
title: "{{ name }}",
navigation: "Top", navigation: "Top",
width: "Large", width: "Large",
links: [ links: [
@ -109,6 +112,7 @@ const BASE_LAYOUTS = [
{ {
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", _id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
_component: "@budibase/standard-components/screenslot", _component: "@budibase/standard-components/screenslot",
_instanceName: "Screen slot",
_styles: { _styles: {
normal: { normal: {
flex: "1 1 auto", flex: "1 1 auto",

View File

@ -14,6 +14,7 @@ export interface FieldSchema {
relationshipType?: string relationshipType?: string
through?: string through?: string
foreignKey?: string foreignKey?: string
autocolumn?: boolean
constraints?: { constraints?: {
type?: string type?: string
email?: boolean email?: boolean

View File

@ -163,14 +163,19 @@ module MySQLModule {
) )
for (let column of descResp) { for (let column of descResp) {
const columnName = column.Field const columnName = column.Field
if (column.Key === "PRI") { if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) {
primaryKeys.push(columnName) primaryKeys.push(columnName)
} }
const constraints = { const constraints = {
presence: column.Null !== "YES", presence: column.Null !== "YES",
} }
const isAuto: boolean =
typeof column.Extra === "string" &&
(column.Extra === "auto_increment" ||
column.Extra.toLowerCase().includes("generated"))
schema[columnName] = { schema[columnName] = {
name: columnName, name: columnName,
autocolumn: isAuto,
type: convertType(column.Type, TYPE_MAP), type: convertType(column.Type, TYPE_MAP),
constraints, constraints,
} }

View File

@ -147,7 +147,11 @@ module PostgresModule {
if (!tableKeys[tableName]) { if (!tableKeys[tableName]) {
tableKeys[tableName] = [] tableKeys[tableName] = []
} }
tableKeys[tableName].push(table.column_name || table.primary_key) const key = table.column_name || table.primary_key
// only add the unique keys
if (key && tableKeys[tableName].indexOf(key) === -1) {
tableKeys[tableName].push(key)
}
} }
} catch (err) { } catch (err) {
tableKeys = {} tableKeys = {}
@ -184,7 +188,11 @@ module PostgresModule {
} }
const type: string = convertType(column.data_type, TYPE_MAP) const type: string = convertType(column.data_type, TYPE_MAP)
const isAuto: boolean =
typeof column.column_default === "string" &&
column.column_default.startsWith("nextval")
tables[tableName].schema[columnName] = { tables[tableName].schema[columnName] = {
autocolumn: isAuto,
name: columnName, name: columnName,
type, type,
} }

View File

@ -17,7 +17,7 @@ describe("Postgres Integration", () => {
it("calls the create method with the correct params", async () => { it("calls the create method with the correct params", async () => {
const sql = "insert into users (name, age) values ('Joe', 123);" const sql = "insert into users (name, age) values ('Joe', 123);"
const response = await config.integration.create({ await config.integration.create({
sql sql
}) })
expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) expect(pg.queryMock).toHaveBeenCalledWith(sql, {})
@ -25,7 +25,7 @@ describe("Postgres Integration", () => {
it("calls the read method with the correct params", async () => { it("calls the read method with the correct params", async () => {
const sql = "select * from users;" const sql = "select * from users;"
const response = await config.integration.read({ await config.integration.read({
sql sql
}) })
expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) expect(pg.queryMock).toHaveBeenCalledWith(sql, {})

View File

@ -2,10 +2,10 @@ const { getAppId, setCookie, getCookie, clearCookie } =
require("@budibase/auth").utils require("@budibase/auth").utils
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { getRole } = require("@budibase/auth/roles") const { getRole } = require("@budibase/auth/roles")
const { getGlobalSelf } = require("../utilities/workerRequests")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { generateUserMetadataID } = require("../db/utils") const { generateUserMetadataID } = require("../db/utils")
const { dbExists } = require("@budibase/auth/db") const { dbExists } = require("@budibase/auth/db")
const { getCachedSelf } = require("../utilities/global")
const CouchDB = require("../db") const CouchDB = require("../db")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
@ -26,29 +26,17 @@ module.exports = async (ctx, next) => {
} }
} }
let updateCookie = false, let appId,
appId,
roleId = BUILTIN_ROLE_IDS.PUBLIC roleId = BUILTIN_ROLE_IDS.PUBLIC
if (!ctx.user) { if (!ctx.user) {
// not logged in, try to set a cookie for public apps // not logged in, try to set a cookie for public apps
updateCookie = true
appId = requestAppId appId = requestAppId
} else if ( } else if (requestAppId != null) {
requestAppId != null &&
(appCookie == null ||
requestAppId !== appCookie.appId ||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC ||
!appCookie.roleId)
) {
// Different App ID means cookie needs reset, or if the same public user has logged in // Different App ID means cookie needs reset, or if the same public user has logged in
const globalUser = await getGlobalSelf(ctx, requestAppId) const globalUser = await getCachedSelf(ctx, requestAppId)
updateCookie = true
appId = requestAppId appId = requestAppId
// retrieving global user gets the right role // retrieving global user gets the right role
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.PUBLIC roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC
} else if (appCookie != null) {
appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
} }
// nothing more to do // nothing more to do
if (!appId) { if (!appId) {
@ -68,8 +56,12 @@ module.exports = async (ctx, next) => {
role: await getRole(appId, roleId), role: await getRole(appId, roleId),
} }
} }
if (updateCookie) { if (
setCookie(ctx, { appId, roleId }, Cookies.CurrentApp) requestAppId !== appId ||
appCookie == null ||
appCookie.appId !== requestAppId
) {
setCookie(ctx, { appId }, Cookies.CurrentApp)
} }
return next() return next()
} }

View File

@ -23,6 +23,15 @@ function mockReset() {
function mockAuthWithNoCookie() { function mockAuthWithNoCookie() {
jest.resetModules() jest.resetModules()
mockWorker() mockWorker()
jest.mock("@budibase/auth/cache", () => ({
user: {
getUser: () => {
return {
_id: "us_uuid1",
}
},
},
}))
jest.mock("@budibase/auth", () => ({ jest.mock("@budibase/auth", () => ({
utils: { utils: {
getAppId: jest.fn(), getAppId: jest.fn(),

View File

@ -17,6 +17,8 @@ const { cleanup } = require("../../utilities/fileSystem")
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth const { jwt } = require("@budibase/auth").auth
const { StaticDatabases } = require("@budibase/auth/db") const { StaticDatabases } = require("@budibase/auth/db")
const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
@ -62,7 +64,7 @@ class TestConfiguration {
return request.body return request.body
} }
async globalUser(id = GLOBAL_USER_ID, builder = true) { async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
const db = new CouchDB(StaticDatabases.GLOBAL.name) const db = new CouchDB(StaticDatabases.GLOBAL.name)
let existing let existing
try { try {
@ -73,8 +75,9 @@ class TestConfiguration {
const user = { const user = {
_id: id, _id: id,
...existing, ...existing,
roles: {}, roles: roles || {},
} }
await createASession(id, "sessionid")
if (builder) { if (builder) {
user.builder = { global: true } user.builder = { global: true }
} }
@ -103,6 +106,7 @@ class TestConfiguration {
defaultHeaders() { defaultHeaders() {
const auth = { const auth = {
userId: GLOBAL_USER_ID, userId: GLOBAL_USER_ID,
sessionId: "sessionid",
} }
const app = { const app = {
roleId: BUILTIN_ROLE_IDS.ADMIN, roleId: BUILTIN_ROLE_IDS.ADMIN,
@ -138,13 +142,7 @@ class TestConfiguration {
roleId = BUILTIN_ROLE_IDS.ADMIN, roleId = BUILTIN_ROLE_IDS.ADMIN,
builder = false, builder = false,
}) { }) {
let user return this.login(email, PASSWORD, { roleId, builder })
try {
user = await this.createUser(email, PASSWORD, roleId)
} catch (err) {
// allow errors here
}
return this.login(email, PASSWORD, { roleId, userId: user._id, builder })
} }
async createApp(appName) { async createApp(appName) {
@ -313,6 +311,7 @@ class TestConfiguration {
async createUser(id = null) { async createUser(id = null) {
const globalId = !id ? `us_${Math.random()}` : `us_${id}` const globalId = !id ? `us_${Math.random()}` : `us_${id}`
const resp = await this.globalUser(globalId) const resp = await this.globalUser(globalId)
await userCache.invalidateUser(globalId)
return { return {
...resp, ...resp,
globalId, globalId,
@ -326,14 +325,19 @@ class TestConfiguration {
} }
// make sure the user exists in the global DB // make sure the user exists in the global DB
if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) { if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser(userId, builder) const appId = `app${this.getAppId().split("app_dev")[1]}`
await this.globalUser(userId, builder, {
[appId]: roleId,
})
} }
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
await createASession(userId, "sessionid")
// have to fake this // have to fake this
const auth = { const auth = {
userId, userId,
sessionId: "sessionid",
} }
const app = { const app = {
roleId: roleId, roleId: roleId,
@ -343,6 +347,7 @@ class TestConfiguration {
const appToken = jwt.sign(app, env.JWT_SECRET) const appToken = jwt.sign(app, env.JWT_SECRET)
// returning necessary request headers // returning necessary request headers
await userCache.invalidateUser(userId)
return { return {
Accept: "application/json", Accept: "application/json",
Cookie: [ Cookie: [

View File

@ -238,7 +238,10 @@ exports.readFileSync = (filepath, options = "utf8") => {
*/ */
exports.cleanup = appIds => { exports.cleanup = appIds => {
for (let appId of appIds) { for (let appId of appIds) {
fs.rmdirSync(join(budibaseTempDir(), appId), { recursive: true }) const path = join(budibaseTempDir(), appId)
if (fs.existsSync(path)) {
fs.rmdirSync(path, { recursive: true })
}
} }
} }

View File

@ -7,6 +7,7 @@ const {
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { getDeployedAppID } = require("@budibase/auth/db") const { getDeployedAppID } = require("@budibase/auth/db")
const { getGlobalUserParams } = require("@budibase/auth/db") const { getGlobalUserParams } = require("@budibase/auth/db")
const { user: userCache } = require("@budibase/auth/cache")
exports.updateAppRole = (appId, user) => { exports.updateAppRole = (appId, user) => {
if (!user.roles) { if (!user.roles) {
@ -19,21 +20,30 @@ exports.updateAppRole = (appId, user) => {
if (!user.roleId && user.builder && user.builder.global) { if (!user.roleId && user.builder && user.builder.global) {
user.roleId = BUILTIN_ROLE_IDS.ADMIN user.roleId = BUILTIN_ROLE_IDS.ADMIN
} else if (!user.roleId) { } else if (!user.roleId) {
user.roleId = BUILTIN_ROLE_IDS.PUBLIC user.roleId = BUILTIN_ROLE_IDS.BASIC
} }
delete user.roles delete user.roles
return user return user
} }
exports.getGlobalUser = async (appId, userId) => { function processUser(appId, user) {
const db = CouchDB(StaticDatabases.GLOBAL.name)
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
if (user) { if (user) {
delete user.password delete user.password
} }
return exports.updateAppRole(appId, user) return exports.updateAppRole(appId, user)
} }
exports.getCachedSelf = async (ctx, appId) => {
const user = await userCache.getUser(ctx.user._id)
return processUser(appId, user)
}
exports.getGlobalUser = async (appId, userId) => {
const db = CouchDB(StaticDatabases.GLOBAL.name)
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
return processUser(appId, user)
}
exports.getGlobalUsers = async (appId = null, users = null) => { exports.getGlobalUsers = async (appId = null, users = null) => {
const db = CouchDB(StaticDatabases.GLOBAL.name) const db = CouchDB(StaticDatabases.GLOBAL.name)
let globalUsers let globalUsers

File diff suppressed because it is too large Load Diff

View File

@ -73,13 +73,13 @@
{ {
"label": "Column", "label": "Column",
"value": "column", "value": "column",
"barIcon": "ViewRow", "barIcon": "ViewColumn",
"barTitle": "Column layout" "barTitle": "Column layout"
}, },
{ {
"label": "Row", "label": "Row",
"value": "row", "value": "row",
"barIcon": "ViewColumn", "barIcon": "ViewRow",
"barTitle": "Row layout" "barTitle": "Row layout"
} }
], ],
@ -230,7 +230,8 @@
"screenslot": { "screenslot": {
"name": "Screenslot", "name": "Screenslot",
"icon": "WebPage", "icon": "WebPage",
"description": "Contains your app screens" "description": "Contains your app screens",
"editable": false
}, },
"button": { "button": {
"name": "Button", "name": "Button",
@ -290,6 +291,11 @@
], ],
"defaultValue": "M" "defaultValue": "M"
}, },
{
"type": "boolean",
"label": "Quiet",
"key": "quiet"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -448,6 +454,7 @@
} }
}, },
"stackedlist": { "stackedlist": {
"deprecated": true,
"name": "Stacked List", "name": "Stacked List",
"icon": "TaskList", "icon": "TaskList",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
@ -811,30 +818,29 @@
"type": "select", "type": "select",
"label": "Size", "label": "Size",
"key": "size", "key": "size",
"defaultValue": "md", "defaultValue": "ri-1x",
"options": [ "options": [
{ "value": "ri-xxs", "label": "xxs" }, { "value": "ri-xxs", "label": "XXS" },
{ "value": "ri-xs", "label": "xs" }, { "value": "ri-xs", "label": "XS" },
{ "value": "ri-sm", "label": "sm" }, { "value": "ri-sm", "label": "Small" },
{ "value": "ri-1x", "label": "md" }, { "value": "ri-1x", "label": "Medium" },
{ "value": "ri-lg", "label": "lg" }, { "value": "ri-lg", "label": "Large" },
{ "value": "ri-xl", "label": "xl" }, { "value": "ri-xl", "label": "XL" },
{ "value": "ri-2x", "label": "2x" }, { "value": "ri-2x", "label": "2XL" },
{ "value": "ri-3x", "label": "3x" }, { "value": "ri-3x", "label": "3XL" },
{ "value": "ri-4x", "label": "4x" }, { "value": "ri-4x", "label": "4XL" },
{ "value": "ri-5x", "label": "5x" }, { "value": "ri-5x", "label": "5XL" },
{ "value": "ri-6x", "label": "6x" }, { "value": "ri-6x", "label": "6XL" },
{ "value": "ri-7x", "label": "7x" }, { "value": "ri-7x", "label": "7XL" },
{ "value": "ri-8x", "label": "8x" }, { "value": "ri-8x", "label": "8XL" },
{ "value": "ri-9x", "label": "9x" }, { "value": "ri-9x", "label": "9XL" },
{ "value": "ri-10x", "label": "10x" } { "value": "ri-10x", "label": "10XL" }
] ]
}, },
{ {
"type": "color", "type": "color",
"label": "Color", "label": "Color",
"key": "color", "key": "color"
"defaultValue": "#000"
}, },
{ {
"type": "event", "type": "event",
@ -844,6 +850,7 @@
] ]
}, },
"navigation": { "navigation": {
"deprecated": true,
"name": "Nav Bar", "name": "Nav Bar",
"description": "A component for handling the navigation within your app.", "description": "A component for handling the navigation within your app.",
"icon": "BreadcrumbNavigation", "icon": "BreadcrumbNavigation",
@ -876,7 +883,7 @@
"key": "text" "key": "text"
}, },
{ {
"type": "text", "type": "url",
"label": "URL", "label": "URL",
"key": "url", "key": "url",
"placeholder": "/screen" "placeholder": "/screen"
@ -1638,6 +1645,7 @@
"actions": [ "actions": [
"ValidateForm" "ValidateForm"
], ],
"styles": ["size"],
"settings": [ "settings": [
{ {
"type": "select", "type": "select",
@ -1651,46 +1659,6 @@
"label": "Schema", "label": "Schema",
"key": "dataSource" "key": "dataSource"
}, },
{
"type": "select",
"label": "Theme",
"key": "theme",
"defaultValue": "spectrum--light",
"options": [
{
"label": "Lightest",
"value": "spectrum--lightest"
},
{
"label": "Light",
"value": "spectrum--light"
},
{
"label": "Dark",
"value": "spectrum--dark"
},
{
"label": "Darkest",
"value": "spectrum--darkest"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "spectrum--medium",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2067,46 +2035,6 @@
"key": "rowCount", "key": "rowCount",
"defaultValue": 8 "defaultValue": 8
}, },
{
"type": "select",
"label": "Theme",
"key": "theme",
"defaultValue": "spectrum--light",
"options": [
{
"label": "Lightest",
"value": "spectrum--lightest"
},
{
"label": "Light",
"value": "spectrum--light"
},
{
"label": "Dark",
"value": "spectrum--dark"
},
{
"label": "Darkest",
"value": "spectrum--darkest"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "spectrum--medium",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
]
},
{ {
"type": "multifield", "type": "multifield",
"label": "Columns", "label": "Columns",
@ -2161,5 +2089,42 @@
"defaultValue": "Last 30 days" "defaultValue": "Last 30 days"
} }
] ]
},
"spectrumcard": {
"name": "Card",
"icon": "Card",
"styles": ["size"],
"settings": [
{
"type": "text",
"key": "title",
"label": "Title"
},
{
"type": "text",
"key": "subtitle",
"label": "Subtitle"
},
{
"type": "text",
"key": "description",
"label": "Description"
},
{
"type": "text",
"key": "imageURL",
"label": "Image URL"
},
{
"type": "url",
"key": "linkURL",
"label": "Link URL"
},
{
"type": "boolean",
"key": "horizontal",
"label": "Horizontal"
}
]
} }
} }

View File

@ -29,13 +29,15 @@
"keywords": [ "keywords": [
"svelte" "svelte"
], ],
"version": "0.9.70", "version": "0.9.71",
"license": "MIT", "license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.70", "@budibase/bbui": "^0.9.71",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/link": "^3.1.3", "@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",
"dayjs": "^1.10.5", "dayjs": "^1.10.5",

View File

@ -9,10 +9,12 @@
export let onClick export let onClick
export let size = "M" export let size = "M"
export let type = "primary" export let type = "primary"
export let quiet = false
</script> </script>
<button <button
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`} class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
class:spectrum-Button--quiet={quiet}
disabled={disabled || false} disabled={disabled || false}
use:styleable={$component.styles} use:styleable={$component.styles}
on:click={onClick} on:click={onClick}

View File

@ -20,14 +20,13 @@
.container { .container {
min-width: 260px; min-width: 260px;
width: max-content; width: max-content;
border: 1px solid var(--grey-3); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 0.3rem; border-radius: 0.3rem;
color: var(--blue);
} }
.title { .title {
font-size: 0.85rem; font-size: 0.85rem;
color: #9e9e9e; color: var(--spectrum-global-color-gray-600);
font-weight: 600; font-weight: 600;
margin: 1rem 1.5rem 0.5rem 1.5rem; margin: 1rem 1.5rem 0.5rem 1.5rem;
white-space: pre-wrap; white-space: pre-wrap;
@ -37,14 +36,14 @@
font-size: 2rem; font-size: 2rem;
font-weight: 600; font-weight: 600;
margin: 0 1.5rem 1.5rem 1.5rem; margin: 0 1.5rem 1.5rem 1.5rem;
color: inherit; color: var(--spectrum-global-color-blue-600);
white-space: pre-wrap; white-space: pre-wrap;
} }
.label { .label {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 400; font-weight: 400;
color: #9e9e9e; color: var(--spectrum-global-color-gray-600);
margin: 1rem 1.5rem; margin: 1rem 1.5rem;
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@ -1,5 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import "@spectrum-css/typography/dist/index-vars.css"
const { styleable, builderStore } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -14,8 +15,10 @@
$: placeholder = $builderStore.inBuilder && !text $: placeholder = $builderStore.inBuilder && !text
$: componentText = $builderStore.inBuilder $: componentText = $builderStore.inBuilder
? text || "Placeholder text" ? text || $component.name || "Placeholder text"
: text || "" : text || ""
$: sizeClass = `spectrum-Heading--size${size || "M"}`
$: alignClass = `align--${align || "left"}`
// Add color styles to main styles object, otherwise the styleable helper // Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style. // overrides the color when it's passed as inline style.
@ -41,25 +44,19 @@
class:bold class:bold
class:italic class:italic
class:underline class:underline
class="align--{align || 'left'} size--{size || 'M'}" class="spectrum-Heading {sizeClass} {alignClass}"
> >
{#if bold} {componentText}
<strong>{componentText}</strong>
{:else}
{componentText}
{/if}
</h1> </h1>
<style> <style>
h1 { h1 {
display: inline-block;
white-space: pre-wrap; white-space: pre-wrap;
font-weight: 600; font-weight: 600;
margin: 0;
} }
.placeholder { .placeholder {
font-style: italic; font-style: italic;
color: var(--grey-6); color: var(--spectrum-global-color-gray-600);
} }
.bold { .bold {
font-weight: 700; font-weight: 700;
@ -70,15 +67,7 @@
.underline { .underline {
text-decoration: underline; text-decoration: underline;
} }
.size--S {
font-size: 18px;
}
.size--M {
font-size: 22px;
}
.size--L {
font-size: 28px;
}
.align--left { .align--left {
text-align: left; text-align: left;
} }

View File

@ -1,21 +1,34 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
const { styleable } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let icon = "" export let icon
export let size = "fa-lg" export let size
export let color = "#f00" export let color
export let onClick export let onClick
$: styles = { $: styles = {
...$component.styles, ...$component.styles,
normal: { normal: {
...$component.styles.normal, ...$component.styles.normal,
color, color: color || "var(--spectrum-global-color-gray-900)",
}, },
} }
</script> </script>
<i use:styleable={styles} class="{icon} {size}" on:click={onClick} /> {#if icon}
<i use:styleable={styles} class="{icon} {size}" on:click={onClick} />
{:else if $builderStore.inBuilder}
<div use:styleable={styles}>
<Placeholder />
</div>
{/if}
<style>
div {
font-style: italic;
}
</style>

View File

@ -125,6 +125,7 @@
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;
background: var(--spectrum-alias-background-color-secondary);
} }
.nav-wrapper { .nav-wrapper {
@ -132,9 +133,10 @@
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
background: white; background: var(--spectrum-alias-background-color-primary);
z-index: 2; z-index: 2;
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075); border-bottom: 1px solid var(--spectrum-global-color-gray-300);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
} }
.layout--top .nav-wrapper.sticky { .layout--top .nav-wrapper.sticky {
position: sticky; position: sticky;
@ -312,7 +314,7 @@
transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out; transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out;
height: 100vh; height: 100vh;
opacity: 0; opacity: 0;
background: white; background: var(--spectrum-alias-background-color-secondary);
z-index: 999; z-index: 999;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
@ -326,7 +328,8 @@
.links.visible { .links.visible {
opacity: 1; opacity: 1;
transform: translateX(250px); transform: translateX(250px);
box-shadow: 0 0 40px 20px rgba(0, 0, 0, 0.1); box-shadow: 0 0 80px 20px rgba(0, 0, 0, 0.2);
border-right: 1px solid var(--spectrum-global-color-gray-300);
} }
.mobile-click-handler.visible { .mobile-click-handler.visible {
position: fixed; position: fixed;

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