Merge branch 'develop' into feature/oidc-support
This commit is contained in:
commit
73c8dcf71b
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
user: require("./src/cache/user"),
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./src/security/sessions")
|
|
@ -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)
|
||||
}
|
|
@ -11,6 +11,7 @@ const {
|
|||
auditLog,
|
||||
} = require("./middleware")
|
||||
const { setDB, getDB } = require("./db")
|
||||
const userCache = require("./cache/user")
|
||||
|
||||
// Strategies
|
||||
passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||
|
@ -55,6 +56,9 @@ module.exports = {
|
|||
jwt: require("jsonwebtoken"),
|
||||
auditLog,
|
||||
},
|
||||
cache: {
|
||||
user: userCache,
|
||||
},
|
||||
StaticDatabases,
|
||||
constants: require("./constants"),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const { Cookies } = require("../constants")
|
||||
const database = require("../db")
|
||||
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 PARAM_REGEX = /\/:(.*?)\//g
|
||||
|
@ -48,14 +48,27 @@ module.exports = (noAuthPatterns = [], opts) => {
|
|||
user = null,
|
||||
internal = false
|
||||
if (authCookie) {
|
||||
let error = null
|
||||
const sessionId = authCookie.sessionId,
|
||||
userId = authCookie.userId
|
||||
const session = await getSession(userId, sessionId)
|
||||
if (!session) {
|
||||
error = "No session found"
|
||||
} else {
|
||||
try {
|
||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||
user = await db.get(authCookie.userId)
|
||||
user = await getUser(userId)
|
||||
delete user.password
|
||||
authenticated = true
|
||||
} catch (err) {
|
||||
// remove the cookie as the use does not exist anymore
|
||||
error = err
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
// remove the cookie as the user does not exist anymore
|
||||
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"]
|
||||
|
|
|
@ -4,6 +4,8 @@ const { compare } = require("../../hashing")
|
|||
const env = require("../../environment")
|
||||
const { getGlobalUserByEmail } = require("../../utils")
|
||||
const { authError } = require("./utils")
|
||||
const { newid } = require("../../hashing")
|
||||
const { createASession } = require("../../security/sessions")
|
||||
|
||||
const INVALID_ERR = "Invalid Credentials"
|
||||
|
||||
|
@ -32,13 +34,16 @@ exports.authenticate = async function (email, password, done) {
|
|||
|
||||
// authenticate
|
||||
if (await compare(password, dbUser.password)) {
|
||||
const payload = {
|
||||
userId: dbUser._id,
|
||||
}
|
||||
const sessionId = newid()
|
||||
await createASession(dbUser._id, sessionId)
|
||||
|
||||
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
||||
expiresIn: "1 day",
|
||||
})
|
||||
dbUser.token = jwt.sign(
|
||||
{
|
||||
userId: dbUser._id,
|
||||
sessionId,
|
||||
},
|
||||
env.JWT_SECRET
|
||||
)
|
||||
// Remove users password in payload
|
||||
delete dbUser.password
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
|||
userInfoURL: body.userinfo_endpoint,
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: callbackUrl
|
||||
callbackURL: callbackUrl,
|
||||
},
|
||||
authenticate
|
||||
)
|
||||
|
|
|
@ -7,6 +7,8 @@ const {
|
|||
ViewNames,
|
||||
} = require("../../db/utils")
|
||||
const { authError } = require("./utils")
|
||||
const { newid } = require("../../hashing")
|
||||
const { createASession } = require("../../security/sessions")
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (!dbUser && requireLocalAccount ) {
|
||||
if (!dbUser && requireLocalAccount) {
|
||||
if (requireLocalAccount) {
|
||||
return authError(
|
||||
done,
|
||||
|
@ -82,15 +84,16 @@ exports.authenticateThirdParty = async function (
|
|||
dbUser._rev = response.rev
|
||||
|
||||
// authenticate
|
||||
const payload = {
|
||||
userId: dbUser._id,
|
||||
builder: dbUser.builder,
|
||||
email: dbUser.email,
|
||||
}
|
||||
const sessionId = newid()
|
||||
await createASession(dbUser._id, sessionId)
|
||||
|
||||
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
||||
expiresIn: "1 day",
|
||||
})
|
||||
dbUser.token = jwt.sign(
|
||||
{
|
||||
userId: dbUser._id,
|
||||
sessionId,
|
||||
},
|
||||
env.JWT_SECRET
|
||||
)
|
||||
|
||||
return done(null, dbUser)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
const env = require("../environment")
|
||||
// ioredis mock is all in memory
|
||||
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 STARTUP_TIMEOUT_MS = 5000
|
||||
|
@ -143,14 +148,15 @@ class RedisWrapper {
|
|||
CLIENT.disconnect()
|
||||
}
|
||||
|
||||
async scan() {
|
||||
async scan(key = "") {
|
||||
const db = this._db
|
||||
key = `${db}${SEPARATOR}${key}`
|
||||
let stream
|
||||
if (CLUSTERED) {
|
||||
let node = CLIENT.nodes("master")
|
||||
stream = node[0].scanStream({ match: db + "-*", count: 100 })
|
||||
stream = node[0].scanStream({ match: key + "*", count: 100 })
|
||||
} else {
|
||||
stream = CLIENT.scanStream({ match: db + "-*", count: 100 })
|
||||
stream = CLIENT.scanStream({ match: key + "*", count: 100 })
|
||||
}
|
||||
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) {
|
||||
const db = this._db
|
||||
await CLIENT.del(addDbPrefix(db, key))
|
||||
|
|
|
@ -11,8 +11,12 @@ exports.Databases = {
|
|||
INVITATIONS: "invitation",
|
||||
DEV_LOCKS: "devLocks",
|
||||
DEBOUNCE: "debounce",
|
||||
SESSIONS: "session",
|
||||
USER_CACHE: "users",
|
||||
}
|
||||
|
||||
exports.SEPARATOR = SEPARATOR
|
||||
|
||||
exports.getRedisOptions = (clustered = false) => {
|
||||
const [host, port] = REDIS_URL.split(":")
|
||||
const opts = {
|
||||
|
|
|
@ -147,7 +147,7 @@ exports.getRole = async (appId, roleId) => {
|
|||
*/
|
||||
async function getAllUserRoles(appId, userRoleId) {
|
||||
if (!userRoleId) {
|
||||
return [BUILTIN_IDS.PUBLIC]
|
||||
return [BUILTIN_IDS.BASIC]
|
||||
}
|
||||
let currentRole = await exports.getRole(appId, userRoleId)
|
||||
let roles = currentRole ? [currentRole] : []
|
||||
|
@ -226,7 +226,7 @@ exports.getAllRoles = async appId => {
|
|||
dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId
|
||||
)[0]
|
||||
if (dbBuiltin == null) {
|
||||
roles.push(builtinRole)
|
||||
roles.push(builtinRole || builtinRoles.BASIC)
|
||||
} else {
|
||||
// remove role and all back after combining with the builtin
|
||||
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {string} name The name of the cookie to set.
|
||||
* @param {string|object} value The value of cookie which will be set.
|
||||
*/
|
||||
exports.setCookie = (ctx, value, name = "builder") => {
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
if (!value) {
|
||||
ctx.cookies.set(name)
|
||||
} else {
|
||||
value = jwt.sign(value, options.secretOrKey, {
|
||||
expiresIn: "1 day",
|
||||
})
|
||||
value = jwt.sign(value, options.secretOrKey)
|
||||
ctx.cookies.set(name, value, {
|
||||
expires,
|
||||
maxAge: Number.MAX_SAFE_INTEGER,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"license": "AGPL-3.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
export let value
|
||||
export let size = "M"
|
||||
export let spectrumTheme
|
||||
|
||||
let open = false
|
||||
|
||||
$: color = value || "transparent"
|
||||
$: customValue = getCustomValue(value)
|
||||
$: checkColor = getCheckColor(value)
|
||||
|
||||
|
@ -21,7 +21,8 @@
|
|||
{
|
||||
label: "Grays",
|
||||
colors: [
|
||||
"white",
|
||||
"gray-50",
|
||||
"gray-75",
|
||||
"gray-100",
|
||||
"gray-200",
|
||||
"gray-300",
|
||||
|
@ -31,7 +32,6 @@
|
|||
"gray-700",
|
||||
"gray-800",
|
||||
"gray-900",
|
||||
"black",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -86,7 +86,7 @@
|
|||
return value
|
||||
}
|
||||
let found = false
|
||||
const comparisonValue = value.substring(35, value.length - 1)
|
||||
const comparisonValue = value.substring(28, value.length - 1)
|
||||
for (let category of categories) {
|
||||
found = category.colors.includes(comparisonValue)
|
||||
if (found) {
|
||||
|
@ -102,17 +102,19 @@
|
|||
|
||||
const getCheckColor = value => {
|
||||
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value)
|
||||
? "black"
|
||||
: "white"
|
||||
? "var(--spectrum-global-color-gray-900)"
|
||||
: "var(--spectrum-global-color-gray-50)"
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
||||
<div
|
||||
class="preview size--{size || 'M'}"
|
||||
style="background: {color};"
|
||||
on:click={() => (open = true)}
|
||||
class="fill {spectrumTheme || ''}"
|
||||
style={value ? `background: ${value};` : ""}
|
||||
class:placeholder={!value}
|
||||
/>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
use:clickOutside={() => (open = false)}
|
||||
|
@ -126,16 +128,20 @@
|
|||
{#each category.colors as color}
|
||||
<div
|
||||
on:click={() => {
|
||||
onChange(`var(--spectrum-global-color-static-${color})`)
|
||||
onChange(`var(--spectrum-global-color-${color})`)
|
||||
}}
|
||||
class="color"
|
||||
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};"
|
||||
title={prettyPrint(color)}
|
||||
>
|
||||
{#if value === `var(--spectrum-global-color-static-${color})`}
|
||||
<div
|
||||
class="fill {spectrumTheme || ''}"
|
||||
style="background: var(--spectrum-global-color-{color}); color: {checkColor};"
|
||||
>
|
||||
{#if value === `var(--spectrum-global-color-${color})`}
|
||||
<Icon name="Checkmark" size="S" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -170,12 +176,43 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 100%;
|
||||
position: relative;
|
||||
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 {
|
||||
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 {
|
||||
width: 20px;
|
||||
|
@ -219,8 +256,7 @@
|
|||
width: 16px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.color:hover {
|
||||
cursor: pointer;
|
||||
|
@ -236,4 +272,8 @@
|
|||
.category--custom .heading {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.spectrum-wrapper {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -145,4 +145,7 @@
|
|||
height: 100vh;
|
||||
z-index: 999;
|
||||
}
|
||||
:global(.flatpickr-calendar) {
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,8 +15,12 @@
|
|||
|
||||
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
|
||||
$: calculateIndicatorLength($tab)
|
||||
|
|
|
@ -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 |
|
@ -29,7 +29,7 @@ context("Create Bindings", () => {
|
|||
// 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
|
||||
// component exists on the page
|
||||
cy.getComponent(componentId).should("have.text", "Placeholder text")
|
||||
cy.getComponent(componentId).should("have.text", "New Paragraph")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.70",
|
||||
"@budibase/client": "^0.9.70",
|
||||
"@budibase/bbui": "^0.9.71",
|
||||
"@budibase/client": "^0.9.71",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^0.9.70",
|
||||
"@budibase/string-templates": "^0.9.71",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
selectedComponent,
|
||||
selectedAccessRole,
|
||||
} from "builderStore"
|
||||
// Backendstores
|
||||
import {
|
||||
datasources,
|
||||
integrations,
|
||||
|
@ -43,6 +42,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
appId: "",
|
||||
routes: {},
|
||||
clientLibPath: "",
|
||||
theme: "",
|
||||
}
|
||||
|
||||
export const getFrontendStore = () => {
|
||||
|
@ -62,6 +62,7 @@ export const getFrontendStore = () => {
|
|||
url: application.url,
|
||||
layouts,
|
||||
screens,
|
||||
theme: application.theme,
|
||||
hasAppPackage: true,
|
||||
appInstance: application.instance,
|
||||
clientLibPath,
|
||||
|
@ -79,6 +80,20 @@ export const getFrontendStore = () => {
|
|||
database.set(application.instance)
|
||||
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: {
|
||||
fetch: async () => {
|
||||
const response = await api.get("/api/routing")
|
||||
|
@ -122,6 +137,9 @@ export const getFrontendStore = () => {
|
|||
save: async screen => {
|
||||
const creatingNewScreen = screen._id === undefined
|
||||
const response = await api.post(`/api/screens`, screen)
|
||||
if (response.status !== 200) {
|
||||
return
|
||||
}
|
||||
screen = await response.json()
|
||||
await store.actions.routing.fetch()
|
||||
|
||||
|
@ -196,6 +214,11 @@ export const getFrontendStore = () => {
|
|||
const response = await api.post(`/api/layouts`, layoutToSave)
|
||||
const savedLayout = await response.json()
|
||||
|
||||
// Abort if saving failed
|
||||
if (response.status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
const layoutIdx = state.layouts.findIndex(
|
||||
stateLayout => stateLayout._id === savedLayout._id
|
||||
|
@ -313,16 +336,6 @@ export const getFrontendStore = () => {
|
|||
create: async (componentName, presetProps) => {
|
||||
const selected = get(selectedComponent)
|
||||
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
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
|
|
|
@ -38,8 +38,6 @@ const createScreen = table => {
|
|||
.instanceName("Form")
|
||||
.customProps({
|
||||
actionType: "Create",
|
||||
theme: "spectrum--lightest",
|
||||
size: "spectrum--medium",
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
makeTitleContainer,
|
||||
makeSaveButton,
|
||||
makeMainForm,
|
||||
spectrumColor,
|
||||
makeDatasourceFormComponents,
|
||||
} from "./utils/commonComponents"
|
||||
|
||||
|
@ -26,36 +25,13 @@ export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
|||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||
|
||||
function generateTitleContainer(table, title, formId, repeaterId) {
|
||||
// have to override style for this, its missing margin
|
||||
const saveButton = makeSaveButton(table, formId).normalStyle({
|
||||
background: "#000000",
|
||||
"border-width": "0",
|
||||
"border-style": "None",
|
||||
color: "#fff",
|
||||
"font-weight": "600",
|
||||
"font-size": "14px",
|
||||
})
|
||||
|
||||
const saveButton = makeSaveButton(table, formId)
|
||||
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")
|
||||
.customProps({
|
||||
className: "",
|
||||
disabled: false,
|
||||
type: "secondary",
|
||||
quiet: true,
|
||||
size: "M",
|
||||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
|
@ -76,7 +52,19 @@ function generateTitleContainer(table, title, formId, repeaterId) {
|
|||
})
|
||||
.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 => {
|
||||
|
@ -98,6 +86,7 @@ const createScreen = table => {
|
|||
valueType: "Binding",
|
||||
},
|
||||
],
|
||||
limit: table.type === "external" ? undefined : 1,
|
||||
paginate: false,
|
||||
})
|
||||
|
||||
|
|
|
@ -19,21 +19,10 @@ export const rowListUrl = table => sanitizeUrl(`/${table.name}`)
|
|||
|
||||
function generateTitleContainer(table) {
|
||||
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")
|
||||
.customProps({
|
||||
className: "",
|
||||
disabled: false,
|
||||
size: "M",
|
||||
type: "primary",
|
||||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
|
@ -46,12 +35,6 @@ function generateTitleContainer(table) {
|
|||
.instanceName("New Button")
|
||||
|
||||
const heading = new Component("@budibase/standard-components/heading")
|
||||
.normalStyle({
|
||||
margin: "0px",
|
||||
flex: "1 1 auto",
|
||||
"text-transform": "capitalize",
|
||||
})
|
||||
.type("h2")
|
||||
.instanceName("Title")
|
||||
.text(table.name)
|
||||
.customProps({
|
||||
|
@ -60,14 +43,12 @@ function generateTitleContainer(table) {
|
|||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
"margin-bottom": "32px",
|
||||
})
|
||||
.customProps({
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
size: "shrink",
|
||||
gap: "M",
|
||||
})
|
||||
.instanceName("Title Container")
|
||||
.addChild(heading)
|
||||
|
@ -91,68 +72,35 @@ const createScreen = table => {
|
|||
const spectrumTable = new Component("@budibase/standard-components/table")
|
||||
.customProps({
|
||||
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
|
||||
theme: "spectrum--lightest",
|
||||
showAutoColumns: false,
|
||||
quiet: true,
|
||||
size: "spectrum--medium",
|
||||
quiet: false,
|
||||
rowCount: 8,
|
||||
})
|
||||
.instanceName(`${table.name} Table`)
|
||||
|
||||
const safeTableId = makePropSafe(spectrumTable._json._id)
|
||||
const safeRowId = makePropSafe("_id")
|
||||
const viewButton = new Component("@budibase/standard-components/button")
|
||||
const viewLink = new Component("@budibase/standard-components/link")
|
||||
.customProps({
|
||||
text: "View",
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
size: "S",
|
||||
color: "var(--spectrum-global-color-gray-600)",
|
||||
align: "left",
|
||||
})
|
||||
.instanceName("View Button")
|
||||
.normalStyle({
|
||||
background: "transparent",
|
||||
"font-weight": "600",
|
||||
color: "#888",
|
||||
"border-width": "0",
|
||||
})
|
||||
.hoverStyle({
|
||||
color: "#4285f4",
|
||||
["margin-left"]: "16px",
|
||||
["margin-right"]: "16px",
|
||||
})
|
||||
.instanceName("View Link")
|
||||
|
||||
spectrumTable.addChild(viewButton)
|
||||
spectrumTable.addChild(viewLink)
|
||||
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()
|
||||
.route(rowListUrl(table))
|
||||
.instanceName(`${table.name} - List`)
|
||||
.addChild(mainContainer)
|
||||
.addChild(generateTitleContainer(table))
|
||||
.addChild(provider)
|
||||
.json()
|
||||
}
|
||||
|
|
|
@ -8,23 +8,16 @@ export function spectrumColor(number) {
|
|||
// 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
|
||||
// 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) {
|
||||
return new Component("@budibase/standard-components/link")
|
||||
.normalStyle({
|
||||
color: "#757575",
|
||||
"text-transform": "capitalize",
|
||||
})
|
||||
.hoverStyle({
|
||||
color: "#4285f4",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(tableName)
|
||||
.customProps({
|
||||
url: `/${tableName.toLowerCase()}`,
|
||||
openInNewTab: false,
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
@ -33,19 +26,12 @@ export function makeLinkComponent(tableName) {
|
|||
export function makeMainForm() {
|
||||
return new Component("@budibase/standard-components/form")
|
||||
.normalStyle({
|
||||
width: "700px",
|
||||
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",
|
||||
width: "600px",
|
||||
})
|
||||
.instanceName("Form")
|
||||
}
|
||||
|
||||
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||
export function makeBreadcrumbContainer(tableName, text) {
|
||||
const link = makeLinkComponent(tableName).instanceName("Back Link")
|
||||
|
||||
const arrowText = new Component("@budibase/standard-components/text")
|
||||
|
@ -53,42 +39,27 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.normalStyle({
|
||||
"margin-right": "4px",
|
||||
"margin-left": "4px",
|
||||
"margin-top": "0px",
|
||||
"margin-bottom": "0px",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(">")
|
||||
.instanceName("Arrow")
|
||||
.customProps({
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
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")
|
||||
.type("none")
|
||||
.normalStyle(textStyling)
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(text)
|
||||
.instanceName("Identifier")
|
||||
.customProps({
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
"font-size": "14px",
|
||||
color: "#757575",
|
||||
})
|
||||
.customProps({
|
||||
gap: "N",
|
||||
direction: "row",
|
||||
hAlign: "left",
|
||||
vAlign: "middle",
|
||||
|
@ -102,22 +73,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
|
||||
export function makeSaveButton(table, formId) {
|
||||
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")
|
||||
.customProps({
|
||||
className: "",
|
||||
disabled: false,
|
||||
type: "primary",
|
||||
size: "M",
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
|
@ -145,12 +104,6 @@ export function makeSaveButton(table, formId) {
|
|||
|
||||
export function makeTitleContainer(title) {
|
||||
const heading = new Component("@budibase/standard-components/heading")
|
||||
.normalStyle({
|
||||
margin: "0px",
|
||||
flex: "1 1 auto",
|
||||
})
|
||||
.customStyle(spectrumColor(900))
|
||||
.type("h2")
|
||||
.instanceName("Title")
|
||||
.text(title)
|
||||
.customProps({
|
||||
|
@ -168,6 +121,7 @@ export function makeTitleContainer(title) {
|
|||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
size: "shrink",
|
||||
gap: "M",
|
||||
})
|
||||
.instanceName("Title Container")
|
||||
.addChild(heading)
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
name: $views.selected?.name,
|
||||
}
|
||||
$: type = $tables.selected?.type
|
||||
$: isInternal = type === "internal"
|
||||
$: isInternal = type !== "external"
|
||||
|
||||
// Fetch rows for specified table
|
||||
$: {
|
||||
|
@ -72,9 +72,7 @@
|
|||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
{/if}
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={$tables.selected?._id} />
|
||||
{/if}
|
||||
|
|
|
@ -10,8 +10,10 @@
|
|||
let selectedRole = {}
|
||||
let errors = []
|
||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||
// Don't allow editing of public role
|
||||
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
|
||||
$: selectedRoleId = selectedRole._id
|
||||
$: otherRoles = $roles.filter(role => role._id !== selectedRoleId)
|
||||
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||
|
||||
const fetchBasePermissions = async () => {
|
||||
|
@ -96,7 +98,7 @@
|
|||
label="Role"
|
||||
value={selectedRoleId}
|
||||
on:change={changeRole}
|
||||
options={$roles}
|
||||
options={editableRoles}
|
||||
placeholder="Create new role"
|
||||
getOptionValue={role => role._id}
|
||||
getOptionLabel={role => role.name}
|
||||
|
|
|
@ -5,14 +5,17 @@
|
|||
import ICONS from "../icons"
|
||||
|
||||
export let integration = {}
|
||||
|
||||
let integrations = []
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
async function fetchIntegrations() {
|
||||
const response = await api.get("/api/integrations")
|
||||
const json = await response.json()
|
||||
|
||||
integrations = json
|
||||
integrations = {
|
||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
...json,
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
@ -21,7 +24,7 @@
|
|||
|
||||
// build the schema
|
||||
const schema = {}
|
||||
for (let key in selected.datasource) {
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
schema[key] = selected.datasource[key].default
|
||||
}
|
||||
|
||||
|
@ -39,7 +42,7 @@
|
|||
|
||||
<section>
|
||||
<div class="integration-list">
|
||||
{#each Object.keys(integrations) as integrationType}
|
||||
{#each Object.entries(integrations) as [integrationType, schema]}
|
||||
<div
|
||||
class="integration hoverable"
|
||||
class:selected={integration.type === integrationType}
|
||||
|
@ -50,7 +53,7 @@
|
|||
height="50"
|
||||
width="50"
|
||||
/>
|
||||
<Body size="XS">{integrationType}</Body>
|
||||
<Body size="XS">{schema.name || integrationType}</Body>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -2,15 +2,21 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { datasources } from "stores/backend"
|
||||
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 CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import analytics from "analytics"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
let error = ""
|
||||
const modalContext = getContext(Context.Modal)
|
||||
|
||||
let tableModal
|
||||
let name
|
||||
let error = ""
|
||||
let integration
|
||||
|
||||
$: checkOpenModal(integration && integration.type === "BUDIBASE")
|
||||
|
||||
function checkValid(evt) {
|
||||
const datasourceName = evt.target.value
|
||||
if (
|
||||
|
@ -22,6 +28,12 @@
|
|||
error = ""
|
||||
}
|
||||
|
||||
function checkOpenModal(isInternal) {
|
||||
if (isInternal) {
|
||||
tableModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
const { type, plus, ...config } = integration
|
||||
|
||||
|
@ -40,6 +52,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={tableModal} on:hide={modalContext.hide}>
|
||||
<CreateTableModal bind:name />
|
||||
</Modal>
|
||||
<ModalContent
|
||||
title="Create Datasource"
|
||||
size="L"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, url } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -27,7 +27,7 @@
|
|||
|
||||
$: tableNames = $tables.list.map(table => table.name)
|
||||
|
||||
let name
|
||||
export let name
|
||||
let dataImport
|
||||
let error = ""
|
||||
let createAutoscreens = true
|
||||
|
@ -91,7 +91,11 @@
|
|||
}
|
||||
|
||||
// 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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -5,12 +5,16 @@
|
|||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||
import { FrontendTypes } from "constants"
|
||||
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 layout
|
||||
let screen
|
||||
let confirmDeleteDialog
|
||||
let idToDelete
|
||||
let loading = true
|
||||
let error
|
||||
|
||||
// Create screen slot placeholder for use when a page is selected rather
|
||||
// than a screen
|
||||
|
@ -44,6 +48,7 @@
|
|||
screen,
|
||||
selectedComponentId,
|
||||
previewType: $store.currentFrontEndType,
|
||||
theme: $store.theme,
|
||||
}
|
||||
|
||||
// Saving pages and screens to the DB causes them to have _revs.
|
||||
|
@ -68,11 +73,28 @@
|
|||
onMount(() => {
|
||||
// Initialise the app when mounted
|
||||
iframe.contentWindow.addEventListener(
|
||||
"bb-ready",
|
||||
"ready",
|
||||
() => refreshContent(strippedJson),
|
||||
{ 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
|
||||
iframe.contentWindow.addEventListener("bb-event", event => {
|
||||
const { type, data } = event.detail
|
||||
|
@ -83,8 +105,14 @@
|
|||
} else if (type === "delete-component" && data.id) {
|
||||
idToDelete = data.id
|
||||
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 {
|
||||
console.log(data)
|
||||
console.warning(`Client sent unknown event type: ${type}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -99,11 +127,25 @@
|
|||
</script>
|
||||
|
||||
<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
|
||||
style="height: 100%; width: 100%"
|
||||
title="componentPreview"
|
||||
bind:this={iframe}
|
||||
srcdoc={template}
|
||||
class:hidden={loading || error}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
|
@ -131,4 +173,32 @@
|
|||
width: 100%;
|
||||
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>
|
||||
|
|
|
@ -27,9 +27,7 @@
|
|||
"name": "Card",
|
||||
"icon": "Card",
|
||||
"children": [
|
||||
"stackedlist",
|
||||
"card",
|
||||
"cardhorizontal",
|
||||
"spectrumcard",
|
||||
"cardstat"
|
||||
]
|
||||
},
|
||||
|
@ -57,13 +55,6 @@
|
|||
"icon",
|
||||
"embed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Other",
|
||||
"icon": "More",
|
||||
"children": [
|
||||
"screenslot"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -25,7 +25,9 @@ export default `
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
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 {
|
||||
flex: 1 1 auto;
|
||||
|
@ -46,7 +48,14 @@ export default `
|
|||
}
|
||||
|
||||
// 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
|
||||
window["##BUDIBASE_IN_BUILDER##"] = true
|
||||
|
@ -56,15 +65,24 @@ export default `
|
|||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||
window["##BUDIBASE_PREVIEW_TYPE##"] = previewType
|
||||
window["##BUDIBASE_PREVIEW_THEME##"] = theme
|
||||
|
||||
// Initialise app
|
||||
try {
|
||||
if (window.loadBudibase) {
|
||||
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.dispatchEvent(new Event("bb-ready"))
|
||||
window.dispatchEvent(new Event("ready"))
|
||||
</script>
|
||||
</head>
|
||||
<body/>
|
||||
|
|
|
@ -65,11 +65,14 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
{#if definition.editable !== false}
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
<MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}>
|
||||
Move up
|
||||
</MenuItem>
|
||||
|
@ -106,11 +109,12 @@
|
|||
>
|
||||
Paste inside
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
<ConfirmDialog
|
||||
</ActionMenu>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
||||
okText="Delete Component"
|
||||
onOk={deleteComponent}
|
||||
/>
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let components = []
|
||||
export let currentComponent
|
||||
|
@ -10,8 +11,6 @@
|
|||
export let level = 0
|
||||
export let dragDropStore
|
||||
|
||||
const isScreenslot = name => name?.endsWith("screenslot")
|
||||
|
||||
const selectComponent = component => {
|
||||
store.actions.components.select(component)
|
||||
}
|
||||
|
@ -42,6 +41,16 @@
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
const getComponentText = component => {
|
||||
if (component._instanceName) {
|
||||
return component._instanceName
|
||||
}
|
||||
const type =
|
||||
component._component.replace("@budibase/standard-components/", "") ||
|
||||
"component"
|
||||
return capitalise(type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
|
@ -63,9 +72,7 @@
|
|||
on:dragstart={dragstart(component)}
|
||||
on:dragover={dragover(component, index)}
|
||||
on:drop={dragDropStore.actions.drop}
|
||||
text={isScreenslot(component._component)
|
||||
? "Screenslot"
|
||||
: component._instanceName}
|
||||
text={getComponentText(component)}
|
||||
withArrow
|
||||
indentLevel={level + 1}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
<script>
|
||||
import { isEmpty } from "lodash/fp"
|
||||
import {
|
||||
Checkbox,
|
||||
Input,
|
||||
Select,
|
||||
DetailSummary,
|
||||
ColorPicker,
|
||||
} from "@budibase/bbui"
|
||||
import { Checkbox, Input, Select, DetailSummary } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||
|
@ -31,6 +25,8 @@
|
|||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import URLSelect from "./PropertyControls/URLSelect.svelte"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
|
@ -66,6 +62,7 @@
|
|||
section: SectionSelect,
|
||||
navigation: NavigationEditor,
|
||||
filter: FilterEditor,
|
||||
url: URLSelect,
|
||||
"field/string": StringFieldSelect,
|
||||
"field/number": NumberFieldSelect,
|
||||
"field/options": OptionsFieldSelect,
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
|
||||
</div>
|
||||
</DetailSummary>
|
||||
<Drawer bind:this={drawer} title="Custom CSS">
|
||||
{#key componentInstance?._id}
|
||||
<Drawer bind:this={drawer} title="Custom CSS">
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<DrawerContent slot="body">
|
||||
<div class="content">
|
||||
|
@ -45,7 +46,8 @@
|
|||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Drawer>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.content {
|
||||
|
|
|
@ -1,42 +1,8 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Colorpicker from "@budibase/colorpicker"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
import { ColorPicker } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
|
||||
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>
|
||||
|
||||
<Colorpicker value={value || "#C4C4C4"} on:change={onChange} />
|
||||
<ColorPicker {value} on:change spectrumTheme={$store.theme} />
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={drawer.show}>Define Actions</ActionButton>
|
||||
<ActionButton on:click={drawer.show}>Define actions</ActionButton>
|
||||
<Drawer bind:this={drawer} title={"Actions"}>
|
||||
<svelte:fragment slot="description">
|
||||
Define what actions to run.
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
export let value = []
|
||||
export let componentInstance
|
||||
let drawer
|
||||
let tempValue = value
|
||||
let tempValue = value || []
|
||||
|
||||
$: numFilters = Array.isArray(tempValue)
|
||||
? tempValue.length
|
||||
|
@ -31,15 +31,6 @@
|
|||
$: schemaFields = Object.values(schema || {})
|
||||
$: 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 () => {
|
||||
dispatch("change", tempValue)
|
||||
notifications.success("Filters saved.")
|
||||
|
@ -47,7 +38,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={drawer.show}>Define Filters</ActionButton>
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<DrawerContent slot="body">
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
loading = false
|
||||
}
|
||||
|
||||
$: displayValue = value ? value.substring(3) : "Pick Icon"
|
||||
$: displayValue = value ? value.substring(3) : "Pick icon"
|
||||
|
||||
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
|
||||
$: pageEndIdx = maxIconsPerPage * currentPage
|
||||
|
|
|
@ -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} />
|
|
@ -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 = {
|
||||
label: "Margin",
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
let selected = "Sources"
|
||||
let modal
|
||||
|
||||
$: isExternal =
|
||||
$params.selectedDatasource &&
|
||||
$params.selectedDatasource !== BUDIBASE_INTERNAL_DB
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
<script>
|
||||
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 { uuid } from "builderStore/uuid"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export let save
|
||||
export let datasource
|
||||
|
@ -14,16 +22,68 @@
|
|||
let originalFromName = fromRelationship.name,
|
||||
originalToName = toRelationship.name
|
||||
|
||||
function isValid(relationship) {
|
||||
if (
|
||||
relationship.relationshipType === RelationshipTypes.MANY_TO_MANY &&
|
||||
!relationship.through
|
||||
) {
|
||||
function inSchema(table, prop, ogName) {
|
||||
if (!table || !prop || prop === ogName) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
relationship.name && relationship.tableId && relationship.relationshipType
|
||||
)
|
||||
const keys = Object.keys(table.schema).map(key => key.toLowerCase())
|
||||
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 => ({
|
||||
|
@ -33,7 +93,15 @@
|
|||
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
|
||||
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
|
||||
$: 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
|
||||
$: relationshipTypes = [
|
||||
{
|
||||
|
@ -155,31 +223,55 @@
|
|||
<Select
|
||||
label="Select from table"
|
||||
options={tableOptions}
|
||||
on:change={() => ($touched.from = true)}
|
||||
bind:error={errors.from}
|
||||
bind:value={toRelationship.tableId}
|
||||
/>
|
||||
<Select
|
||||
label={"Select to table"}
|
||||
options={tableOptions}
|
||||
on:change={() => ($touched.to = true)}
|
||||
bind:error={errors.to}
|
||||
bind:value={fromRelationship.tableId}
|
||||
/>
|
||||
{#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
|
||||
<Select
|
||||
label={"Through"}
|
||||
options={tableOptions}
|
||||
on:change={() => ($touched.through = true)}
|
||||
bind:error={errors.through}
|
||||
bind:value={fromRelationship.through}
|
||||
/>
|
||||
{:else if toTable}
|
||||
{:else if fromRelationship?.relationshipType && toTable}
|
||||
<Select
|
||||
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}
|
||||
/>
|
||||
{/if}
|
||||
<div class="headings">
|
||||
<Detail>Column names</Detail>
|
||||
</div>
|
||||
<Input label="From table column" bind:value={fromRelationship.name} />
|
||||
<Input label="To table column" bind:value={toRelationship.name} />
|
||||
<Body>
|
||||
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">
|
||||
{#if originalFromName != null}
|
||||
<Button warning text on:click={deleteRelationship}>Delete</Button>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import { FrontendTypes } from "constants"
|
||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||
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
|
||||
let previousType
|
||||
|
@ -147,7 +148,10 @@
|
|||
|
||||
<div class="preview-pane">
|
||||
{#if $currentAsset}
|
||||
<div class="preview-header">
|
||||
<ComponentSelectionList />
|
||||
<AppThemeSelect />
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<CurrentItemPreview />
|
||||
</div>
|
||||
|
@ -193,6 +197,10 @@
|
|||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-xl) 40px;
|
||||
}
|
||||
.preview-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
}
|
||||
.preview-content {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
|
|
@ -28,12 +28,7 @@
|
|||
onMount(async () => {
|
||||
await organisation.init()
|
||||
await apps.load()
|
||||
// Skip the portal if you only have one app
|
||||
if (!$auth.isBuilder && $apps.filter(publishedAppsOnly).length === 1) {
|
||||
window.location = `/${publishedApps[0].prodId}`
|
||||
} else {
|
||||
loaded = true
|
||||
}
|
||||
})
|
||||
|
||||
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
$redirect("../")
|
||||
}
|
||||
}
|
||||
|
||||
$: console.log($page)
|
||||
</script>
|
||||
|
||||
{#if $auth.isAdmin}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
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
|
||||
$: appList = Object.keys($apps?.data).map(id => {
|
||||
const role = $userFetch?.data?.roles?.[id] || defaultRoleId
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
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]
|
||||
|
||||
async function updateUserRoles() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -18,9 +18,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.70",
|
||||
"@budibase/standard-components": "^0.9.70",
|
||||
"@budibase/string-templates": "^0.9.70",
|
||||
"@budibase/bbui": "^0.9.71",
|
||||
"@budibase/standard-components": "^0.9.71",
|
||||
"@budibase/string-templates": "^0.9.71",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -43,10 +43,9 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
|||
case 400:
|
||||
return handleError(`${url}: Bad Request`)
|
||||
case 403:
|
||||
// reload the page incase the token has expired
|
||||
if (!url.includes("self")) {
|
||||
location.reload()
|
||||
}
|
||||
notificationStore.danger(
|
||||
"Your session has expired, or you don't have permission to access that data"
|
||||
)
|
||||
return handleError(`${url}: Forbidden`)
|
||||
default:
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import API from "./api"
|
|||
/**
|
||||
* Fetches screen definition for an app.
|
||||
*/
|
||||
export const fetchAppDefinition = async appId => {
|
||||
export const fetchAppPackage = async appId => {
|
||||
return await API.get({
|
||||
url: `/api/applications/${appId}/definition`,
|
||||
url: `/api/applications/${appId}/appPackage`,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -24,7 +24,12 @@ export const logIn = async ({ email, password }) => {
|
|||
export const fetchSelf = async () => {
|
||||
const user = await API.get({ url: "/api/self" })
|
||||
if (user?._id) {
|
||||
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 {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -13,11 +13,14 @@
|
|||
authStore,
|
||||
routeStore,
|
||||
builderStore,
|
||||
appStore,
|
||||
} from "../store"
|
||||
import { TableNames, ActionTypes } from "../constants"
|
||||
import SettingsBar from "./preview/SettingsBar.svelte"
|
||||
import SelectionIndicator from "./preview/SelectionIndicator.svelte"
|
||||
import HoverIndicator from "./preview/HoverIndicator.svelte"
|
||||
import { Layout, Heading, Body } from "@budibase/bbui"
|
||||
import ErrorSVG from "../../../builder/assets/error.svg"
|
||||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
|
@ -25,12 +28,16 @@
|
|||
setContext("context", createContextStore())
|
||||
|
||||
let dataLoaded = false
|
||||
let permissionError = false
|
||||
|
||||
// Load app config
|
||||
onMount(async () => {
|
||||
await initialise()
|
||||
await authStore.actions.fetchUser()
|
||||
dataLoaded = true
|
||||
if ($builderStore.inBuilder) {
|
||||
builderStore.actions.notifyLoaded()
|
||||
}
|
||||
})
|
||||
|
||||
// Register this as a refreshable datasource so that user changes cause
|
||||
|
@ -43,27 +50,48 @@
|
|||
},
|
||||
]
|
||||
|
||||
// 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 ($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 {
|
||||
// The user is not logged in, redirect them to login
|
||||
const returnUrl = `${window.location.pathname}${window.location.hash}`
|
||||
const encodedUrl = encodeURIComponent(returnUrl)
|
||||
window.location = `/builder/auth/login?returnUrl=${encodedUrl}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: themeClass =
|
||||
$builderStore.theme || $appStore.application?.theme || "spectrum--light"
|
||||
</script>
|
||||
|
||||
{#if dataLoaded && $screenStore.activeLayout}
|
||||
{#if dataLoaded}
|
||||
<div
|
||||
id="spectrum-root"
|
||||
lang="en"
|
||||
dir="ltr"
|
||||
class="spectrum spectrum--medium spectrum--light"
|
||||
class="spectrum spectrum--medium {themeClass}"
|
||||
>
|
||||
{#if permissionError}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
{@html ErrorSVG}
|
||||
<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>
|
||||
{:else if $screenStore.activeLayout}
|
||||
<Provider key="user" data={$authStore} {actions}>
|
||||
<div id="app-root">
|
||||
{#key $screenStore.activeLayout._id}
|
||||
|
@ -87,6 +115,7 @@
|
|||
<HoverIndicator />
|
||||
{/if}
|
||||
</Provider>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -101,5 +130,54 @@
|
|||
}
|
||||
#app-root {
|
||||
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>
|
||||
|
|
|
@ -29,4 +29,9 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<IndicatorSet {componentId} color="rgb(120, 170, 244)" transition {zIndex} />
|
||||
<IndicatorSet
|
||||
{componentId}
|
||||
color="var(--spectrum-global-color-static-blue-200)"
|
||||
transition
|
||||
{zIndex}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<IndicatorSet
|
||||
componentId={$builderStore.selectedComponentId}
|
||||
color="rgb(66, 133, 244)"
|
||||
color="var(--spectrum-global-color-static-blue-600)"
|
||||
zIndex="910"
|
||||
transition
|
||||
/>
|
||||
|
|
|
@ -138,11 +138,11 @@
|
|||
padding: 6px 8px;
|
||||
opacity: 0;
|
||||
flex-direction: row;
|
||||
background: var(--background);
|
||||
background: var(--spectrum-alias-background-color-primary);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
transition: opacity 0.13s ease-in-out;
|
||||
}
|
||||
|
@ -155,4 +155,14 @@
|
|||
margin: 0 4px;
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import ClientApp from "./components/ClientApp.svelte"
|
||||
import { builderStore } from "./store"
|
||||
import { builderStore, appStore } from "./store"
|
||||
|
||||
let app
|
||||
|
||||
|
@ -7,14 +7,18 @@ const loadBudibase = () => {
|
|||
// Update builder store with any builder flags
|
||||
builderStore.set({
|
||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||
appId: window["##BUDIBASE_APP_ID##"],
|
||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||
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
|
||||
if (!app) {
|
||||
app = new ClientApp({
|
||||
|
|
|
@ -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()
|
|
@ -1,27 +1,14 @@
|
|||
import * as API from "../api"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { builderStore } from "./builder"
|
||||
import { TableNames } from "../constants"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
const createAuthStore = () => {
|
||||
const store = writable(null)
|
||||
|
||||
// Fetches the user object if someone is logged in and has reloaded the page
|
||||
const fetchUser = async () => {
|
||||
// Fetch the first user if inside the builder
|
||||
if (get(builderStore).inBuilder) {
|
||||
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 {
|
||||
subscribe: store.subscribe,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
import Manifest from "@budibase/standard-components/manifest.json"
|
||||
|
||||
const dispatchEvent = (type, data) => {
|
||||
const dispatchEvent = (type, data = {}) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bb-event", {
|
||||
detail: { type, data },
|
||||
|
@ -64,6 +64,9 @@ const createBuilderStore = () => {
|
|||
deleteComponent: id => {
|
||||
dispatchEvent("delete-component", { id })
|
||||
},
|
||||
notifyLoaded: () => {
|
||||
dispatchEvent("preview-loaded")
|
||||
},
|
||||
}
|
||||
return {
|
||||
...writableStore,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { authStore } from "./auth"
|
||||
export { appStore } from "./app"
|
||||
export { notificationStore } from "./notification"
|
||||
export { routeStore } from "./routes"
|
||||
export { screenStore } from "./screens"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { routeStore } from "./routes"
|
||||
import { screenStore } from "./screens"
|
||||
import { appStore } from "./app"
|
||||
|
||||
export async function initialise() {
|
||||
await routeStore.actions.fetchRoutes()
|
||||
await screenStore.actions.fetchScreens()
|
||||
await appStore.actions.fetchAppDefinition()
|
||||
}
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { derived } from "svelte/store"
|
||||
import { routeStore } from "./routes"
|
||||
import { builderStore } from "./builder"
|
||||
import * as API from "../api"
|
||||
import { appStore } from "./app"
|
||||
|
||||
const createScreenStore = () => {
|
||||
const config = writable({
|
||||
screens: [],
|
||||
layouts: [],
|
||||
})
|
||||
const store = derived(
|
||||
[config, routeStore, builderStore],
|
||||
([$config, $routeStore, $builderStore]) => {
|
||||
let activeLayout
|
||||
let activeScreen
|
||||
[appStore, routeStore, builderStore],
|
||||
([$appStore, $routeStore, $builderStore]) => {
|
||||
let activeLayout, activeScreen
|
||||
let layouts, screens
|
||||
if ($builderStore.inBuilder) {
|
||||
// Use builder defined definitions if inside the builder preview
|
||||
activeLayout = $builderStore.layout
|
||||
activeScreen = $builderStore.screen
|
||||
layouts = [activeLayout]
|
||||
screens = [activeScreen]
|
||||
} else {
|
||||
activeLayout = { props: { _component: "screenslot" } }
|
||||
|
||||
// Find the correct screen by matching the current route
|
||||
const { screens, layouts } = $config
|
||||
screens = $appStore.screens
|
||||
layouts = $appStore.layouts
|
||||
if ($routeStore.activeRoute) {
|
||||
activeScreen = screens.find(
|
||||
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 {
|
||||
subscribe: store.subscribe,
|
||||
actions: { fetchScreens },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export const styleable = (node, styles = {}) => {
|
|||
// overridden by any user specified styles
|
||||
let baseStyles = {}
|
||||
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.overflow = "hidden"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"watch": ["src"],
|
||||
"ext": "js,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
|
||||
"exec": "ts-node src/index.ts"
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -18,7 +18,7 @@
|
|||
"dev:stack:up": "node scripts/dev/manage.js up",
|
||||
"dev:stack:down": "node scripts/dev/manage.js down",
|
||||
"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",
|
||||
"lint": "eslint --fix src/",
|
||||
"lint:fix": "yarn run format && yarn run lint",
|
||||
|
@ -59,9 +59,9 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.70",
|
||||
"@budibase/client": "^0.9.70",
|
||||
"@budibase/string-templates": "^0.9.70",
|
||||
"@budibase/auth": "^0.9.71",
|
||||
"@budibase/client": "^0.9.71",
|
||||
"@budibase/string-templates": "^0.9.71",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
|
@ -114,7 +114,7 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/preset-env": "^7.14.4",
|
||||
"@budibase/standard-components": "^0.9.70",
|
||||
"@budibase/standard-components": "^0.9.71",
|
||||
"@jest/test-sequencer": "^24.8.0",
|
||||
"@types/bull": "^3.15.1",
|
||||
"@types/jest": "^26.0.23",
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
CREATE DATABASE IF NOT EXISTS main;
|
||||
USE main;
|
||||
CREATE TABLE Persons (
|
||||
PersonID int NOT NULL PRIMARY KEY,
|
||||
PersonID int NOT NULL AUTO_INCREMENT,
|
||||
LastName varchar(255),
|
||||
FirstName 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');
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
docker-compose down
|
||||
docker volume prune -f
|
|
@ -1,14 +1,14 @@
|
|||
SELECT 'CREATE DATABASE main'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
|
||||
CREATE TABLE Persons (
|
||||
PersonID INT NOT NULL PRIMARY KEY,
|
||||
PersonID SERIAL PRIMARY KEY,
|
||||
LastName varchar(255),
|
||||
FirstName varchar(255),
|
||||
Address varchar(255),
|
||||
City varchar(255)
|
||||
City varchar(255) DEFAULT 'Belfast'
|
||||
);
|
||||
CREATE TABLE Tasks (
|
||||
TaskID INT NOT NULL PRIMARY KEY,
|
||||
TaskID SERIAL PRIMARY KEY,
|
||||
PersonID INT,
|
||||
TaskName varchar(255),
|
||||
CONSTRAINT fkPersons
|
||||
|
@ -16,7 +16,7 @@ CREATE TABLE Tasks (
|
|||
REFERENCES Persons(PersonID)
|
||||
);
|
||||
CREATE TABLE Products (
|
||||
ProductID INT NOT NULL PRIMARY KEY,
|
||||
ProductID SERIAL PRIMARY KEY,
|
||||
ProductName varchar(255)
|
||||
);
|
||||
CREATE TABLE Products_Tasks (
|
||||
|
@ -30,12 +30,12 @@ CREATE TABLE Products_Tasks (
|
|||
REFERENCES Tasks(TaskID),
|
||||
PRIMARY KEY (ProductID, TaskID)
|
||||
);
|
||||
INSERT INTO Persons (PersonID, FirstName, LastName, Address, City) VALUES (1, 'Mike', 'Hughes', '123 Fake Street', 'Belfast');
|
||||
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (1, 1, 'assembling');
|
||||
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (2, 1, 'processing');
|
||||
INSERT INTO Products (ProductID, ProductName) VALUES (1, 'Computers');
|
||||
INSERT INTO Products (ProductID, ProductName) VALUES (2, 'Laptops');
|
||||
INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs');
|
||||
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');
|
||||
INSERT INTO Products (ProductName) VALUES ('Computers');
|
||||
INSERT INTO Products (ProductName) VALUES ('Laptops');
|
||||
INSERT INTO Products (ProductName) VALUES ('Chairs');
|
||||
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 (3, 1);
|
||||
|
|
|
@ -67,15 +67,18 @@ async function getAppUrlIfNotInUse(ctx) {
|
|||
let url
|
||||
if (ctx.request.body.url) {
|
||||
url = encodeURI(ctx.request.body.url)
|
||||
} else {
|
||||
} else if (ctx.request.body.name) {
|
||||
url = encodeURI(`${ctx.request.body.name}`)
|
||||
}
|
||||
if (url) {
|
||||
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
|
||||
}
|
||||
if (!env.SELF_HOSTED) {
|
||||
return url
|
||||
}
|
||||
const deployedApps = await getDeployedApps(ctx)
|
||||
if (
|
||||
url &&
|
||||
deployedApps[url] != null &&
|
||||
deployedApps[url].appId !== ctx.params.appId
|
||||
) {
|
||||
|
@ -161,7 +164,15 @@ exports.fetchAppDefinition = async function (ctx) {
|
|||
exports.fetchAppPackage = async function (ctx) {
|
||||
const db = new CouchDB(ctx.params.appId)
|
||||
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 = {
|
||||
application,
|
||||
|
|
|
@ -162,7 +162,7 @@ module External {
|
|||
manyRelationships: ManyRelationship[] = []
|
||||
for (let [key, field] of Object.entries(table.schema)) {
|
||||
// if set already, or not set just skip it
|
||||
if (!row[key] || newRow[key]) {
|
||||
if (!row[key] || newRow[key] || field.autocolumn) {
|
||||
continue
|
||||
}
|
||||
// if its not a link then just copy it over
|
||||
|
|
|
@ -34,13 +34,55 @@
|
|||
*:after {
|
||||
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>
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</body>
|
||||
|
|
|
@ -9,9 +9,13 @@ jest.mock("../../../utilities/redis", () => ({
|
|||
updateLock: jest.fn(),
|
||||
setDebounce: 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 { AppStatus } = require("../../../db/utils")
|
||||
|
||||
|
@ -32,7 +36,7 @@ describe("/applications", () => {
|
|||
.post("/api/applications")
|
||||
.send({ name: "My App" })
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body._id).toBeDefined()
|
||||
})
|
||||
|
@ -42,7 +46,7 @@ describe("/applications", () => {
|
|||
config,
|
||||
method: "POST",
|
||||
url: `/api/applications`,
|
||||
body: { name: "My App" }
|
||||
body: { name: "My App" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -55,7 +59,7 @@ describe("/applications", () => {
|
|||
const res = await request
|
||||
.get(`/api/applications?status=${AppStatus.DEV}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
// two created apps + the inited app
|
||||
|
@ -68,7 +72,7 @@ describe("/applications", () => {
|
|||
const res = await request
|
||||
.get(`/api/applications/${config.getAppId()}/definition`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
// should have empty packages
|
||||
expect(res.body.screens.length).toEqual(1)
|
||||
|
@ -81,7 +85,7 @@ describe("/applications", () => {
|
|||
const res = await request
|
||||
.get(`/api/applications/${config.getAppId()}/appPackage`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.application).toBeDefined()
|
||||
expect(res.body.screens.length).toEqual(1)
|
||||
|
@ -94,10 +98,10 @@ describe("/applications", () => {
|
|||
const res = await request
|
||||
.put(`/api/applications/${config.getAppId()}`)
|
||||
.send({
|
||||
name: "TEST_APP"
|
||||
name: "TEST_APP",
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.rev).toBeDefined()
|
||||
})
|
||||
|
@ -113,14 +117,14 @@ describe("/applications", () => {
|
|||
name: "UPDATED_NAME",
|
||||
})
|
||||
.set(headers)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.rev).toBeDefined()
|
||||
// retrieve the app to check it
|
||||
const getRes = await request
|
||||
.get(`/api/applications/${config.getAppId()}/appPackage`)
|
||||
.set(headers)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(getRes.body.application.updatedAt).toBeDefined()
|
||||
})
|
||||
|
|
|
@ -2,7 +2,6 @@ const setup = require("./utilities")
|
|||
const { basicScreen } = setup.structures
|
||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||
const workerRequests = require("../../../utilities/workerRequests")
|
||||
|
||||
const route = "/test"
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const EMPTY_LAYOUT = {
|
|||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_instanceName: "Screen slot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
@ -63,6 +64,7 @@ const BASE_LAYOUTS = [
|
|||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_instanceName: "Screen slot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
@ -84,6 +86,7 @@ const BASE_LAYOUTS = [
|
|||
normal: {},
|
||||
selected: {},
|
||||
},
|
||||
title: "{{ name }}",
|
||||
navigation: "Top",
|
||||
width: "Large",
|
||||
links: [
|
||||
|
@ -109,6 +112,7 @@ const BASE_LAYOUTS = [
|
|||
{
|
||||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
|
||||
_component: "@budibase/standard-components/screenslot",
|
||||
_instanceName: "Screen slot",
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface FieldSchema {
|
|||
relationshipType?: string
|
||||
through?: string
|
||||
foreignKey?: string
|
||||
autocolumn?: boolean
|
||||
constraints?: {
|
||||
type?: string
|
||||
email?: boolean
|
||||
|
|
|
@ -163,14 +163,19 @@ module MySQLModule {
|
|||
)
|
||||
for (let column of descResp) {
|
||||
const columnName = column.Field
|
||||
if (column.Key === "PRI") {
|
||||
if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) {
|
||||
primaryKeys.push(columnName)
|
||||
}
|
||||
const constraints = {
|
||||
presence: column.Null !== "YES",
|
||||
}
|
||||
const isAuto: boolean =
|
||||
typeof column.Extra === "string" &&
|
||||
(column.Extra === "auto_increment" ||
|
||||
column.Extra.toLowerCase().includes("generated"))
|
||||
schema[columnName] = {
|
||||
name: columnName,
|
||||
autocolumn: isAuto,
|
||||
type: convertType(column.Type, TYPE_MAP),
|
||||
constraints,
|
||||
}
|
||||
|
|
|
@ -147,7 +147,11 @@ module PostgresModule {
|
|||
if (!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) {
|
||||
tableKeys = {}
|
||||
|
@ -184,7 +188,11 @@ module PostgresModule {
|
|||
}
|
||||
|
||||
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] = {
|
||||
autocolumn: isAuto,
|
||||
name: columnName,
|
||||
type,
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("Postgres Integration", () => {
|
|||
|
||||
it("calls the create method with the correct params", async () => {
|
||||
const sql = "insert into users (name, age) values ('Joe', 123);"
|
||||
const response = await config.integration.create({
|
||||
await config.integration.create({
|
||||
sql
|
||||
})
|
||||
expect(pg.queryMock).toHaveBeenCalledWith(sql, {})
|
||||
|
@ -25,7 +25,7 @@ describe("Postgres Integration", () => {
|
|||
|
||||
it("calls the read method with the correct params", async () => {
|
||||
const sql = "select * from users;"
|
||||
const response = await config.integration.read({
|
||||
await config.integration.read({
|
||||
sql
|
||||
})
|
||||
expect(pg.queryMock).toHaveBeenCalledWith(sql, {})
|
||||
|
|
|
@ -2,10 +2,10 @@ const { getAppId, setCookie, getCookie, clearCookie } =
|
|||
require("@budibase/auth").utils
|
||||
const { Cookies } = require("@budibase/auth").constants
|
||||
const { getRole } = require("@budibase/auth/roles")
|
||||
const { getGlobalSelf } = require("../utilities/workerRequests")
|
||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||
const { generateUserMetadataID } = require("../db/utils")
|
||||
const { dbExists } = require("@budibase/auth/db")
|
||||
const { getCachedSelf } = require("../utilities/global")
|
||||
const CouchDB = require("../db")
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
|
@ -26,29 +26,17 @@ module.exports = async (ctx, next) => {
|
|||
}
|
||||
}
|
||||
|
||||
let updateCookie = false,
|
||||
appId,
|
||||
let appId,
|
||||
roleId = BUILTIN_ROLE_IDS.PUBLIC
|
||||
if (!ctx.user) {
|
||||
// not logged in, try to set a cookie for public apps
|
||||
updateCookie = true
|
||||
appId = requestAppId
|
||||
} else if (
|
||||
requestAppId != null &&
|
||||
(appCookie == null ||
|
||||
requestAppId !== appCookie.appId ||
|
||||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC ||
|
||||
!appCookie.roleId)
|
||||
) {
|
||||
} else if (requestAppId != null) {
|
||||
// Different App ID means cookie needs reset, or if the same public user has logged in
|
||||
const globalUser = await getGlobalSelf(ctx, requestAppId)
|
||||
updateCookie = true
|
||||
const globalUser = await getCachedSelf(ctx, requestAppId)
|
||||
appId = requestAppId
|
||||
// retrieving global user gets the right role
|
||||
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||
} else if (appCookie != null) {
|
||||
appId = appCookie.appId
|
||||
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC
|
||||
}
|
||||
// nothing more to do
|
||||
if (!appId) {
|
||||
|
@ -68,8 +56,12 @@ module.exports = async (ctx, next) => {
|
|||
role: await getRole(appId, roleId),
|
||||
}
|
||||
}
|
||||
if (updateCookie) {
|
||||
setCookie(ctx, { appId, roleId }, Cookies.CurrentApp)
|
||||
if (
|
||||
requestAppId !== appId ||
|
||||
appCookie == null ||
|
||||
appCookie.appId !== requestAppId
|
||||
) {
|
||||
setCookie(ctx, { appId }, Cookies.CurrentApp)
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
|
|
@ -23,6 +23,15 @@ function mockReset() {
|
|||
function mockAuthWithNoCookie() {
|
||||
jest.resetModules()
|
||||
mockWorker()
|
||||
jest.mock("@budibase/auth/cache", () => ({
|
||||
user: {
|
||||
getUser: () => {
|
||||
return {
|
||||
_id: "us_uuid1",
|
||||
}
|
||||
},
|
||||
},
|
||||
}))
|
||||
jest.mock("@budibase/auth", () => ({
|
||||
utils: {
|
||||
getAppId: jest.fn(),
|
||||
|
|
|
@ -17,6 +17,8 @@ const { cleanup } = require("../../utilities/fileSystem")
|
|||
const { Cookies } = require("@budibase/auth").constants
|
||||
const { jwt } = require("@budibase/auth").auth
|
||||
const { StaticDatabases } = require("@budibase/auth/db")
|
||||
const { createASession } = require("@budibase/auth/sessions")
|
||||
const { user: userCache } = require("@budibase/auth/cache")
|
||||
const CouchDB = require("../../db")
|
||||
|
||||
const GLOBAL_USER_ID = "us_uuid1"
|
||||
|
@ -62,7 +64,7 @@ class TestConfiguration {
|
|||
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)
|
||||
let existing
|
||||
try {
|
||||
|
@ -73,8 +75,9 @@ class TestConfiguration {
|
|||
const user = {
|
||||
_id: id,
|
||||
...existing,
|
||||
roles: {},
|
||||
roles: roles || {},
|
||||
}
|
||||
await createASession(id, "sessionid")
|
||||
if (builder) {
|
||||
user.builder = { global: true }
|
||||
}
|
||||
|
@ -103,6 +106,7 @@ class TestConfiguration {
|
|||
defaultHeaders() {
|
||||
const auth = {
|
||||
userId: GLOBAL_USER_ID,
|
||||
sessionId: "sessionid",
|
||||
}
|
||||
const app = {
|
||||
roleId: BUILTIN_ROLE_IDS.ADMIN,
|
||||
|
@ -138,13 +142,7 @@ class TestConfiguration {
|
|||
roleId = BUILTIN_ROLE_IDS.ADMIN,
|
||||
builder = false,
|
||||
}) {
|
||||
let user
|
||||
try {
|
||||
user = await this.createUser(email, PASSWORD, roleId)
|
||||
} catch (err) {
|
||||
// allow errors here
|
||||
}
|
||||
return this.login(email, PASSWORD, { roleId, userId: user._id, builder })
|
||||
return this.login(email, PASSWORD, { roleId, builder })
|
||||
}
|
||||
|
||||
async createApp(appName) {
|
||||
|
@ -313,6 +311,7 @@ class TestConfiguration {
|
|||
async createUser(id = null) {
|
||||
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
|
||||
const resp = await this.globalUser(globalId)
|
||||
await userCache.invalidateUser(globalId)
|
||||
return {
|
||||
...resp,
|
||||
globalId,
|
||||
|
@ -326,14 +325,19 @@ class TestConfiguration {
|
|||
}
|
||||
// make sure the user exists in the global DB
|
||||
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) {
|
||||
await this.createUser()
|
||||
}
|
||||
await createASession(userId, "sessionid")
|
||||
// have to fake this
|
||||
const auth = {
|
||||
userId,
|
||||
sessionId: "sessionid",
|
||||
}
|
||||
const app = {
|
||||
roleId: roleId,
|
||||
|
@ -343,6 +347,7 @@ class TestConfiguration {
|
|||
const appToken = jwt.sign(app, env.JWT_SECRET)
|
||||
|
||||
// returning necessary request headers
|
||||
await userCache.invalidateUser(userId)
|
||||
return {
|
||||
Accept: "application/json",
|
||||
Cookie: [
|
||||
|
|
|
@ -238,7 +238,10 @@ exports.readFileSync = (filepath, options = "utf8") => {
|
|||
*/
|
||||
exports.cleanup = 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ const {
|
|||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||
const { getGlobalUserParams } = require("@budibase/auth/db")
|
||||
const { user: userCache } = require("@budibase/auth/cache")
|
||||
|
||||
exports.updateAppRole = (appId, user) => {
|
||||
if (!user.roles) {
|
||||
|
@ -19,21 +20,30 @@ exports.updateAppRole = (appId, user) => {
|
|||
if (!user.roleId && user.builder && user.builder.global) {
|
||||
user.roleId = BUILTIN_ROLE_IDS.ADMIN
|
||||
} else if (!user.roleId) {
|
||||
user.roleId = BUILTIN_ROLE_IDS.PUBLIC
|
||||
user.roleId = BUILTIN_ROLE_IDS.BASIC
|
||||
}
|
||||
delete user.roles
|
||||
return user
|
||||
}
|
||||
|
||||
exports.getGlobalUser = async (appId, userId) => {
|
||||
const db = CouchDB(StaticDatabases.GLOBAL.name)
|
||||
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
|
||||
function processUser(appId, user) {
|
||||
if (user) {
|
||||
delete user.password
|
||||
}
|
||||
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) => {
|
||||
const db = CouchDB(StaticDatabases.GLOBAL.name)
|
||||
let globalUsers
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -73,13 +73,13 @@
|
|||
{
|
||||
"label": "Column",
|
||||
"value": "column",
|
||||
"barIcon": "ViewRow",
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "Column layout"
|
||||
},
|
||||
{
|
||||
"label": "Row",
|
||||
"value": "row",
|
||||
"barIcon": "ViewColumn",
|
||||
"barIcon": "ViewRow",
|
||||
"barTitle": "Row layout"
|
||||
}
|
||||
],
|
||||
|
@ -230,7 +230,8 @@
|
|||
"screenslot": {
|
||||
"name": "Screenslot",
|
||||
"icon": "WebPage",
|
||||
"description": "Contains your app screens"
|
||||
"description": "Contains your app screens",
|
||||
"editable": false
|
||||
},
|
||||
"button": {
|
||||
"name": "Button",
|
||||
|
@ -290,6 +291,11 @@
|
|||
],
|
||||
"defaultValue": "M"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Quiet",
|
||||
"key": "quiet"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
|
@ -448,6 +454,7 @@
|
|||
}
|
||||
},
|
||||
"stackedlist": {
|
||||
"deprecated": true,
|
||||
"name": "Stacked List",
|
||||
"icon": "TaskList",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
|
@ -811,30 +818,29 @@
|
|||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"defaultValue": "md",
|
||||
"defaultValue": "ri-1x",
|
||||
"options": [
|
||||
{ "value": "ri-xxs", "label": "xxs" },
|
||||
{ "value": "ri-xs", "label": "xs" },
|
||||
{ "value": "ri-sm", "label": "sm" },
|
||||
{ "value": "ri-1x", "label": "md" },
|
||||
{ "value": "ri-lg", "label": "lg" },
|
||||
{ "value": "ri-xl", "label": "xl" },
|
||||
{ "value": "ri-2x", "label": "2x" },
|
||||
{ "value": "ri-3x", "label": "3x" },
|
||||
{ "value": "ri-4x", "label": "4x" },
|
||||
{ "value": "ri-5x", "label": "5x" },
|
||||
{ "value": "ri-6x", "label": "6x" },
|
||||
{ "value": "ri-7x", "label": "7x" },
|
||||
{ "value": "ri-8x", "label": "8x" },
|
||||
{ "value": "ri-9x", "label": "9x" },
|
||||
{ "value": "ri-10x", "label": "10x" }
|
||||
{ "value": "ri-xxs", "label": "XXS" },
|
||||
{ "value": "ri-xs", "label": "XS" },
|
||||
{ "value": "ri-sm", "label": "Small" },
|
||||
{ "value": "ri-1x", "label": "Medium" },
|
||||
{ "value": "ri-lg", "label": "Large" },
|
||||
{ "value": "ri-xl", "label": "XL" },
|
||||
{ "value": "ri-2x", "label": "2XL" },
|
||||
{ "value": "ri-3x", "label": "3XL" },
|
||||
{ "value": "ri-4x", "label": "4XL" },
|
||||
{ "value": "ri-5x", "label": "5XL" },
|
||||
{ "value": "ri-6x", "label": "6XL" },
|
||||
{ "value": "ri-7x", "label": "7XL" },
|
||||
{ "value": "ri-8x", "label": "8XL" },
|
||||
{ "value": "ri-9x", "label": "9XL" },
|
||||
{ "value": "ri-10x", "label": "10XL" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"label": "Color",
|
||||
"key": "color",
|
||||
"defaultValue": "#000"
|
||||
"key": "color"
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
|
@ -844,6 +850,7 @@
|
|||
]
|
||||
},
|
||||
"navigation": {
|
||||
"deprecated": true,
|
||||
"name": "Nav Bar",
|
||||
"description": "A component for handling the navigation within your app.",
|
||||
"icon": "BreadcrumbNavigation",
|
||||
|
@ -876,7 +883,7 @@
|
|||
"key": "text"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"type": "url",
|
||||
"label": "URL",
|
||||
"key": "url",
|
||||
"placeholder": "/screen"
|
||||
|
@ -1638,6 +1645,7 @@
|
|||
"actions": [
|
||||
"ValidateForm"
|
||||
],
|
||||
"styles": ["size"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -1651,46 +1659,6 @@
|
|||
"label": "Schema",
|
||||
"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",
|
||||
"label": "Disabled",
|
||||
|
@ -2067,46 +2035,6 @@
|
|||
"key": "rowCount",
|
||||
"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",
|
||||
"label": "Columns",
|
||||
|
@ -2161,5 +2089,42 @@
|
|||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,15 @@
|
|||
"keywords": [
|
||||
"svelte"
|
||||
],
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"license": "MIT",
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
|
||||
"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/page": "^3.0.1",
|
||||
"@spectrum-css/typography": "^3.0.2",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"apexcharts": "^3.22.1",
|
||||
"dayjs": "^1.10.5",
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
export let onClick
|
||||
export let size = "M"
|
||||
export let type = "primary"
|
||||
export let quiet = false
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type}`}
|
||||
class:spectrum-Button--quiet={quiet}
|
||||
disabled={disabled || false}
|
||||
use:styleable={$component.styles}
|
||||
on:click={onClick}
|
||||
|
|
|
@ -20,14 +20,13 @@
|
|||
.container {
|
||||
min-width: 260px;
|
||||
width: max-content;
|
||||
border: 1px solid var(--grey-3);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 0.3rem;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.85rem;
|
||||
color: #9e9e9e;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-weight: 600;
|
||||
margin: 1rem 1.5rem 0.5rem 1.5rem;
|
||||
white-space: pre-wrap;
|
||||
|
@ -37,14 +36,14 @@
|
|||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 1.5rem 1.5rem 1.5rem;
|
||||
color: inherit;
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 400;
|
||||
color: #9e9e9e;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
margin: 1rem 1.5rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import "@spectrum-css/typography/dist/index-vars.css"
|
||||
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
@ -14,8 +15,10 @@
|
|||
|
||||
$: placeholder = $builderStore.inBuilder && !text
|
||||
$: componentText = $builderStore.inBuilder
|
||||
? text || "Placeholder text"
|
||||
? text || $component.name || "Placeholder text"
|
||||
: text || ""
|
||||
$: sizeClass = `spectrum-Heading--size${size || "M"}`
|
||||
$: alignClass = `align--${align || "left"}`
|
||||
|
||||
// Add color styles to main styles object, otherwise the styleable helper
|
||||
// overrides the color when it's passed as inline style.
|
||||
|
@ -41,25 +44,19 @@
|
|||
class:bold
|
||||
class:italic
|
||||
class:underline
|
||||
class="align--{align || 'left'} size--{size || 'M'}"
|
||||
class="spectrum-Heading {sizeClass} {alignClass}"
|
||||
>
|
||||
{#if bold}
|
||||
<strong>{componentText}</strong>
|
||||
{:else}
|
||||
{componentText}
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
.placeholder {
|
||||
font-style: italic;
|
||||
color: var(--grey-6);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
|
@ -70,15 +67,7 @@
|
|||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.size--S {
|
||||
font-size: 18px;
|
||||
}
|
||||
.size--M {
|
||||
font-size: 22px;
|
||||
}
|
||||
.size--L {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.align--left {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,34 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let icon = ""
|
||||
export let size = "fa-lg"
|
||||
export let color = "#f00"
|
||||
export let icon
|
||||
export let size
|
||||
export let color
|
||||
export let onClick
|
||||
|
||||
$: styles = {
|
||||
...$component.styles,
|
||||
normal: {
|
||||
...$component.styles.normal,
|
||||
color,
|
||||
color: color || "var(--spectrum-global-color-gray-900)",
|
||||
},
|
||||
}
|
||||
</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>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
|
@ -132,9 +133,10 @@
|
|||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
background: white;
|
||||
background: var(--spectrum-alias-background-color-primary);
|
||||
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 {
|
||||
position: sticky;
|
||||
|
@ -312,7 +314,7 @@
|
|||
transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out;
|
||||
height: 100vh;
|
||||
opacity: 0;
|
||||
background: white;
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
z-index: 999;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
@ -326,7 +328,8 @@
|
|||
.links.visible {
|
||||
opacity: 1;
|
||||
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 {
|
||||
position: fixed;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue