diff --git a/lerna.json b/lerna.json index c35a2dbdc9..78f7618651 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.70", + "version": "0.9.71", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/cache.js b/packages/auth/cache.js new file mode 100644 index 0000000000..48563a16f3 --- /dev/null +++ b/packages/auth/cache.js @@ -0,0 +1,3 @@ +module.exports = { + user: require("./src/cache/user"), +} diff --git a/packages/auth/package.json b/packages/auth/package.json index 9155d37eeb..690d232459 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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", diff --git a/packages/auth/sessions.js b/packages/auth/sessions.js new file mode 100644 index 0000000000..c07efa2380 --- /dev/null +++ b/packages/auth/sessions.js @@ -0,0 +1 @@ +module.exports = require("./src/security/sessions") diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js new file mode 100644 index 0000000000..46202cbfe9 --- /dev/null +++ b/packages/auth/src/cache/user.js @@ -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) +} diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index c56c5c5a05..98c558706a 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -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"), } diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index 64494f709d..db1fdfacd9 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -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) { - try { - const db = database.getDB(StaticDatabases.GLOBAL.name) - user = await db.get(authCookie.userId) - delete user.password - authenticated = true - } catch (err) { - // remove the cookie as the use does not exist anymore + let error = null + const sessionId = authCookie.sessionId, + userId = authCookie.userId + const session = await getSession(userId, sessionId) + if (!session) { + error = "No session found" + } else { + try { + user = await getUser(userId) + delete user.password + authenticated = true + } catch (err) { + error = err + } + } + if (error) { + // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookies.Auth) + } else { + // make sure we denote that the session is still in use + await updateSessionTTL(session) } } const apiKey = ctx.request.headers["x-budibase-api-key"] diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 01d1e00934..16b53bf894 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -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 diff --git a/packages/auth/src/middleware/passport/oidc.js b/packages/auth/src/middleware/passport/oidc.js index 6b39a0b20e..a1e3039121 100644 --- a/packages/auth/src/middleware/passport/oidc.js +++ b/packages/auth/src/middleware/passport/oidc.js @@ -110,7 +110,7 @@ exports.strategyFactory = async function (config, callbackUrl) { userInfoURL: body.userinfo_endpoint, clientID: clientId, clientSecret: clientSecret, - callbackURL: callbackUrl + callbackURL: callbackUrl, }, authenticate ) diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index c11465ec3b..82083236ca 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -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) } @@ -120,7 +123,7 @@ function syncUser(user, thirdPartyUser) { user.lastName = name.familyName } } - + // profile // @reviewers: Historically stored at the root level of the user // Nest to prevent conflicts with future fields diff --git a/packages/auth/src/redis/authRedis.js b/packages/auth/src/redis/authRedis.js new file mode 100644 index 0000000000..decce6763b --- /dev/null +++ b/packages/auth/src/redis/authRedis.js @@ -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 + }, +} diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js index e20255bfd3..4f2b5288ea 100644 --- a/packages/auth/src/redis/index.js +++ b/packages/auth/src/redis/index.js @@ -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)) diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index 23702353d8..415dcbf463 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -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 = { diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index 53e1b90d73..baa8fc40dc 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -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) diff --git a/packages/auth/src/security/sessions.js b/packages/auth/src/security/sessions.js new file mode 100644 index 0000000000..4051df7123 --- /dev/null +++ b/packages/auth/src/security/sessions.js @@ -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) +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 278ad07174..8bd635e2e3 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -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, diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 9b2b2823ef..a4b6349128 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -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", diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index 2f4b79b91a..4d248d6190 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -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)" }
-
(open = true)} - /> +
(open = true)}> +
+
{#if open}
(open = false)} @@ -126,15 +128,19 @@ {#each category.colors as color}
{ - 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})`} - - {/if} +
+ {#if value === `var(--spectrum-global-color-${color})`} + + {/if} +
{/each}
@@ -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; + } diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index c8f8bfc6c9..44b77e0bbe 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -145,4 +145,7 @@ height: 100vh; z-index: 999; } + :global(.flatpickr-calendar) { + font-family: "Source Sans Pro", sans-serif; + } diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte index 3e1080f2cd..77a8526a15 100644 --- a/packages/bbui/src/Tabs/Tabs.svelte +++ b/packages/bbui/src/Tabs/Tabs.svelte @@ -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) diff --git a/packages/builder/assets/error.svg b/packages/builder/assets/error.svg new file mode 100644 index 0000000000..4cc1d753c9 --- /dev/null +++ b/packages/builder/assets/error.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js index 04b7b0d6de..3f85526b11 100644 --- a/packages/builder/cypress/integration/createBinding.spec.js +++ b/packages/builder/cypress/integration/createBinding.spec.js @@ -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") }) }) diff --git a/packages/builder/package.json b/packages/builder/package.json index c13f883a3d..cf856552ce 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -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", diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index a462167ce2..ac837978a9 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -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( diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index 23f2fd846f..b890d42d54 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -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, diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index cb97bd53db..a1a1e17fd5 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -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, }) diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index 83b6e6a502..ccf1fceb29 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -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} }}`, - }, - }, - ], + 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() } diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 93361e6c11..aaf25f6d03 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -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) diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index e87ad35184..3293c694b6 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -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} {/if} - {#if isInternal} - - {/if} + {/if} diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte index f58b9f197f..8b7417c41f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -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} diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/index.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/index.svelte index 7e11a4c3ff..79ebdb1020 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/index.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/index.svelte @@ -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 @@
- {#each Object.keys(integrations) as integrationType} + {#each Object.entries(integrations) as [integrationType, schema]}
- {integrationType} + {schema.name || integrationType}
{/each}
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte index d8c29ed250..9cdd893230 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte @@ -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 @@ } + + + - 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) } diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte new file mode 100644 index 0000000000..38f8693f51 --- /dev/null +++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte @@ -0,0 +1,97 @@ + + +
+ onChange(event.detail)} + {placeholder} + {options} + /> + {#if !disabled} +
+ +
+ {/if} +
+ + + Add the objects on the left to enrich your text. + + + (tempValue = event.detail)} + bindableProperties={bindings} + /> + + + diff --git a/packages/builder/src/components/design/AppPreview/AppThemeSelect.svelte b/packages/builder/src/components/design/AppPreview/AppThemeSelect.svelte new file mode 100644 index 0000000000..c9d937593b --- /dev/null +++ b/packages/builder/src/components/design/AppPreview/AppThemeSelect.svelte @@ -0,0 +1,38 @@ + + +
+