Merge branch 'develop' into cypress-testing
This commit is contained in:
commit
2ee4c13826
|
@ -96,6 +96,10 @@ spec:
|
||||||
value: worker-service:{{ .Values.services.worker.port }}
|
value: worker-service:{{ .Values.services.worker.port }}
|
||||||
- name: COOKIE_DOMAIN
|
- name: COOKIE_DOMAIN
|
||||||
value: {{ .Values.globals.cookieDomain | quote }}
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
|
- name: ACCOUNT_PORTAL_URL
|
||||||
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
|
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||||
image: budibase/apps
|
image: budibase/apps
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
|
|
|
@ -89,6 +89,8 @@ spec:
|
||||||
value: {{ .Values.globals.selfHosted | quote }}
|
value: {{ .Values.globals.selfHosted | quote }}
|
||||||
- name: ACCOUNT_PORTAL_URL
|
- name: ACCOUNT_PORTAL_URL
|
||||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
|
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||||
- name: COOKIE_DOMAIN
|
- name: COOKIE_DOMAIN
|
||||||
value: {{ .Values.globals.cookieDomain | quote }}
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
image: budibase/worker
|
image: budibase/worker
|
||||||
|
|
|
@ -90,6 +90,7 @@ globals:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
selfHosted: 1
|
selfHosted: 1
|
||||||
accountPortalUrL: ""
|
accountPortalUrL: ""
|
||||||
|
accountPortalApiKey: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,11 @@ static_resources:
|
||||||
route:
|
route:
|
||||||
cluster: app-service
|
cluster: app-service
|
||||||
|
|
||||||
|
- match: { path: "/api/deploy" }
|
||||||
|
route:
|
||||||
|
timeout: 60s
|
||||||
|
cluster: app-service
|
||||||
|
|
||||||
# special case for when API requests are made, can just forward, not to minio
|
# special case for when API requests are made, can just forward, not to minio
|
||||||
- match: { prefix: "/api/" }
|
- match: { prefix: "/api/" }
|
||||||
route:
|
route:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -50,6 +50,8 @@
|
||||||
"multi:disable": "lerna run multi:disable",
|
"multi:disable": "lerna run multi:disable",
|
||||||
"selfhost:enable": "lerna run selfhost:enable",
|
"selfhost:enable": "lerna run selfhost:enable",
|
||||||
"selfhost:disable": "lerna run selfhost:disable",
|
"selfhost:disable": "lerna run selfhost:disable",
|
||||||
|
"localdomain:enable": "lerna run localdomain:enable",
|
||||||
|
"localdomain:disable": "lerna run localdomain:disable",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -12,7 +12,7 @@ const populateFromDB = async (userId, tenantId) => {
|
||||||
const user = await getGlobalDB(tenantId).get(userId)
|
const user = await getGlobalDB(tenantId).get(userId)
|
||||||
user.budibaseAccess = true
|
user.budibaseAccess = true
|
||||||
|
|
||||||
if (!env.SELF_HOSTED) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const account = await accounts.getAccount(user.email)
|
const account = await accounts.getAccount(user.email)
|
||||||
if (account) {
|
if (account) {
|
||||||
user.account = account
|
user.account = account
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
const API = require("./api")
|
const API = require("./api")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
const { Headers } = require("../constants")
|
||||||
|
|
||||||
const api = new API(env.ACCOUNT_PORTAL_URL)
|
const api = new API(env.ACCOUNT_PORTAL_URL)
|
||||||
|
|
||||||
// TODO: Authorization
|
|
||||||
|
|
||||||
exports.getAccount = async email => {
|
exports.getAccount = async email => {
|
||||||
const payload = {
|
const payload = {
|
||||||
email,
|
email,
|
||||||
}
|
}
|
||||||
const response = await api.post(`/api/accounts/search`, {
|
const response = await api.post(`/api/accounts/search`, {
|
||||||
body: payload,
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ module.exports = {
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
||||||
|
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
||||||
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
isTest,
|
isTest,
|
||||||
|
|
|
@ -7,6 +7,7 @@ exports.buildMatcherRegex = patterns => {
|
||||||
return patterns.map(pattern => {
|
return patterns.map(pattern => {
|
||||||
const isObj = typeof pattern === "object" && pattern.route
|
const isObj = typeof pattern === "object" && pattern.route
|
||||||
const method = isObj ? pattern.method : "GET"
|
const method = isObj ? pattern.method : "GET"
|
||||||
|
const strict = pattern.strict ? pattern.strict : false
|
||||||
let route = isObj ? pattern.route : pattern
|
let route = isObj ? pattern.route : pattern
|
||||||
|
|
||||||
const matches = route.match(PARAM_REGEX)
|
const matches = route.match(PARAM_REGEX)
|
||||||
|
@ -16,13 +17,19 @@ exports.buildMatcherRegex = patterns => {
|
||||||
route = route.replace(match, pattern)
|
route = route.replace(match, pattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { regex: new RegExp(route), method }
|
return { regex: new RegExp(route), method, strict, route }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.matches = (ctx, options) => {
|
exports.matches = (ctx, options) => {
|
||||||
return options.find(({ regex, method }) => {
|
return options.find(({ regex, method, strict, route }) => {
|
||||||
const urlMatch = regex.test(ctx.request.url)
|
let urlMatch
|
||||||
|
if (strict) {
|
||||||
|
urlMatch = ctx.request.url === route
|
||||||
|
} else {
|
||||||
|
urlMatch = regex.test(ctx.request.url)
|
||||||
|
}
|
||||||
|
|
||||||
const methodMatch =
|
const methodMatch =
|
||||||
method === "ALL"
|
method === "ALL"
|
||||||
? true
|
? true
|
||||||
|
|
|
@ -20,6 +20,10 @@ const getErrorMessage = () => {
|
||||||
return done.mock.calls[0][2].message
|
return done.mock.calls[0][2].message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveUser = async (user) => {
|
||||||
|
return await db.put(user)
|
||||||
|
}
|
||||||
|
|
||||||
describe("third party common", () => {
|
describe("third party common", () => {
|
||||||
describe("authenticateThirdParty", () => {
|
describe("authenticateThirdParty", () => {
|
||||||
let thirdPartyUser
|
let thirdPartyUser
|
||||||
|
@ -36,7 +40,7 @@ describe("third party common", () => {
|
||||||
|
|
||||||
describe("validation", () => {
|
describe("validation", () => {
|
||||||
const testValidation = async (message) => {
|
const testValidation = async (message) => {
|
||||||
await authenticateThirdParty(thirdPartyUser, false, done)
|
await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
|
||||||
expect(done.mock.calls.length).toBe(1)
|
expect(done.mock.calls.length).toBe(1)
|
||||||
expect(getErrorMessage()).toContain(message)
|
expect(getErrorMessage()).toContain(message)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +82,7 @@ describe("third party common", () => {
|
||||||
describe("when the user doesn't exist", () => {
|
describe("when the user doesn't exist", () => {
|
||||||
describe("when a local account is required", () => {
|
describe("when a local account is required", () => {
|
||||||
it("returns an error message", async () => {
|
it("returns an error message", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done)
|
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
||||||
expect(done.mock.calls.length).toBe(1)
|
expect(done.mock.calls.length).toBe(1)
|
||||||
expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.")
|
expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.")
|
||||||
})
|
})
|
||||||
|
@ -86,7 +90,7 @@ describe("third party common", () => {
|
||||||
|
|
||||||
describe("when a local account isn't required", () => {
|
describe("when a local account isn't required", () => {
|
||||||
it("creates and authenticates the user", async () => {
|
it("creates and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, false, done)
|
await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
expect(user.roles).toStrictEqual({})
|
expect(user.roles).toStrictEqual({})
|
||||||
|
@ -123,7 +127,7 @@ describe("third party common", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("syncs and authenticates the user", async () => {
|
it("syncs and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done)
|
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
||||||
|
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
|
@ -139,7 +143,7 @@ describe("third party common", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("syncs and authenticates the user", async () => {
|
it("syncs and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done)
|
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
||||||
|
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { generateGlobalUserID } = require("../../db/utils")
|
const { generateGlobalUserID } = require("../../db/utils")
|
||||||
|
const { saveUser } = require("../../utils")
|
||||||
const { authError } = require("./utils")
|
const { authError } = require("./utils")
|
||||||
const { newid } = require("../../hashing")
|
const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
|
@ -14,7 +15,8 @@ const fetch = require("node-fetch")
|
||||||
exports.authenticateThirdParty = async function (
|
exports.authenticateThirdParty = async function (
|
||||||
thirdPartyUser,
|
thirdPartyUser,
|
||||||
requireLocalAccount = true,
|
requireLocalAccount = true,
|
||||||
done
|
done,
|
||||||
|
saveUserFn = saveUser
|
||||||
) {
|
) {
|
||||||
if (!thirdPartyUser.provider) {
|
if (!thirdPartyUser.provider) {
|
||||||
return authError(done, "third party user provider required")
|
return authError(done, "third party user provider required")
|
||||||
|
@ -71,7 +73,13 @@ exports.authenticateThirdParty = async function (
|
||||||
dbUser = await syncUser(dbUser, thirdPartyUser)
|
dbUser = await syncUser(dbUser, thirdPartyUser)
|
||||||
|
|
||||||
// create or sync the user
|
// create or sync the user
|
||||||
const response = await db.put(dbUser)
|
let response
|
||||||
|
try {
|
||||||
|
response = await saveUserFn(dbUser, getTenantId(), false, false)
|
||||||
|
} catch (err) {
|
||||||
|
return authError(done, err)
|
||||||
|
}
|
||||||
|
|
||||||
dbUser._rev = response.rev
|
dbUser._rev = response.rev
|
||||||
|
|
||||||
// authenticate
|
// authenticate
|
||||||
|
|
|
@ -265,7 +265,7 @@ exports.downloadTarball = async (url, bucketName, path) => {
|
||||||
|
|
||||||
const tmpPath = join(budibaseTempDir(), path)
|
const tmpPath = join(budibaseTempDir(), path)
|
||||||
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
|
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
|
||||||
if (!env.isTest()) {
|
if (!env.isTest() && env.SELF_HOSTED) {
|
||||||
await exports.uploadDirectory(bucketName, tmpPath, path)
|
await exports.uploadDirectory(bucketName, tmpPath, path)
|
||||||
}
|
}
|
||||||
// return the temporary path incase there is a use for it
|
// return the temporary path incase there is a use for it
|
||||||
|
|
|
@ -191,6 +191,12 @@ class RedisWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTTL(key) {
|
||||||
|
const db = this._db
|
||||||
|
const prefixedKey = addDbPrefix(db, key)
|
||||||
|
return CLIENT.ttl(prefixedKey)
|
||||||
|
}
|
||||||
|
|
||||||
async setExpiry(key, expirySeconds) {
|
async setExpiry(key, expirySeconds) {
|
||||||
const db = this._db
|
const db = this._db
|
||||||
const prefixedKey = addDbPrefix(db, key)
|
const prefixedKey = addDbPrefix(db, key)
|
||||||
|
|
|
@ -19,6 +19,22 @@ const removeTenantFromInfoDB = async tenantId => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.removeUserFromInfoDB = async dbUser => {
|
||||||
|
const infoDb = getDB(PLATFORM_INFO_DB)
|
||||||
|
const keys = [dbUser._id, dbUser.email]
|
||||||
|
const userDocs = await infoDb.allDocs({
|
||||||
|
keys,
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
const toDelete = userDocs.rows.map(row => {
|
||||||
|
return {
|
||||||
|
...row.doc,
|
||||||
|
_deleted: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await infoDb.bulkDocs(toDelete)
|
||||||
|
}
|
||||||
|
|
||||||
const removeUsersFromInfoDB = async tenantId => {
|
const removeUsersFromInfoDB = async tenantId => {
|
||||||
try {
|
try {
|
||||||
const globalDb = getGlobalDB(tenantId)
|
const globalDb = getGlobalDB(tenantId)
|
||||||
|
|
|
@ -73,7 +73,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalDB = (tenantId = null) => {
|
exports.getGlobalDBName = (tenantId = null) => {
|
||||||
// tenant ID can be set externally, for example user API where
|
// tenant ID can be set externally, for example user API where
|
||||||
// new tenants are being created, this may be the case
|
// new tenants are being created, this may be the case
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
|
@ -81,13 +81,16 @@ exports.getGlobalDB = (tenantId = null) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dbName
|
let dbName
|
||||||
|
|
||||||
if (tenantId === DEFAULT_TENANT_ID) {
|
if (tenantId === DEFAULT_TENANT_ID) {
|
||||||
dbName = StaticDatabases.GLOBAL.name
|
dbName = StaticDatabases.GLOBAL.name
|
||||||
} else {
|
} else {
|
||||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||||
}
|
}
|
||||||
|
return dbName
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getGlobalDB = (tenantId = null) => {
|
||||||
|
const dbName = exports.getGlobalDBName(tenantId)
|
||||||
return getDB(dbName)
|
return getDB(dbName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,3 +107,13 @@ exports.lookupTenantId = async userId => {
|
||||||
}
|
}
|
||||||
return tenantId
|
return tenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lookup, could be email or userId, either will return a doc
|
||||||
|
exports.getTenantUser = async identifier => {
|
||||||
|
const db = getDB(PLATFORM_INFO_DB)
|
||||||
|
try {
|
||||||
|
return await db.get(identifier)
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
|
const {
|
||||||
|
DocumentTypes,
|
||||||
|
SEPARATOR,
|
||||||
|
ViewNames,
|
||||||
|
generateGlobalUserID,
|
||||||
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
const { createUserEmailView } = require("./db/views")
|
const { createUserEmailView } = require("./db/views")
|
||||||
const { Headers } = require("./constants")
|
const { Headers, UserStatus } = require("./constants")
|
||||||
const { getGlobalDB } = require("./tenancy")
|
const {
|
||||||
|
getGlobalDB,
|
||||||
|
updateTenantId,
|
||||||
|
getTenantUser,
|
||||||
|
tryAddTenant,
|
||||||
|
} = require("./tenancy")
|
||||||
const environment = require("./environment")
|
const environment = require("./environment")
|
||||||
|
const accounts = require("./cloud/accounts")
|
||||||
|
const { hash } = require("./hashing")
|
||||||
|
const userCache = require("./cache/user")
|
||||||
|
const env = require("./environment")
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
|
||||||
|
@ -131,3 +145,93 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.saveUser = async (
|
||||||
|
user,
|
||||||
|
tenantId,
|
||||||
|
hashPassword = true,
|
||||||
|
requirePassword = true
|
||||||
|
) => {
|
||||||
|
if (!tenantId) {
|
||||||
|
throw "No tenancy specified."
|
||||||
|
}
|
||||||
|
// need to set the context for this request, as specified
|
||||||
|
updateTenantId(tenantId)
|
||||||
|
// specify the tenancy incase we're making a new admin user (public)
|
||||||
|
const db = getGlobalDB(tenantId)
|
||||||
|
let { email, password, _id } = user
|
||||||
|
// make sure another user isn't using the same email
|
||||||
|
let dbUser
|
||||||
|
if (email) {
|
||||||
|
// check budibase users inside the tenant
|
||||||
|
dbUser = await exports.getGlobalUserByEmail(email)
|
||||||
|
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
||||||
|
throw `Email address ${email} already in use.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// check budibase users in other tenants
|
||||||
|
if (env.MULTI_TENANCY) {
|
||||||
|
dbUser = await getTenantUser(email)
|
||||||
|
if (dbUser != null && dbUser.tenantId !== tenantId) {
|
||||||
|
throw `Email address ${email} already in use.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check root account users in account portal
|
||||||
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
const account = await accounts.getAccount(email)
|
||||||
|
if (account && account.verified && account.tenantId !== tenantId) {
|
||||||
|
throw `Email address ${email} already in use.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbUser = await db.get(_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the password, make sure one is defined
|
||||||
|
let hashedPassword
|
||||||
|
if (password) {
|
||||||
|
hashedPassword = hashPassword ? await hash(password) : password
|
||||||
|
} else if (dbUser) {
|
||||||
|
hashedPassword = dbUser.password
|
||||||
|
} else if (requirePassword) {
|
||||||
|
throw "Password must be specified."
|
||||||
|
}
|
||||||
|
|
||||||
|
_id = _id || generateGlobalUserID()
|
||||||
|
user = {
|
||||||
|
createdAt: Date.now(),
|
||||||
|
...dbUser,
|
||||||
|
...user,
|
||||||
|
_id,
|
||||||
|
password: hashedPassword,
|
||||||
|
tenantId,
|
||||||
|
}
|
||||||
|
// make sure the roles object is always present
|
||||||
|
if (!user.roles) {
|
||||||
|
user.roles = {}
|
||||||
|
}
|
||||||
|
// add the active status to a user if its not provided
|
||||||
|
if (user.status == null) {
|
||||||
|
user.status = UserStatus.ACTIVE
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await db.put({
|
||||||
|
password: hashedPassword,
|
||||||
|
...user,
|
||||||
|
})
|
||||||
|
await tryAddTenant(tenantId, _id, email)
|
||||||
|
await userCache.invalidateUser(response.id)
|
||||||
|
return {
|
||||||
|
_id: response.id,
|
||||||
|
_rev: response.rev,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err.status === 409) {
|
||||||
|
throw "User exists already"
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
"@spectrum-css/underlay": "^2.0.9",
|
"@spectrum-css/underlay": "^2.0.9",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"svelte-flatpickr": "^3.1.0",
|
"svelte-flatpickr": "^3.2.3",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
|
disableMobile: "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
on:input={onInput}
|
on:input={onInput}
|
||||||
on:keyup={updateValueOnEnter}
|
on:keyup={updateValueOnEnter}
|
||||||
{type}
|
{type}
|
||||||
|
inputmode={type === "number" ? "decimal" : "text"}
|
||||||
class="spectrum-Textfield-input"
|
class="spectrum-Textfield-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2415,10 +2415,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
svelte-flatpickr@^3.1.0:
|
svelte-flatpickr@^3.2.3:
|
||||||
version "3.1.0"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.1.0.tgz#ad83588430dbd55196a1a258b8ba27e7f9c1ee37"
|
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8"
|
||||||
integrity sha512-zKyV+ukeVuJ8CW0Ing3T19VSekc4bPkou/5Riutt1yATrLvSsanNqcgqi7Q5IePvIoOF9GJ5OtHvn1qK9Wx9BQ==
|
integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw==
|
||||||
dependencies:
|
dependencies:
|
||||||
flatpickr "^4.5.2"
|
flatpickr "^4.5.2"
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
|
@ -24,9 +24,7 @@ context("Create a Table", () => {
|
||||||
it("updates a column on the table", () => {
|
it("updates a column on the table", () => {
|
||||||
cy.get(".title").click()
|
cy.get(".title").click()
|
||||||
cy.get(".spectrum-Table-editIcon > use").click()
|
cy.get(".spectrum-Table-editIcon > use").click()
|
||||||
cy.get("input")
|
cy.get("input").eq(1).type("updated", { force: true })
|
||||||
.eq(1)
|
|
||||||
.type("updated", { force: true })
|
|
||||||
// Unset table display column
|
// Unset table display column
|
||||||
cy.get(".spectrum-Switch-input").eq(1).click()
|
cy.get(".spectrum-Switch-input").eq(1).click()
|
||||||
cy.contains("Save Column").click()
|
cy.contains("Save Column").click()
|
||||||
|
@ -45,9 +43,7 @@ context("Create a Table", () => {
|
||||||
it("deletes a row", () => {
|
it("deletes a row", () => {
|
||||||
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||||
cy.contains("Delete 1 row(s)").click()
|
cy.contains("Delete 1 row(s)").click()
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal").contains("Delete").click()
|
||||||
.contains("Delete")
|
|
||||||
.click()
|
|
||||||
cy.contains("RoverUpdated").should("not.exist")
|
cy.contains("RoverUpdated").should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,15 +52,18 @@ context("Create a Table", () => {
|
||||||
cy.get(".spectrum-Table-editIcon > use").click()
|
cy.get(".spectrum-Table-editIcon > use").click()
|
||||||
cy.contains("Delete").click()
|
cy.contains("Delete").click()
|
||||||
cy.wait(50)
|
cy.wait(50)
|
||||||
cy.contains("Delete Column")
|
cy.contains("Delete Column").click()
|
||||||
.click()
|
|
||||||
cy.contains("nameupdated").should("not.exist")
|
cy.contains("nameupdated").should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes a table", () => {
|
it("deletes a table", () => {
|
||||||
cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use")
|
cy.get(".nav-item")
|
||||||
.eq(1)
|
.contains("dog")
|
||||||
.click({ force: true })
|
.parents(".nav-item")
|
||||||
|
.first()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".actions .spectrum-Icon").click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
||||||
cy.contains("Delete Table").click()
|
cy.contains("Delete Table").click()
|
||||||
cy.contains("dog").should("not.exist")
|
cy.contains("dog").should("not.exist")
|
||||||
|
|
|
@ -28,11 +28,7 @@ context("Create a View", () => {
|
||||||
const headers = Array.from($headers).map(header =>
|
const headers = Array.from($headers).map(header =>
|
||||||
header.textContent.trim()
|
header.textContent.trim()
|
||||||
)
|
)
|
||||||
expect(removeSpacing(headers)).to.deep.eq([
|
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
|
||||||
"group",
|
|
||||||
"age",
|
|
||||||
"rating",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -62,7 +58,7 @@ context("Create a View", () => {
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
cy.get(".spectrum-Picker-label").eq(0).click()
|
cy.get(".spectrum-Picker-label").eq(0).click()
|
||||||
cy.contains("Statistics").click()
|
cy.contains("Statistics").click()
|
||||||
|
|
||||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("age").click({ force: true })
|
cy.contains("age").click({ force: true })
|
||||||
|
|
||||||
|
@ -105,20 +101,20 @@ context("Create a View", () => {
|
||||||
cy.get(".spectrum-Table-cell").then($values => {
|
cy.get(".spectrum-Table-cell").then($values => {
|
||||||
let values = Array.from($values).map(header => header.textContent.trim())
|
let values = Array.from($values).map(header => header.textContent.trim())
|
||||||
expect(values).to.deep.eq([
|
expect(values).to.deep.eq([
|
||||||
"Students",
|
"Students",
|
||||||
"70",
|
"70",
|
||||||
"20",
|
"20",
|
||||||
"25",
|
"25",
|
||||||
"3",
|
"3",
|
||||||
"1650",
|
"1650",
|
||||||
"23.333333333333332",
|
"23.333333333333332",
|
||||||
"Teachers",
|
"Teachers",
|
||||||
"85",
|
"85",
|
||||||
"36",
|
"36",
|
||||||
"49",
|
"49",
|
||||||
"2",
|
"2",
|
||||||
"3697",
|
"3697",
|
||||||
"42.5",
|
"42.5",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,8 +31,7 @@ Cypress.Commands.add("login", () => {
|
||||||
Cypress.Commands.add("createApp", name => {
|
Cypress.Commands.add("createApp", name => {
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.contains(/Create (new )?app/).click()
|
cy.contains(/Start from scratch/).click()
|
||||||
cy.wait(500)
|
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
|
@ -187,7 +186,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.should("not.be.null")
|
.should("not.be.null")
|
||||||
.then(cy.wrap)
|
.then(cy.wrap)
|
||||||
.find(`[data-component-id=${componentId}]`)
|
.find(`[data-id=${componentId}]`)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("navigateToFrontend", () => {
|
Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset='utf8'>
|
<meta charset='utf8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
<title>Budibase</title>
|
<title>Budibase</title>
|
||||||
<link rel='icon' href='/src/favicon.ico'>
|
<link rel='icon' href='/src/favicon.png'>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.146-alpha.4",
|
"@budibase/bbui": "^0.9.154-alpha.1",
|
||||||
"@budibase/client": "^0.9.146-alpha.4",
|
"@budibase/client": "^0.9.154-alpha.1",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
"@budibase/string-templates": "^0.9.154-alpha.1",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
findAllMatchingComponents,
|
|
||||||
} from "./storeUtils"
|
} from "./storeUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
|
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
|
||||||
|
@ -422,6 +422,10 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
|
||||||
return !invalids.find(invalid => noSpaces?.includes(invalid))
|
return !invalids.find(invalid => noSpaces?.includes(invalid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceBetween(string, start, end, replacement) {
|
||||||
|
return string.substring(0, start) + replacement + string.substring(end)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||||
*/
|
*/
|
||||||
|
@ -431,6 +435,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
||||||
if (typeof textWithBindings !== "string") {
|
if (typeof textWithBindings !== "string") {
|
||||||
return textWithBindings
|
return textWithBindings
|
||||||
}
|
}
|
||||||
|
// work from longest to shortest
|
||||||
const convertFromProps = bindableProperties
|
const convertFromProps = bindableProperties
|
||||||
.map(el => el[convertFrom])
|
.map(el => el[convertFrom])
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
@ -440,12 +445,29 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
||||||
let result = textWithBindings
|
let result = textWithBindings
|
||||||
for (let boundValue of boundValues) {
|
for (let boundValue of boundValues) {
|
||||||
let newBoundValue = boundValue
|
let newBoundValue = boundValue
|
||||||
|
// we use a search string, where any time we replace something we blank it out
|
||||||
|
// in the search, working from longest to shortest so always use best match first
|
||||||
|
let searchString = newBoundValue
|
||||||
for (let from of convertFromProps) {
|
for (let from of convertFromProps) {
|
||||||
if (shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
if (shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
||||||
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
||||||
while (newBoundValue.includes(from)) {
|
let idx
|
||||||
newBoundValue = newBoundValue.replace(from, binding[convertTo])
|
do {
|
||||||
}
|
// see if any instances of this binding exist in the search string
|
||||||
|
idx = searchString.indexOf(from)
|
||||||
|
if (idx !== -1) {
|
||||||
|
let end = idx + from.length,
|
||||||
|
searchReplace = Array(binding[convertTo].length).join("*")
|
||||||
|
// blank out parts of the search string
|
||||||
|
searchString = replaceBetween(searchString, idx, end, searchReplace)
|
||||||
|
newBoundValue = replaceBetween(
|
||||||
|
newBoundValue,
|
||||||
|
idx,
|
||||||
|
end,
|
||||||
|
binding[convertTo]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} while (idx !== -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = result.replace(boundValue, newBoundValue)
|
result = result.replace(boundValue, newBoundValue)
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default class Automation {
|
||||||
this.automation.testData = data
|
this.automation.testData = data
|
||||||
}
|
}
|
||||||
|
|
||||||
addBlock(block) {
|
addBlock(block, idx) {
|
||||||
// Make sure to add trigger if doesn't exist
|
// Make sure to add trigger if doesn't exist
|
||||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
||||||
const trigger = { id: generate(), ...block }
|
const trigger = { id: generate(), ...block }
|
||||||
|
@ -26,10 +26,7 @@ export default class Automation {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBlock = { id: generate(), ...block }
|
const newBlock = { id: generate(), ...block }
|
||||||
this.automation.definition.steps = [
|
this.automation.definition.steps.splice(idx, 0, newBlock)
|
||||||
...this.automation.definition.steps,
|
|
||||||
newBlock,
|
|
||||||
]
|
|
||||||
return newBlock
|
return newBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,9 +104,12 @@ const automationActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addBlockToAutomation: block => {
|
addBlockToAutomation: (block, blockIdx) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const newBlock = state.selectedAutomation.addBlock(cloneDeep(block))
|
const newBlock = state.selectedAutomation.addBlock(
|
||||||
|
cloneDeep(block),
|
||||||
|
blockIdx
|
||||||
|
)
|
||||||
state.selectedBlock = newBlock
|
state.selectedBlock = newBlock
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
database,
|
database,
|
||||||
tables,
|
tables,
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
|
|
||||||
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
||||||
import api from "../api"
|
import api from "../api"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
|
@ -25,6 +24,7 @@ import {
|
||||||
findComponentParent,
|
findComponentParent,
|
||||||
findClosestMatchingComponent,
|
findClosestMatchingComponent,
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
|
findComponent,
|
||||||
} from "../storeUtils"
|
} from "../storeUtils"
|
||||||
import { uuid } from "../uuid"
|
import { uuid } from "../uuid"
|
||||||
import { removeBindings } from "../dataBinding"
|
import { removeBindings } from "../dataBinding"
|
||||||
|
@ -67,6 +67,14 @@ export const getFrontendStore = () => {
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath } = pkg
|
const { layouts, screens, application, clientLibPath } = pkg
|
||||||
const components = await fetchComponentLibDefinitions(application.appId)
|
const components = await fetchComponentLibDefinitions(application.appId)
|
||||||
|
// make sure app isn't locked
|
||||||
|
if (
|
||||||
|
components &&
|
||||||
|
components.status === 400 &&
|
||||||
|
components.message?.includes("lock")
|
||||||
|
) {
|
||||||
|
throw { ok: false, reason: "locked" }
|
||||||
|
}
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
libraries: application.componentLibraries,
|
libraries: application.componentLibraries,
|
||||||
|
@ -464,6 +472,24 @@ export const getFrontendStore = () => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch full definition
|
||||||
|
component = findComponent(asset.props, component._id)
|
||||||
|
|
||||||
|
// Ensure we aren't deleting the screen slot
|
||||||
|
if (component._component?.endsWith("/screenslot")) {
|
||||||
|
throw "You can't delete the screen slot"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we aren't deleting something that contains the screen slot
|
||||||
|
const screenslot = findComponentType(
|
||||||
|
component,
|
||||||
|
"@budibase/standard-components/screenslot"
|
||||||
|
)
|
||||||
|
if (screenslot != null) {
|
||||||
|
throw "You can't delete a component that contains the screen slot"
|
||||||
|
}
|
||||||
|
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset.props, component._id)
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, Detail, Body, Icon } from "@budibase/bbui"
|
import { ModalContent, Layout, Detail, Body, Icon } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { database } from "stores/backend"
|
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
$: instanceId = $database._id
|
|
||||||
|
|
||||||
|
export let blockIdx
|
||||||
let selectedAction
|
let selectedAction
|
||||||
let actionVal
|
let actionVal
|
||||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||||
|
@ -39,7 +38,8 @@
|
||||||
)
|
)
|
||||||
automationStore.actions.addBlockToAutomation(newBlock)
|
automationStore.actions.addBlockToAutomation(newBlock)
|
||||||
await automationStore.actions.save(
|
await automationStore.actions.save(
|
||||||
$automationStore.selectedAutomation?.automation
|
$automationStore.selectedAutomation?.automation,
|
||||||
|
blockIdx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
import FlowItem from "./FlowItem.svelte"
|
import FlowItem from "./FlowItem.svelte"
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
import { flip } from "svelte/animate"
|
import { flip } from "svelte/animate"
|
||||||
import { fade, fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import {
|
import {
|
||||||
Detail,
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -57,26 +57,24 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
<Detail size="L">{automation.name}</Detail>
|
<Heading size="S">{automation.name}</Heading>
|
||||||
<div
|
<div style="display:flex;">
|
||||||
style="display:flex;
|
<div class="iconPadding">
|
||||||
color: var(--spectrum-global-color-gray-400);"
|
|
||||||
>
|
|
||||||
<span class="iconPadding">
|
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<Icon
|
<Icon
|
||||||
on:click={confirmDeleteDialog.show}
|
on:click={confirmDeleteDialog.show}
|
||||||
hoverable
|
hoverable
|
||||||
|
size="M"
|
||||||
name="DeleteOutline"
|
name="DeleteOutline"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
testDataModal.show()
|
testDataModal.show()
|
||||||
}}
|
}}
|
||||||
icon="MultipleCheck"
|
icon="MultipleCheck"
|
||||||
size="S">Run test</ActionButton
|
size="M">Run test</ActionButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,16 +82,11 @@
|
||||||
{#each blocks as block, idx (block.id)}
|
{#each blocks as block, idx (block.id)}
|
||||||
<div
|
<div
|
||||||
class="block"
|
class="block"
|
||||||
animate:flip={{ duration: 800 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fade|local
|
in:fly|local={{ x: 500, duration: 1500 }}
|
||||||
out:fly|local={{ x: 500 }}
|
out:fly|local={{ x: 500, duration: 800 }}
|
||||||
>
|
>
|
||||||
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
|
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
|
||||||
{#if idx !== blocks.length - 1}
|
|
||||||
<div class="separator" />
|
|
||||||
<Icon name="AddCircle" size="S" />
|
|
||||||
<div class="separator" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,14 +107,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.separator {
|
|
||||||
width: 1px;
|
|
||||||
height: 25px;
|
|
||||||
border-left: 1px dashed var(--grey-4);
|
|
||||||
color: var(--grey-4);
|
|
||||||
/* center horizontally */
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
.canvas {
|
.canvas {
|
||||||
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
|
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@ -153,11 +138,14 @@
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.iconPadding {
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
|
||||||
padding-right: var(--spacing-m);
|
padding-right: var(--spacing-m);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import ResultsModal from "./ResultsModal.svelte"
|
import ResultsModal from "./ResultsModal.svelte"
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
import { database } from "stores/backend"
|
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
|
||||||
export let onSelect
|
export let onSelect
|
||||||
|
@ -29,7 +28,6 @@
|
||||||
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
|
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
|
||||||
step => step.stepId === block.stepId
|
step => step.stepId === block.stepId
|
||||||
)
|
)
|
||||||
$: instanceId = $database._id
|
|
||||||
|
|
||||||
$: isTrigger = block.type === "TRIGGER"
|
$: isTrigger = block.type === "TRIGGER"
|
||||||
|
|
||||||
|
@ -40,6 +38,10 @@
|
||||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||||
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
|
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
|
||||||
|
|
||||||
|
$: totalBlocks =
|
||||||
|
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
||||||
|
1
|
||||||
|
|
||||||
// Logic for hiding / showing the add button.first we check if it has a child
|
// Logic for hiding / showing the add button.first we check if it has a child
|
||||||
// then we check to see whether its inputs have been commpleted
|
// then we check to see whether its inputs have been commpleted
|
||||||
$: disableAddButton = isTrigger
|
$: disableAddButton = isTrigger
|
||||||
|
@ -167,13 +169,24 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={actionModal} width="30%">
|
<Modal bind:this={actionModal} width="30%">
|
||||||
<ActionModal bind:blockComplete />
|
<ActionModal {blockIdx} bind:blockComplete />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
<CreateWebhookModal />
|
<CreateWebhookModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="separator" />
|
||||||
|
<Icon
|
||||||
|
on:click={() => actionModal.show()}
|
||||||
|
disabled={!hasCompletedInputs}
|
||||||
|
hoverable
|
||||||
|
name="AddCircle"
|
||||||
|
size="S"
|
||||||
|
/>
|
||||||
|
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||||
|
<div class="separator" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.center-items {
|
.center-items {
|
||||||
|
@ -191,8 +204,7 @@
|
||||||
.block {
|
.block {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
background-color: var(--background);
|
||||||
color: var(--grey-9);
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px 4px 4px 4px;
|
border-radius: 4px 4px 4px 4px;
|
||||||
}
|
}
|
||||||
|
@ -200,4 +212,13 @@
|
||||||
.blockSection {
|
.blockSection {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 25px;
|
||||||
|
border-left: 1px dashed var(--grey-4);
|
||||||
|
color: var(--grey-4);
|
||||||
|
/* center horizontally */
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Icon, notifications } from "@budibase/bbui"
|
import { Input, Icon, notifications } from "@budibase/bbui"
|
||||||
import { store, hostingStore } from "builderStore"
|
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let production = false
|
|
||||||
|
|
||||||
$: appId = $store.appId
|
|
||||||
$: appUrl = $hostingStore.appUrl
|
|
||||||
|
|
||||||
function fullWebhookURL(uri) {
|
function fullWebhookURL(uri) {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if (production) {
|
|
||||||
return `${appUrl}/${uri}`
|
return `${window.location.origin}/${uri}`
|
||||||
} else {
|
|
||||||
return `${window.location.origin}/${uri}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
|
|
|
@ -11,16 +11,18 @@
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
$: enrichedDataSources = $datasources.list.map(datasource => {
|
$: enrichedDataSources = Array.isArray($datasources.list)
|
||||||
const selected = $datasources.selected === datasource._id
|
? $datasources.list.map(datasource => {
|
||||||
const open = openDataSources.includes(datasource._id)
|
const selected = $datasources.selected === datasource._id
|
||||||
const containsSelected = containsActiveEntity(datasource)
|
const open = openDataSources.includes(datasource._id)
|
||||||
return {
|
const containsSelected = containsActiveEntity(datasource)
|
||||||
...datasource,
|
return {
|
||||||
selected,
|
...datasource,
|
||||||
open: selected || open || containsSelected,
|
selected,
|
||||||
}
|
open: selected || open || containsSelected,
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
: []
|
||||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||||
$: {
|
$: {
|
||||||
// Ensure the open data source is always included in the list of open
|
// Ensure the open data source is always included in the list of open
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Body size="XS"
|
<Body size="S"
|
||||||
>All apps need data. You can connect to a data source below, or add data
|
>All apps need data. You can connect to a data source below, or add data
|
||||||
to your app using Budibase's built-in database.
|
to your app using Budibase's built-in database.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
|
@ -3,26 +3,33 @@
|
||||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources, tables } from "stores/backend"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames } from "constants"
|
||||||
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
|
export let modal
|
||||||
|
|
||||||
|
// kill the reference so the input isn't saved
|
||||||
|
let config = cloneDeep(integration)
|
||||||
|
|
||||||
function prepareData() {
|
function prepareData() {
|
||||||
let datasource = {}
|
let datasource = {}
|
||||||
let existingTypeCount = $datasources.list.filter(
|
let existingTypeCount = $datasources.list.filter(
|
||||||
ds => ds.source == integration.type
|
ds => ds.source == config.type
|
||||||
).length
|
).length
|
||||||
|
|
||||||
let baseName = IntegrationNames[integration.type]
|
let baseName = IntegrationNames[config.type]
|
||||||
let name =
|
let name =
|
||||||
existingTypeCount == 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
existingTypeCount === 0
|
||||||
|
? baseName
|
||||||
|
: `${baseName}-${existingTypeCount + 1}`
|
||||||
|
|
||||||
datasource.type = "datasource"
|
datasource.type = "datasource"
|
||||||
datasource.source = integration.type
|
datasource.source = config.type
|
||||||
datasource.config = integration.config
|
datasource.config = config.config
|
||||||
datasource.name = name
|
datasource.name = name
|
||||||
datasource.plus = integration.plus
|
datasource.plus = config.plus
|
||||||
|
|
||||||
return datasource
|
return datasource
|
||||||
}
|
}
|
||||||
|
@ -32,6 +39,8 @@
|
||||||
// Create datasource
|
// Create datasource
|
||||||
const resp = await datasources.save(datasource, datasource.plus)
|
const resp = await datasources.save(datasource, datasource.plus)
|
||||||
|
|
||||||
|
// update the tables incase data source plus
|
||||||
|
await tables.fetch()
|
||||||
await datasources.select(resp._id)
|
await datasources.select(resp._id)
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
|
@ -48,9 +57,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[integration.type]}`}
|
title={`Connect to ${IntegrationNames[config.type]}`}
|
||||||
onConfirm={() => saveDatasource()}
|
onConfirm={() => saveDatasource()}
|
||||||
confirmText={integration.plus
|
onCancel={() => modal.show()}
|
||||||
|
confirmText={config.plus
|
||||||
? "Fetch tables from database"
|
? "Fetch tables from database"
|
||||||
: "Save and continue to query"}
|
: "Save and continue to query"}
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
|
@ -62,10 +72,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm schema={config.schema} integration={config.config} />
|
||||||
schema={integration.schema}
|
|
||||||
bind:integration={integration.config}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,21 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte"
|
|
||||||
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
|
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
|
||||||
const DeploymentStatus = {
|
|
||||||
SUCCESS: "SUCCESS",
|
|
||||||
PENDING: "PENDING",
|
|
||||||
FAILURE: "FAILURE",
|
|
||||||
}
|
|
||||||
|
|
||||||
const POLL_INTERVAL = 10000
|
|
||||||
|
|
||||||
let feedbackModal
|
let feedbackModal
|
||||||
let deployments = []
|
|
||||||
let poll
|
|
||||||
let publishModal
|
let publishModal
|
||||||
|
|
||||||
async function deployApp() {
|
async function deployApp() {
|
||||||
|
@ -34,62 +23,6 @@
|
||||||
notifications.error(`Error publishing app: ${err}`)
|
notifications.error(`Error publishing app: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDeployments() {
|
|
||||||
try {
|
|
||||||
const response = await api.get(`/api/deployments`)
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
if (deployments.length > 0) {
|
|
||||||
checkIncomingDeploymentStatus(deployments, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
deployments = json
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
clearInterval(poll)
|
|
||||||
notifications.error(
|
|
||||||
"Error fetching deployment history. Please try again."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required to check any updated deployment statuses between polls
|
|
||||||
function checkIncomingDeploymentStatus(current, incoming) {
|
|
||||||
for (let incomingDeployment of incoming) {
|
|
||||||
if (
|
|
||||||
incomingDeployment.status === DeploymentStatus.FAILURE ||
|
|
||||||
incomingDeployment.status === DeploymentStatus.SUCCESS
|
|
||||||
) {
|
|
||||||
const currentDeployment = current.find(
|
|
||||||
deployment => deployment._id === incomingDeployment._id
|
|
||||||
)
|
|
||||||
|
|
||||||
// We have just been notified of an ongoing deployments status change
|
|
||||||
if (
|
|
||||||
!currentDeployment ||
|
|
||||||
currentDeployment.status === DeploymentStatus.PENDING
|
|
||||||
) {
|
|
||||||
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
|
|
||||||
notifications.error(incomingDeployment.err)
|
|
||||||
} else {
|
|
||||||
notifications.send(
|
|
||||||
"Published to Production.",
|
|
||||||
"success",
|
|
||||||
"CheckmarkCircle"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchDeployments()
|
|
||||||
poll = setInterval(fetchDeployments, POLL_INTERVAL)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => clearInterval(poll))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button secondary on:click={publishModal.show}>Publish</Button>
|
<Button secondary on:click={publishModal.show}>Publish</Button>
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
import {
|
||||||
|
ProgressCircle,
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import ErrorSVG from "assets/error.svg?raw"
|
import ErrorSVG from "assets/error.svg?raw"
|
||||||
|
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
|
@ -102,7 +110,7 @@
|
||||||
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (iframe.contentWindow) {
|
if (iframe.contentWindow) {
|
||||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
||||||
|
@ -122,6 +130,26 @@
|
||||||
// Wait for this event to show the client library if intelligent
|
// Wait for this event to show the client library if intelligent
|
||||||
// loading is supported
|
// loading is supported
|
||||||
loading = false
|
loading = false
|
||||||
|
} else if (type === "move-component") {
|
||||||
|
const { componentId, destinationComponentId } = data
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
|
||||||
|
// Get source and destination components
|
||||||
|
const source = findComponent(rootComponent, componentId)
|
||||||
|
const destination = findComponent(rootComponent, destinationComponentId)
|
||||||
|
|
||||||
|
// Stop if the target is a child of source
|
||||||
|
const path = findComponentPath(source, destinationComponentId)
|
||||||
|
const ids = path.map(component => component._id)
|
||||||
|
if (ids.includes(data.destinationComponentId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut and paste the component to the new destination
|
||||||
|
if (source && destination) {
|
||||||
|
store.actions.components.copy(source, true)
|
||||||
|
store.actions.components.paste(destination, data.mode)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warning(`Client sent unknown event type: ${type}`)
|
console.warning(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
|
@ -144,10 +172,15 @@
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponent = () => {
|
const deleteComponent = async () => {
|
||||||
store.actions.components.delete({ _id: idToDelete })
|
try {
|
||||||
|
await store.actions.components.delete({ _id: idToDelete })
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error)
|
||||||
|
}
|
||||||
idToDelete = null
|
idToDelete = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelDeleteComponent = () => {
|
const cancelDeleteComponent = () => {
|
||||||
idToDelete = null
|
idToDelete = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
cancelText="Close"
|
cancelText="View changes"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
title="Theme settings"
|
title="Theme settings"
|
||||||
>
|
>
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<Label size="L">Primary color</Label>
|
<Label size="L">Accent color</Label>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor}
|
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor}
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<Label size="L">Primary color (hover)</Label>
|
<Label size="L">Accent color (hover)</Label>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
spectrumTheme={$store.theme}
|
spectrumTheme={$store.theme}
|
||||||
value={$store.customTheme?.primaryColorHover ||
|
value={$store.customTheme?.primaryColorHover ||
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { findComponentParent } from "builderStore/storeUtils"
|
import { findComponentParent } from "builderStore/storeUtils"
|
||||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
||||||
|
@ -51,7 +51,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponent = async () => {
|
const deleteComponent = async () => {
|
||||||
await store.actions.components.delete(component)
|
try {
|
||||||
|
await store.actions.components.delete(component)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeComponentForCopy = (cut = false) => {
|
const storeComponentForCopy = (cut = false) => {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
|
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
|
||||||
import getTemplates from "builderStore/store/screenTemplates"
|
import getTemplates from "builderStore/store/screenTemplates"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
|
||||||
const CONTAINER = "@budibase/standard-components/container"
|
const CONTAINER = "@budibase/standard-components/container"
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@
|
||||||
if (!event.detail.startsWith("/")) {
|
if (!event.detail.startsWith("/")) {
|
||||||
route = "/" + event.detail
|
route = "/" + event.detail
|
||||||
}
|
}
|
||||||
route = route.replace(/ +/g, "-")
|
route = sanitizeUrl(route)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let bindings
|
export let bindings
|
||||||
|
@ -37,7 +38,12 @@
|
||||||
key: "routing.route",
|
key: "routing.route",
|
||||||
label: "Route",
|
label: "Route",
|
||||||
control: Input,
|
control: Input,
|
||||||
parser: val => val.replace(/ +/g, "-"),
|
parser: val => {
|
||||||
|
if (!val.startsWith("/")) {
|
||||||
|
val = "/" + val
|
||||||
|
}
|
||||||
|
return sanitizeUrl(val)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
||||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { APP_NAME_REGEX } from "constants"
|
import { APP_NAME_REGEX } from "constants"
|
||||||
|
import TemplateList from "./TemplateList.svelte"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
|
@ -31,12 +32,16 @@
|
||||||
APP_NAME_REGEX,
|
APP_NAME_REGEX,
|
||||||
"App name must be letters, numbers and spaces only"
|
"App name must be letters, numbers and spaces only"
|
||||||
),
|
),
|
||||||
file: template ? mixed().required("Please choose a file to import") : null,
|
file: template?.fromFile
|
||||||
|
? mixed().required("Please choose a file to import")
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
let submitting = false
|
let submitting = false
|
||||||
let valid = false
|
let valid = false
|
||||||
|
|
||||||
$: checkValidity($values, validator)
|
$: checkValidity($values, validator)
|
||||||
|
$: showTemplateSelection = !template?.fromFile && !template?.key
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
await hostingStore.actions.fetchDeployedApps()
|
||||||
|
@ -73,7 +78,7 @@
|
||||||
submitting = true
|
submitting = true
|
||||||
|
|
||||||
// Check a template exists if we are important
|
// Check a template exists if we are important
|
||||||
if (template && !$values.file) {
|
if (template?.fromFile && !$values.file) {
|
||||||
$errors.file = "Please choose a file to import"
|
$errors.file = "Please choose a file to import"
|
||||||
valid = false
|
valid = false
|
||||||
submitting = false
|
submitting = false
|
||||||
|
@ -133,33 +138,59 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
{#if showTemplateSelection}
|
||||||
title={template ? "Import app" : "Create app"}
|
<ModalContent
|
||||||
confirmText={template ? "Import app" : "Create app"}
|
title={"Get started quickly"}
|
||||||
onConfirm={createNewApp}
|
showConfirmButton={false}
|
||||||
disabled={!valid}
|
size="L"
|
||||||
>
|
onConfirm={() => {
|
||||||
{#if template}
|
showTemplateSelection = false
|
||||||
<Dropzone
|
return false
|
||||||
error={$touched.file && $errors.file}
|
}}
|
||||||
gallery={false}
|
showCancelButton={false}
|
||||||
label="File to import"
|
showCloseIcon={false}
|
||||||
value={[$values.file]}
|
>
|
||||||
on:change={e => {
|
<Body size="M">Select a template below, or start from scratch.</Body>
|
||||||
$values.file = e.detail?.[0]
|
<TemplateList
|
||||||
$touched.file = true
|
onSelect={selected => {
|
||||||
|
if (!selected) {
|
||||||
|
showTemplateSelection = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template = selected
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
</ModalContent>
|
||||||
<Body size="S">
|
{:else}
|
||||||
Give your new app a name, and choose which groups have access (paid plans
|
<ModalContent
|
||||||
only).
|
title={template?.fromFile ? "Import app" : "Create app"}
|
||||||
</Body>
|
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||||
<Input
|
onConfirm={createNewApp}
|
||||||
bind:value={$values.name}
|
disabled={!valid}
|
||||||
error={$touched.name && $errors.name}
|
>
|
||||||
on:blur={() => ($touched.name = true)}
|
{#if template?.fromFile}
|
||||||
label="Name"
|
<Dropzone
|
||||||
/>
|
error={$touched.file && $errors.file}
|
||||||
<Checkbox label="Group access" disabled value={true} text="All users" />
|
gallery={false}
|
||||||
</ModalContent>
|
label="File to import"
|
||||||
|
value={[$values.file]}
|
||||||
|
on:change={e => {
|
||||||
|
$values.file = e.detail?.[0]
|
||||||
|
$touched.file = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Body size="S">
|
||||||
|
Give your new app a name, and choose which groups have access (paid plans
|
||||||
|
only).
|
||||||
|
</Body>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.name}
|
||||||
|
error={$touched.name && $errors.name}
|
||||||
|
on:blur={() => ($touched.name = true)}
|
||||||
|
label="Name"
|
||||||
|
/>
|
||||||
|
<Checkbox label="Group access" disabled value={true} text="All users" />
|
||||||
|
</ModalContent>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Heading, Body } from "@budibase/bbui"
|
import { Heading, Layout, Icon } from "@budibase/bbui"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
@ -13,8 +13,7 @@
|
||||||
let templatesPromise = fetchTemplates()
|
let templatesPromise = fetchTemplates()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="M">Start With a Template</Heading>
|
|
||||||
{#await templatesPromise}
|
{#await templatesPromise}
|
||||||
<div class="spinner-container">
|
<div class="spinner-container">
|
||||||
<Spinner size="30" />
|
<Spinner size="30" />
|
||||||
|
@ -22,41 +21,69 @@
|
||||||
{:then templates}
|
{:then templates}
|
||||||
<div class="templates">
|
<div class="templates">
|
||||||
{#each templates as template}
|
{#each templates as template}
|
||||||
<div class="templates-card">
|
<div class="template" on:click={() => onSelect(template)}>
|
||||||
<Heading size="S">{template.name}</Heading>
|
<div
|
||||||
<Body size="M" grey>{template.category}</Body>
|
class="background-icon"
|
||||||
<Body size="S" black>{template.description}</Body>
|
style={`background: ${template.background};`}
|
||||||
<div><img alt="template" src={template.image} width="100%" /></div>
|
>
|
||||||
<div class="card-footer">
|
<Icon name={template.icon} />
|
||||||
<Button secondary on:click={() => onSelect(template)}>
|
|
||||||
Create
|
|
||||||
{template.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Heading size="XS">{template.name}</Heading>
|
||||||
|
<p class="detail">{template?.category?.toUpperCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
|
||||||
|
<div class="background-icon" style={`background: var(--background);`}>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
<Heading size="XS">Start from scratch</Heading>
|
||||||
|
<p class="detail">BLANK</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:catch err}
|
{:catch err}
|
||||||
<h1 style="color:red">{err}</h1>
|
<h1 style="color:red">{err}</h1>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.templates {
|
.templates {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
width: 100%;
|
||||||
grid-gap: var(--layout-m);
|
grid-gap: var(--spacing-m);
|
||||||
|
grid-template-columns: 1fr;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.templates-card {
|
.background-icon {
|
||||||
background-color: var(--background);
|
padding: 10px;
|
||||||
padding: var(--spacing-xl);
|
border-radius: 4px;
|
||||||
border-radius: var(--border-radius-m);
|
display: flex;
|
||||||
border: var(--border-dark);
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.template {
|
||||||
margin-top: var(--spacing-m);
|
height: 60px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--layout-m);
|
||||||
|
grid-template-columns: 5% 1fr 15%;
|
||||||
|
border: 1px solid #494949;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-from-scratch {
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Modal, ModalContent, Button } from "@budibase/bbui"
|
import { Modal, ModalContent, Button } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
let upgradeModal
|
let upgradeModal
|
||||||
|
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
window.open("https://account.budibase.app/portal/install", "_blank")
|
window.open(`${$admin.accountPortalUrl}/portal/install`, "_blank")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -21,12 +22,12 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="M"
|
||||||
{onConfirm}
|
{onConfirm}
|
||||||
title="Upgrade to self-hosted"
|
title="Self-host Budibase"
|
||||||
confirmText="Upgrade"
|
confirmText="Self-host Budibase"
|
||||||
>
|
>
|
||||||
<span
|
<span>
|
||||||
>Upgrade to Budibase self-hosting for free, and get SSO, unlimited apps,
|
Self-host budibase for free to get unlimited apps and more - and it only
|
||||||
and more - and it only takes a few minutes!</span
|
takes a few minutes!
|
||||||
>
|
</span>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -44,6 +44,15 @@ export const OperatorOptions = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NoEmptyFilterStrings = [
|
||||||
|
OperatorOptions.StartsWith.value,
|
||||||
|
OperatorOptions.Like.value,
|
||||||
|
OperatorOptions.Equals.value,
|
||||||
|
OperatorOptions.NotEquals.value,
|
||||||
|
OperatorOptions.Contains.value,
|
||||||
|
OperatorOptions.NotContains.value,
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the valid operator options for a certain data type
|
* Returns the valid operator options for a certain data type
|
||||||
* @param type the data type
|
* @param type the data type
|
||||||
|
|
|
@ -1,3 +1,26 @@
|
||||||
|
import { NoEmptyFilterStrings } from "../constants/lucene"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any fields that contain empty strings that would cause inconsistent
|
||||||
|
* behaviour with how backend tables are filtered (no value means no filter).
|
||||||
|
*/
|
||||||
|
function cleanupQuery(query) {
|
||||||
|
if (!query) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
for (let filterField of NoEmptyFilterStrings) {
|
||||||
|
if (!query[filterField]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let [key, value] of Object.entries(query[filterField])) {
|
||||||
|
if (!value || value === "") {
|
||||||
|
delete query[filterField][key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a lucene JSON query from the filter structure generated in the builder
|
* Builds a lucene JSON query from the filter structure generated in the builder
|
||||||
* @param filter the builder filter structure
|
* @param filter the builder filter structure
|
||||||
|
@ -76,6 +99,8 @@ export const luceneQuery = (docs, query) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
// make query consistent first
|
||||||
|
query = cleanupQuery(query)
|
||||||
|
|
||||||
// Iterates over a set of filters and evaluates a fail function against a doc
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||||
const match = (type, failFn) => doc => {
|
const match = (type, failFn) => doc => {
|
||||||
|
|
|
@ -11,18 +11,38 @@
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
$: user = $auth.user
|
$: user = $auth.user
|
||||||
|
|
||||||
const validateTenantId = async () => {
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
// set the tenant from the url in the cloud
|
|
||||||
const tenantId = window.location.host.split(".")[0]
|
|
||||||
|
|
||||||
if (!tenantId.includes("localhost:")) {
|
const validateTenantId = async () => {
|
||||||
// user doesn't have permission to access this tenant - kick them out
|
const host = window.location.host
|
||||||
if (user?.tenantId !== tenantId) {
|
if (host.includes("localhost:")) {
|
||||||
|
// ignore local dev
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. ['tenant', 'budibase', 'app'] vs ['budibase', 'app']
|
||||||
|
let urlTenantId
|
||||||
|
const hostParts = host.split(".")
|
||||||
|
if (hostParts.length > 2) {
|
||||||
|
urlTenantId = hostParts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user && user.tenantId) {
|
||||||
|
// no tenant in the url - send to account portal to fix this
|
||||||
|
if (!urlTenantId) {
|
||||||
|
window.location.href = $admin.accountPortalUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.tenantId !== urlTenantId) {
|
||||||
|
// user should not be here - play it safe and log them out
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
await auth.setOrganisation(null)
|
await auth.setOrganisation(null)
|
||||||
} else {
|
return
|
||||||
await auth.setOrganisation(tenantId)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// no user - set the org according to the url
|
||||||
|
await auth.setOrganisation(urlTenantId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +50,7 @@
|
||||||
await auth.checkAuth()
|
await auth.checkAuth()
|
||||||
await admin.init()
|
await admin.init()
|
||||||
|
|
||||||
if (cloud && multiTenancyEnabled) {
|
if (useAccountPortal && multiTenancyEnabled) {
|
||||||
await validateTenantId()
|
await validateTenantId()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,31 +58,35 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// We should never see the org or admin user creation screens in the cloud
|
const apiReady = $admin.loaded && $auth.loaded
|
||||||
if (!cloud) {
|
// if tenant is not set go to it
|
||||||
const apiReady = $admin.loaded && $auth.loaded
|
|
||||||
// if tenant is not set go to it
|
|
||||||
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
|
|
||||||
$redirect("./auth/org")
|
|
||||||
}
|
|
||||||
// Force creation of an admin user if one doesn't exist
|
|
||||||
else if (loaded && apiReady && !hasAdminUser) {
|
|
||||||
$redirect("./admin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Redirect to log in at any time if the user isn't authenticated
|
|
||||||
$: {
|
|
||||||
if (
|
if (
|
||||||
|
loaded &&
|
||||||
|
!useAccountPortal &&
|
||||||
|
apiReady &&
|
||||||
|
multiTenancyEnabled &&
|
||||||
|
!tenantSet
|
||||||
|
) {
|
||||||
|
$redirect("./auth/org")
|
||||||
|
}
|
||||||
|
// Force creation of an admin user if one doesn't exist
|
||||||
|
else if (loaded && !useAccountPortal && apiReady && !hasAdminUser) {
|
||||||
|
$redirect("./admin")
|
||||||
|
}
|
||||||
|
// Redirect to log in at any time if the user isn't authenticated
|
||||||
|
else if (
|
||||||
loaded &&
|
loaded &&
|
||||||
(hasAdminUser || cloud) &&
|
(hasAdminUser || cloud) &&
|
||||||
!$auth.user &&
|
!$auth.user &&
|
||||||
!$isActive("./auth") &&
|
!$isActive("./auth") &&
|
||||||
!$isActive("./invite")
|
!$isActive("./invite") &&
|
||||||
|
!$isActive("./admin")
|
||||||
) {
|
) {
|
||||||
const returnUrl = encodeURIComponent(window.location.pathname)
|
const returnUrl = encodeURIComponent(window.location.pathname)
|
||||||
$redirect("./auth?", { returnUrl })
|
$redirect("./auth?", { returnUrl })
|
||||||
} else if ($auth?.user?.forceResetPassword) {
|
}
|
||||||
|
// check if password reset required for user
|
||||||
|
else if ($auth.user?.forceResetPassword) {
|
||||||
$redirect("./auth/reset")
|
$redirect("./auth/reset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
|
||||||
|
import { post } from "builderStore/api"
|
||||||
|
|
||||||
|
let submitting = false
|
||||||
|
|
||||||
|
$: value = { file: null }
|
||||||
|
|
||||||
|
async function importApps() {
|
||||||
|
submitting = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create form data to create app
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("importFile", value.file)
|
||||||
|
|
||||||
|
// Create App
|
||||||
|
const importResp = await post("/api/cloud/import", data, {})
|
||||||
|
const importJson = await importResp.json()
|
||||||
|
if (!importResp.ok) {
|
||||||
|
throw new Error(importJson.message)
|
||||||
|
}
|
||||||
|
// now reload to get to login
|
||||||
|
window.location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error)
|
||||||
|
submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Import apps"
|
||||||
|
confirmText="Import apps"
|
||||||
|
onConfirm={importApps}
|
||||||
|
disabled={!value.file}
|
||||||
|
>
|
||||||
|
<Body
|
||||||
|
>Please upload the file that was exported from your Cloud environment to get
|
||||||
|
started</Body
|
||||||
|
>
|
||||||
|
<Dropzone
|
||||||
|
gallery={false}
|
||||||
|
label="File to import"
|
||||||
|
value={[value.file]}
|
||||||
|
on:change={e => {
|
||||||
|
value.file = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
|
@ -5,8 +5,11 @@
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
$: cloud = $admin.cloud
|
||||||
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($admin?.checklist?.adminUser.checked) {
|
if ($admin?.checklist?.adminUser.checked || useAccountPortal) {
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
} else {
|
} else {
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
|
@ -7,18 +7,22 @@
|
||||||
Input,
|
Input,
|
||||||
Body,
|
Body,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
|
||||||
let adminUser = {}
|
let adminUser = {}
|
||||||
let error
|
let error
|
||||||
|
let modal
|
||||||
|
|
||||||
$: tenantId = $auth.tenantId
|
$: tenantId = $auth.tenantId
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
|
@ -38,6 +42,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal} padding={false} width="600px">
|
||||||
|
<ImportAppsModal />
|
||||||
|
</Modal>
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -66,6 +73,15 @@
|
||||||
>
|
>
|
||||||
Change organisation
|
Change organisation
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
{:else if !cloud}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
modal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import from cloud
|
||||||
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
|
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
|
||||||
import { get } from "builderStore/api"
|
import { get } from "builderStore/api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import { isActive, goto, layout } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte"
|
import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte"
|
||||||
|
@ -34,7 +34,16 @@
|
||||||
const pkg = await res.json()
|
const pkg = await res.json()
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await store.actions.initialise(pkg)
|
try {
|
||||||
|
await store.actions.initialise(pkg)
|
||||||
|
// edge case, lock wasn't known to client when it re-directed, or user went directly
|
||||||
|
} catch (err) {
|
||||||
|
if (!err.ok && err.reason === "locked") {
|
||||||
|
$redirect("../../")
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
await roles.fetch()
|
await roles.fetch()
|
||||||
return pkg
|
return pkg
|
||||||
|
@ -92,7 +101,7 @@
|
||||||
<ActionGroup />
|
<ActionGroup />
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
{#if $admin.cloud}
|
{#if $admin.cloud && $auth.user.account}
|
||||||
<UpgradeModal />
|
<UpgradeModal />
|
||||||
{/if}
|
{/if}
|
||||||
<VersionModal />
|
<VersionModal />
|
||||||
|
|
|
@ -156,6 +156,8 @@
|
||||||
...relateTo,
|
...relateTo,
|
||||||
through: through._id,
|
through: through._id,
|
||||||
fieldName: fromTable.primary[0],
|
fieldName: fromTable.primary[0],
|
||||||
|
throughFrom: relateFrom.throughTo,
|
||||||
|
throughTo: relateFrom.throughFrom,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
|
// the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
|
||||||
|
@ -251,6 +253,22 @@
|
||||||
bind:error={errors.through}
|
bind:error={errors.through}
|
||||||
bind:value={fromRelationship.through}
|
bind:value={fromRelationship.through}
|
||||||
/>
|
/>
|
||||||
|
{#if fromTable && toTable && through}
|
||||||
|
<Select
|
||||||
|
label={`Foreign Key (${fromTable?.name})`}
|
||||||
|
options={Object.keys(through?.schema)}
|
||||||
|
on:change={() => ($touched.fromForeign = true)}
|
||||||
|
bind:error={errors.fromForeign}
|
||||||
|
bind:value={fromRelationship.throughTo}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={`Foreign Key (${toTable?.name})`}
|
||||||
|
options={Object.keys(through?.schema)}
|
||||||
|
on:change={() => ($touched.toForeign = true)}
|
||||||
|
bind:error={errors.toForeign}
|
||||||
|
bind:value={fromRelationship.throughFrom}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{:else if fromRelationship?.relationshipType && toTable}
|
{:else if fromRelationship?.relationshipType && toTable}
|
||||||
<Select
|
<Select
|
||||||
label={`Foreign Key (${toTable?.name})`}
|
label={`Foreign Key (${toTable?.name})`}
|
||||||
|
|
|
@ -159,8 +159,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
filter: brightness(110%);
|
filter: brightness(110%);
|
||||||
}
|
}
|
||||||
.group {
|
|
||||||
}
|
|
||||||
.app {
|
.app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to account portal for authentication in the cloud
|
|
||||||
if (
|
if (
|
||||||
!$auth.user &&
|
!$auth.user &&
|
||||||
$admin.cloud &&
|
$admin.cloud &&
|
||||||
|
!$admin.disableAccountPortal &&
|
||||||
$admin.accountPortalUrl &&
|
$admin.accountPortalUrl &&
|
||||||
!$admin?.checklist?.sso?.checked
|
!$admin?.checklist?.sso?.checked
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
|
Link,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { auth, organisation, oidc, admin } from "stores/portal"
|
import { auth, organisation, oidc, admin } from "stores/portal"
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
try {
|
try {
|
||||||
|
@ -84,7 +86,7 @@
|
||||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{#if multiTenancyEnabled}
|
{#if multiTenancyEnabled && !cloud}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -96,6 +98,16 @@
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if cloud}
|
||||||
|
<Body size="xs" textAlign="center">
|
||||||
|
By using Budibase Cloud
|
||||||
|
<br />
|
||||||
|
you are agreeing to our
|
||||||
|
<Link href="https://budibase.com/eula" target="_blank"
|
||||||
|
>License Agreement</Link
|
||||||
|
>
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
|
|
||||||
async function setOrg() {
|
async function setOrg() {
|
||||||
if (tenantId == null || tenantId === "") {
|
if (tenantId == null || tenantId === "") {
|
||||||
tenantId = "default"
|
tenantId = "default"
|
||||||
|
@ -26,7 +28,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await auth.checkQueryString()
|
await auth.checkQueryString()
|
||||||
if (!multiTenancyEnabled || cloud) {
|
if (!multiTenancyEnabled || useAccountPortal) {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
} else {
|
} else {
|
||||||
admin.unload()
|
admin.unload()
|
||||||
|
|
|
@ -5,11 +5,9 @@
|
||||||
auth.checkQueryString()
|
auth.checkQueryString()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!$auth.user) {
|
if ($auth.user?.builder?.global) {
|
||||||
$redirect(`./auth`)
|
|
||||||
} else if ($auth.user.builder?.global) {
|
|
||||||
$redirect(`./portal`)
|
$redirect(`./portal`)
|
||||||
} else {
|
} else if ($auth.user) {
|
||||||
$redirect(`./apps`)
|
$redirect(`./apps`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,9 @@
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Select,
|
Select,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
|
||||||
Page,
|
Page,
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Search,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
@ -35,8 +34,13 @@
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let searchTerm = ""
|
||||||
|
let cloud = $admin.cloud
|
||||||
|
|
||||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||||
|
$: filteredApps = enrichedApps.filter(app =>
|
||||||
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -45,6 +49,7 @@
|
||||||
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
||||||
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (sortBy === "status") {
|
if (sortBy === "status") {
|
||||||
return enrichedApps.sort((a, b) => {
|
return enrichedApps.sort((a, b) => {
|
||||||
if (a.status === b.status) {
|
if (a.status === b.status) {
|
||||||
|
@ -70,6 +75,15 @@
|
||||||
creatingApp = true
|
creatingApp = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initiateAppsExport = () => {
|
||||||
|
try {
|
||||||
|
download(`/api/cloud/export`)
|
||||||
|
notifications.success("Apps exported successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error exporting apps: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initiateAppImport = () => {
|
const initiateAppImport = () => {
|
||||||
template = { fromFile: true }
|
template = { fromFile: true }
|
||||||
creationModal.show()
|
creationModal.show()
|
||||||
|
@ -190,6 +204,9 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading>Apps</Heading>
|
<Heading>Apps</Heading>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
{#if cloud}
|
||||||
|
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
|
||||||
|
{/if}
|
||||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||||
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
@ -197,6 +214,7 @@
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<Select
|
<Select
|
||||||
|
autoWidth
|
||||||
bind:value={sortBy}
|
bind:value={sortBy}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
options={[
|
options={[
|
||||||
|
@ -205,6 +223,9 @@
|
||||||
{ label: "Sort by status", value: "status" },
|
{ label: "Sort by status", value: "status" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<div class="desktop-search">
|
||||||
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -221,11 +242,14 @@
|
||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mobile-search">
|
||||||
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class:appGrid={layout === "grid"}
|
class:appGrid={layout === "grid"}
|
||||||
class:appTable={layout === "table"}
|
class:appTable={layout === "table"}
|
||||||
>
|
>
|
||||||
{#each enrichedApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={layout === "grid" ? AppCard : AppRow}
|
this={layout === "grid" ? AppCard : AppRow}
|
||||||
{releaseLock}
|
{releaseLock}
|
||||||
|
@ -244,22 +268,7 @@
|
||||||
{#if !enrichedApps.length && !creatingApp && loaded}
|
{#if !enrichedApps.length && !creatingApp && loaded}
|
||||||
<div class="empty-wrapper">
|
<div class="empty-wrapper">
|
||||||
<Modal inline>
|
<Modal inline>
|
||||||
<ModalContent
|
<CreateAppModal {template} />
|
||||||
title="Create your first app"
|
|
||||||
confirmText="Create app"
|
|
||||||
showCancelButton={false}
|
|
||||||
showCloseIcon={false}
|
|
||||||
onConfirm={initiateAppCreation}
|
|
||||||
size="M"
|
|
||||||
>
|
|
||||||
<div slot="footer">
|
|
||||||
<Button on:click={initiateAppImport} secondary>Import app</Button>
|
|
||||||
</div>
|
|
||||||
<Body size="S">
|
|
||||||
The purpose of the Budibase builder is to help you build beautiful,
|
|
||||||
powerful applications quickly and easily.
|
|
||||||
</Body>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -298,10 +307,26 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 560px) {
|
||||||
|
.title {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
width: 190px;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
.filter :global(.spectrum-ActionGroup) {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.mobile-search {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appGrid {
|
.appGrid {
|
||||||
|
@ -342,5 +367,11 @@
|
||||||
.appTable {
|
.appTable {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
.desktop-search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mobile-search {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -52,11 +52,11 @@
|
||||||
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
const res = await users.delete(userId)
|
const res = await users.delete(userId)
|
||||||
if (res.message) {
|
if (res.status === 200) {
|
||||||
notifications.success(`User ${$userFetch?.data?.email} deleted.`)
|
notifications.success(`User ${$userFetch?.data?.email} deleted.`)
|
||||||
$goto("./")
|
$goto("./")
|
||||||
} else {
|
} else {
|
||||||
notifications.error("Failed to delete user.")
|
notifications.error(res?.message ? res.message : "Failed to delete user.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export function createAdminStore() {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
multiTenancy: false,
|
multiTenancy: false,
|
||||||
cloud: false,
|
cloud: false,
|
||||||
|
disableAccountPortal: false,
|
||||||
accountPortalUrl: "",
|
accountPortalUrl: "",
|
||||||
onboardingProgress: 0,
|
onboardingProgress: 0,
|
||||||
checklist: {
|
checklist: {
|
||||||
|
@ -47,12 +48,14 @@ export function createAdminStore() {
|
||||||
async function getEnvironment() {
|
async function getEnvironment() {
|
||||||
let multiTenancyEnabled = false
|
let multiTenancyEnabled = false
|
||||||
let cloud = false
|
let cloud = false
|
||||||
|
let disableAccountPortal = false
|
||||||
let accountPortalUrl = ""
|
let accountPortalUrl = ""
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/system/environment`)
|
const response = await api.get(`/api/system/environment`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
multiTenancyEnabled = json.multiTenancy
|
multiTenancyEnabled = json.multiTenancy
|
||||||
cloud = json.cloud
|
cloud = json.cloud
|
||||||
|
disableAccountPortal = json.disableAccountPortal
|
||||||
accountPortalUrl = json.accountPortalUrl
|
accountPortalUrl = json.accountPortalUrl
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// just let it stay disabled
|
// just let it stay disabled
|
||||||
|
@ -60,6 +63,7 @@ export function createAdminStore() {
|
||||||
admin.update(store => {
|
admin.update(store => {
|
||||||
store.multiTenancy = multiTenancyEnabled
|
store.multiTenancy = multiTenancyEnabled
|
||||||
store.cloud = cloud
|
store.cloud = cloud
|
||||||
|
store.disableAccountPortal = disableAccountPortal
|
||||||
store.accountPortalUrl = accountPortalUrl
|
store.accountPortalUrl = accountPortalUrl
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
|
|
|
@ -55,7 +55,11 @@ export function createUsersStore() {
|
||||||
async function del(id) {
|
async function del(id) {
|
||||||
const response = await api.delete(`/api/global/users/${id}`)
|
const response = await api.delete(`/api/global/users/${id}`)
|
||||||
update(users => users.filter(user => user._id !== id))
|
update(users => users.filter(user => user._id !== id))
|
||||||
return await response.json()
|
const json = await response.json()
|
||||||
|
return {
|
||||||
|
...json,
|
||||||
|
status: response.status,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(data) {
|
async function save(data) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.146-alpha.4",
|
"@budibase/bbui": "^0.9.154-alpha.1",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
"@budibase/string-templates": "^0.9.154-alpha.1",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that an end user client app has been loaded.
|
||||||
|
*/
|
||||||
|
export const pingEndUser = async () => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/analytics/ping`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,3 +9,4 @@ export * from "./routes"
|
||||||
export * from "./queries"
|
export * from "./queries"
|
||||||
export * from "./app"
|
export * from "./app"
|
||||||
export * from "./automations"
|
export * from "./automations"
|
||||||
|
export * from "./analytics"
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
import ErrorSVG from "builder/assets/error.svg"
|
import ErrorSVG from "builder/assets/error.svg"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
|
@ -40,6 +41,8 @@
|
||||||
dataLoaded = true
|
dataLoaded = true
|
||||||
if ($builderStore.inBuilder) {
|
if ($builderStore.inBuilder) {
|
||||||
builderStore.actions.notifyLoaded()
|
builderStore.actions.notifyLoaded()
|
||||||
|
} else {
|
||||||
|
builderStore.actions.pingEndUser()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -104,7 +107,10 @@
|
||||||
<div id="app-root">
|
<div id="app-root">
|
||||||
<CustomThemeWrapper>
|
<CustomThemeWrapper>
|
||||||
{#key $screenStore.activeLayout._id}
|
{#key $screenStore.activeLayout._id}
|
||||||
<Component instance={$screenStore.activeLayout.props} />
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
|
@ -122,6 +128,7 @@
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<SelectionIndicator />
|
<SelectionIndicator />
|
||||||
<HoverIndicator />
|
<HoverIndicator />
|
||||||
|
<DNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</StateBindingsProvider>
|
</StateBindingsProvider>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
|
export let isLayout = false
|
||||||
|
export let isScreen = false
|
||||||
|
|
||||||
// The enriched component settings
|
// The enriched component settings
|
||||||
let enrichedSettings
|
let enrichedSettings
|
||||||
|
@ -49,11 +51,11 @@
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = instance._instanceName
|
$: name = instance._instanceName
|
||||||
$: empty =
|
$: interactive =
|
||||||
!children.length &&
|
$builderStore.inBuilder &&
|
||||||
definition?.hasChildren &&
|
($builderStore.previewType === "layout" || insideScreenslot)
|
||||||
definition?.showEmptyState !== false &&
|
$: empty = interactive && !children.length && definition?.hasChildren
|
||||||
$builderStore.inBuilder
|
$: emptyState = empty && definition?.showEmptyState !== false
|
||||||
$: rawProps = getRawProps(instance)
|
$: rawProps = getRawProps(instance)
|
||||||
$: instanceKey = JSON.stringify(rawProps)
|
$: instanceKey = JSON.stringify(rawProps)
|
||||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||||
|
@ -61,16 +63,16 @@
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
$builderStore.selectedComponentId === instance._id
|
$builderStore.selectedComponentId === instance._id
|
||||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
|
||||||
$: evaluateConditions(enrichedSettings?._conditions)
|
$: evaluateConditions(enrichedSettings?._conditions)
|
||||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||||
|
$: renderKey = `${propsHash}-${emptyState}`
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
id,
|
id,
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: { ...instance._styles, id, empty, interactive },
|
styles: { ...instance._styles, id, empty: emptyState, interactive },
|
||||||
empty,
|
empty: emptyState,
|
||||||
selected,
|
selected,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
@ -169,13 +171,22 @@
|
||||||
conditionalSettings = result.settingUpdates
|
conditionalSettings = result.settingUpdates
|
||||||
visible = nextVisible
|
visible = nextVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag and drop helper tags
|
||||||
|
$: draggable = interactive && !isLayout && !isScreen
|
||||||
|
$: droppable = interactive && !isLayout && !isScreen
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key propsHash}
|
{#key renderKey}
|
||||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||||
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
class={`component ${id}`}
|
class={`component ${id}`}
|
||||||
data-type={interactive ? "component" : ""}
|
class:draggable
|
||||||
|
class:droppable
|
||||||
|
class:empty
|
||||||
|
class:interactive
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
>
|
>
|
||||||
|
@ -184,7 +195,7 @@
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if empty}
|
{:else if emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
|
@ -196,4 +207,10 @@
|
||||||
.component {
|
.component {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
.interactive :global(*:hover) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.draggable :global(*:hover) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
<!-- Ensure to fully remount when screen changes -->
|
<!-- Ensure to fully remount when screen changes -->
|
||||||
{#key screenDefinition?._id}
|
{#key screenDefinition?._id}
|
||||||
<Provider key="url" data={params}>
|
<Provider key="url" data={params}>
|
||||||
<Component instance={screenDefinition} />
|
<Component isScreen instance={screenDefinition} />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -31,4 +31,7 @@
|
||||||
.spectrum-Button--overBackground:hover {
|
.spectrum-Button--overBackground:hover {
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
.spectrum-Button::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.valid-container :global([data-type="component"] > *) {
|
.valid-container :global(.component > *) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.direction-row {
|
.direction-row {
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
||||||
/* The nested selector for data-type is the wrapper around all components */
|
/* The nested selector for data-type is the wrapper around all components */
|
||||||
.direction-row :global(> [data-type="component"] > .size-grow) {
|
.direction-row :global(> .component > .size-grow) {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -353,7 +353,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduce padding */
|
/* Reduce padding */
|
||||||
.mobile .main {
|
.mobile:not(.layout--none) .main {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
<script context="module">
|
||||||
|
export const Sides = {
|
||||||
|
Top: "Top",
|
||||||
|
Right: "Right",
|
||||||
|
Bottom: "Bottom",
|
||||||
|
Left: "Left",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
|
let dragInfo
|
||||||
|
let dropInfo
|
||||||
|
|
||||||
|
const getEdges = (bounds, mousePoint) => {
|
||||||
|
const { width, height, top, left } = bounds
|
||||||
|
return {
|
||||||
|
[Sides.Top]: [mousePoint[0], top],
|
||||||
|
[Sides.Right]: [left + width, mousePoint[1]],
|
||||||
|
[Sides.Bottom]: [mousePoint[0], top + height],
|
||||||
|
[Sides.Left]: [left, mousePoint[1]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePointDelta = (point1, point2) => {
|
||||||
|
const deltaX = Math.abs(point1[0] - point2[0])
|
||||||
|
const deltaY = Math.abs(point1[1] - point2[1])
|
||||||
|
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDOMNodeForComponent = component => {
|
||||||
|
const parent = component.closest(".component")
|
||||||
|
const children = Array.from(parent.childNodes)
|
||||||
|
return children?.find(node => node?.nodeType === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when initially starting a drag on a draggable component
|
||||||
|
const onDragStart = e => {
|
||||||
|
const parent = e.target.closest(".component")
|
||||||
|
if (!parent?.classList.contains("draggable")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
target: parent.dataset.id,
|
||||||
|
parent: parent.dataset.parent,
|
||||||
|
}
|
||||||
|
builderStore.actions.selectComponent(dragInfo.target)
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
|
||||||
|
// Highlight being dragged by setting opacity
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = "0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when drag stops (whether dropped or not)
|
||||||
|
const onDragEnd = e => {
|
||||||
|
// Reset opacity style
|
||||||
|
if (dragInfo) {
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state and styles
|
||||||
|
dragInfo = null
|
||||||
|
dropInfo = null
|
||||||
|
builderStore.actions.setDragging(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when on top of a component
|
||||||
|
const onDragOver = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo || !dropInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
const { droppableInside, bounds } = dropInfo
|
||||||
|
const { top, left, height, width } = bounds
|
||||||
|
const mouseY = e.clientY
|
||||||
|
const mouseX = e.clientX
|
||||||
|
const snapFactor = droppableInside ? 0.33 : 0.5
|
||||||
|
const snapLimitV = Math.min(40, height * snapFactor)
|
||||||
|
const snapLimitH = Math.min(40, width * snapFactor)
|
||||||
|
|
||||||
|
// Determine all sies we are within snap range of
|
||||||
|
let sides = []
|
||||||
|
if (mouseY <= top + snapLimitV) {
|
||||||
|
sides.push(Sides.Top)
|
||||||
|
} else if (mouseY >= top + height - snapLimitV) {
|
||||||
|
sides.push(Sides.Bottom)
|
||||||
|
}
|
||||||
|
if (mouseX < left + snapLimitH) {
|
||||||
|
sides.push(Sides.Left)
|
||||||
|
} else if (mouseX > left + width - snapLimitH) {
|
||||||
|
sides.push(Sides.Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When no edges match, drop inside if possible
|
||||||
|
if (!sides.length) {
|
||||||
|
dropInfo.mode = droppableInside ? "inside" : null
|
||||||
|
dropInfo.side = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When one edge matches, use that edge
|
||||||
|
if (sides.length === 1) {
|
||||||
|
dropInfo.side = sides[0]
|
||||||
|
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 2 edges match, work out which is closer
|
||||||
|
const mousePoint = [mouseX, mouseY]
|
||||||
|
const edges = getEdges(bounds, mousePoint)
|
||||||
|
const edge1 = edges[sides[0]]
|
||||||
|
const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||||
|
const edge2 = edges[sides[1]]
|
||||||
|
const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||||
|
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
||||||
|
dropInfo.side = edge
|
||||||
|
if ([Sides.Top, Sides.Left].includes(edge)) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when entering a potential drop target
|
||||||
|
const onDragEnter = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = e.target.closest(".component")
|
||||||
|
if (
|
||||||
|
element &&
|
||||||
|
element.classList.contains("droppable") &&
|
||||||
|
element.dataset.id !== dragInfo.target
|
||||||
|
) {
|
||||||
|
// Do nothing if this is the same target
|
||||||
|
if (element.dataset.id === dropInfo?.target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the dragging flag is always set.
|
||||||
|
// There's a bit of a race condition between the app reinitialisation
|
||||||
|
// after selecting the DND component and setting this the first time
|
||||||
|
if (!get(builderStore).isDragging) {
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store target ID
|
||||||
|
const target = element.dataset.id
|
||||||
|
|
||||||
|
// Precompute and store some info to avoid recalculating everything in
|
||||||
|
// dragOver
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
const bounds = child.getBoundingClientRect()
|
||||||
|
dropInfo = {
|
||||||
|
target,
|
||||||
|
name: element.dataset.name,
|
||||||
|
droppableInside: element.classList.contains("empty"),
|
||||||
|
bounds,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropInfo = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when leaving a potential drop target.
|
||||||
|
// Since we don't style our targets, we don't need to unset anything.
|
||||||
|
const onDragLeave = () => {}
|
||||||
|
|
||||||
|
// Callback when dropping a drag on top of some component
|
||||||
|
const onDrop = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (dropInfo?.mode) {
|
||||||
|
builderStore.actions.moveComponent(
|
||||||
|
dragInfo.target,
|
||||||
|
dropInfo.target,
|
||||||
|
dropInfo.mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
|
document.addEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
document.addEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.addEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.addEventListener("drop", onDrop, false)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
|
document.removeEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
document.removeEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.removeEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.removeEventListener("drop", onDrop, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IndicatorSet
|
||||||
|
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="930"
|
||||||
|
transition
|
||||||
|
prefix="Inside"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DNDPositionIndicator
|
||||||
|
{dropInfo}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="940"
|
||||||
|
transition
|
||||||
|
/>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script>
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
import { Sides } from "./DNDHandler.svelte"
|
||||||
|
|
||||||
|
export let dropInfo
|
||||||
|
export let zIndex
|
||||||
|
export let color
|
||||||
|
export let transition
|
||||||
|
|
||||||
|
$: dimensions = getDimensions(dropInfo)
|
||||||
|
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
|
||||||
|
$: text = `${prefix} ${dropInfo?.name}`
|
||||||
|
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
|
||||||
|
|
||||||
|
const getDimensions = info => {
|
||||||
|
const { bounds, side } = info ?? {}
|
||||||
|
if (!bounds || !side) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get preview offset
|
||||||
|
const root = document.getElementById("clip-root")
|
||||||
|
const rootBounds = root.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Subtract preview offset from bounds
|
||||||
|
let { left, top, width, height } = bounds
|
||||||
|
left -= rootBounds.left
|
||||||
|
top -= rootBounds.top
|
||||||
|
|
||||||
|
// Determine position
|
||||||
|
if (side === Sides.Top || side === Sides.Bottom) {
|
||||||
|
return {
|
||||||
|
top: side === Sides.Top ? top - 4 : top + height,
|
||||||
|
left: left - 2,
|
||||||
|
width: width + 4,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
top: top - 2,
|
||||||
|
left: side === Sides.Left ? left - 4 : left + width,
|
||||||
|
width: 0,
|
||||||
|
height: height + 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key renderKey}
|
||||||
|
{#if dimensions && dropInfo?.mode !== "inside"}
|
||||||
|
<Indicator
|
||||||
|
left={Math.round(dimensions.left)}
|
||||||
|
top={Math.round(dimensions.top)}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
{text}
|
||||||
|
{zIndex}
|
||||||
|
{color}
|
||||||
|
{transition}
|
||||||
|
alignRight={dropInfo?.side === Sides.Right}
|
||||||
|
line
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
|
@ -7,7 +7,7 @@
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest("[data-type='component']")
|
const element = e.target.closest(".interactive.component")
|
||||||
const newId = element?.dataset?.id
|
const newId = element?.dataset?.id
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
{componentId}
|
componentId={$builderStore.isDragging ? null : componentId}
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
transition
|
transition
|
||||||
{zIndex}
|
{zIndex}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
export let color
|
export let color
|
||||||
export let zIndex
|
export let zIndex
|
||||||
export let transition = false
|
export let transition = false
|
||||||
|
export let line = false
|
||||||
|
export let alignRight = false
|
||||||
|
|
||||||
|
$: flipped = top < 20
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -18,11 +22,12 @@
|
||||||
}}
|
}}
|
||||||
out:fade={{ duration: transition ? 130 : 0 }}
|
out:fade={{ duration: transition ? 130 : 0 }}
|
||||||
class="indicator"
|
class="indicator"
|
||||||
class:flipped={top < 20}
|
class:flipped
|
||||||
|
class:line
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||||
>
|
>
|
||||||
{#if text}
|
{#if text}
|
||||||
<div class="text" class:flipped={top < 20}>
|
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -30,6 +35,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.indicator {
|
.indicator {
|
||||||
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--zIndex);
|
z-index: var(--zIndex);
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
|
@ -42,6 +48,9 @@
|
||||||
.indicator.flipped {
|
.indicator.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.indicator.line {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
.text {
|
.text {
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -61,9 +70,18 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.text.line {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.text.flipped {
|
.text.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
top: -2px;
|
top: -2px;
|
||||||
}
|
}
|
||||||
|
.text.right {
|
||||||
|
right: -2px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let color
|
export let color
|
||||||
export let transition
|
export let transition
|
||||||
export let zIndex
|
export let zIndex
|
||||||
|
export let prefix = null
|
||||||
|
|
||||||
let indicators = []
|
let indicators = []
|
||||||
let interval
|
let interval
|
||||||
|
@ -51,6 +52,9 @@
|
||||||
const parents = document.getElementsByClassName(componentId)
|
const parents = document.getElementsByClassName(componentId)
|
||||||
if (parents.length) {
|
if (parents.length) {
|
||||||
text = parents[0].dataset.name
|
text = parents[0].dataset.name
|
||||||
|
if (prefix) {
|
||||||
|
text = `${prefix} ${text}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch reads to minimize reflow
|
// Batch reads to minimize reflow
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
let measured = false
|
let measured = false
|
||||||
|
|
||||||
$: definition = $builderStore.selectedComponentDefinition
|
$: definition = $builderStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar
|
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import Manifest from "manifest.json"
|
import Manifest from "manifest.json"
|
||||||
import { findComponentById, findComponentPathById } from "../utils/components"
|
import { findComponentById, findComponentPathById } from "../utils/components"
|
||||||
|
import { pingEndUser } from "../api"
|
||||||
|
|
||||||
const dispatchEvent = (type, data = {}) => {
|
const dispatchEvent = (type, data = {}) => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
|
@ -23,6 +24,7 @@ const createBuilderStore = () => {
|
||||||
theme: null,
|
theme: null,
|
||||||
customTheme: null,
|
customTheme: null,
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
|
isDragging: false,
|
||||||
}
|
}
|
||||||
const writableStore = writable(initialState)
|
const writableStore = writable(initialState)
|
||||||
const derivedStore = derived(writableStore, $state => {
|
const derivedStore = derived(writableStore, $state => {
|
||||||
|
@ -63,14 +65,28 @@ const createBuilderStore = () => {
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
|
pingEndUser: () => {
|
||||||
|
pingEndUser()
|
||||||
|
},
|
||||||
setSelectedPath: path => {
|
setSelectedPath: path => {
|
||||||
console.log("set to ")
|
|
||||||
console.log(path)
|
|
||||||
writableStore.update(state => {
|
writableStore.update(state => {
|
||||||
state.selectedPath = path
|
state.selectedPath = path
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
moveComponent: (componentId, destinationComponentId, mode) => {
|
||||||
|
dispatchEvent("move-component", {
|
||||||
|
componentId,
|
||||||
|
destinationComponentId,
|
||||||
|
mode,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setDragging: dragging => {
|
||||||
|
writableStore.update(state => {
|
||||||
|
state.isDragging = dragging
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...writableStore,
|
...writableStore,
|
||||||
|
|
|
@ -23,10 +23,14 @@ export const styleable = (node, styles = {}) => {
|
||||||
let applyHoverStyles
|
let applyHoverStyles
|
||||||
let selectComponent
|
let selectComponent
|
||||||
|
|
||||||
|
// Allow dragging if required
|
||||||
|
const parent = node.closest(".component")
|
||||||
|
if (parent && parent.classList.contains("draggable")) {
|
||||||
|
node.setAttribute("draggable", true)
|
||||||
|
}
|
||||||
|
|
||||||
// Creates event listeners and applies initial styles
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = (newStyles = {}) => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
// Use empty state styles as base styles if required, but let them, get
|
|
||||||
// overridden by any user specified styles
|
|
||||||
let baseStyles = {}
|
let baseStyles = {}
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
||||||
|
@ -45,7 +49,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Applies a style string to a DOM node
|
// Applies a style string to a DOM node
|
||||||
const applyStyles = styleString => {
|
const applyStyles = styleString => {
|
||||||
node.style = styleString
|
node.style = styleString
|
||||||
node.dataset.componentId = componentId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the "normal" style definition
|
// Applies the "normal" style definition
|
||||||
|
|
|
@ -29,9 +29,9 @@
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^0.9.139":
|
"@budibase/bbui@^0.9.139":
|
||||||
version "0.9.145"
|
version "0.9.142"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.145.tgz#e65425e927e9488847aaf8209ff3eb0cf00c219c"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.142.tgz#7edbda7967c9e5dfc96e5be5231656e5aab8d0e3"
|
||||||
integrity sha512-vHSi+J52U24YSJPd1cfH9ePN92kCGLxKw4naYDjavYGd568GbRPJWzerzyqhm4VQtWn8FFi47jbzAsfAhiFfLA==
|
integrity sha512-m2YlqqH87T4RwqD/oGhH6twHIgvFv4oUMEhKpkgLsbxjXVLVD0OOF7WqjpDnSa4khVQaixjdkI/Jiw2qhBUSaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||||
"@spectrum-css/actionbutton" "^1.0.1"
|
"@spectrum-css/actionbutton" "^1.0.1"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.146-alpha.4",
|
"version": "0.9.154-alpha.1",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -27,7 +27,9 @@
|
||||||
"multi:enable": "node scripts/multiTenancy.js enable",
|
"multi:enable": "node scripts/multiTenancy.js enable",
|
||||||
"multi:disable": "node scripts/multiTenancy.js disable",
|
"multi:disable": "node scripts/multiTenancy.js disable",
|
||||||
"selfhost:enable": "node scripts/selfhost.js enable",
|
"selfhost:enable": "node scripts/selfhost.js enable",
|
||||||
"selfhost:disable": "node scripts/selfhost.js disable"
|
"selfhost:disable": "node scripts/selfhost.js disable",
|
||||||
|
"localdomain:enable": "node scripts/localdomain.js enable",
|
||||||
|
"localdomain:disable": "node scripts/localdomain.js disable"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
@ -64,9 +66,9 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.146-alpha.4",
|
"@budibase/auth": "^0.9.154-alpha.1",
|
||||||
"@budibase/client": "^0.9.146-alpha.4",
|
"@budibase/client": "^0.9.154-alpha.1",
|
||||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
"@budibase/string-templates": "^0.9.154-alpha.1",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@koa/router": "8.0.0",
|
"@koa/router": "8.0.0",
|
||||||
"@sendgrid/mail": "7.1.1",
|
"@sendgrid/mail": "7.1.1",
|
||||||
|
@ -96,6 +98,7 @@
|
||||||
"koa-session": "5.12.0",
|
"koa-session": "5.12.0",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"memorystream": "^0.3.1",
|
||||||
"mongodb": "3.6.3",
|
"mongodb": "3.6.3",
|
||||||
"mssql": "6.2.3",
|
"mssql": "6.2.3",
|
||||||
"mysql": "2.18.1",
|
"mysql": "2.18.1",
|
||||||
|
@ -103,6 +106,7 @@
|
||||||
"open": "7.3.0",
|
"open": "7.3.0",
|
||||||
"pg": "8.5.1",
|
"pg": "8.5.1",
|
||||||
"pino-pretty": "4.0.0",
|
"pino-pretty": "4.0.0",
|
||||||
|
"posthog-node": "^1.1.4",
|
||||||
"pouchdb": "7.2.1",
|
"pouchdb": "7.2.1",
|
||||||
"pouchdb-adapter-memory": "^7.2.1",
|
"pouchdb-adapter-memory": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "1.0.2",
|
"pouchdb-all-dbs": "1.0.2",
|
||||||
|
|
|
@ -37,7 +37,7 @@ async function init() {
|
||||||
const envFileJson = {
|
const envFileJson = {
|
||||||
PORT: 4001,
|
PORT: 4001,
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
COUCH_DB_URL: "http://@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
REDIS_URL: "localhost:6379",
|
REDIS_URL: "localhost:6379",
|
||||||
WORKER_URL: "http://localhost:4002",
|
WORKER_URL: "http://localhost:4002",
|
||||||
INTERNAL_API_KEY: "budibase",
|
INTERNAL_API_KEY: "budibase",
|
||||||
|
@ -48,6 +48,7 @@ async function init() {
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
COUCH_DB_PASSWORD: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
|
DISABLE_ACCOUNT_PORTAL: "",
|
||||||
MULTI_TENANCY: "",
|
MULTI_TENANCY: "",
|
||||||
}
|
}
|
||||||
let envFile = ""
|
let envFile = ""
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: postgres
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_PASSWORD: root
|
||||||
|
POSTGRES_DB: main
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
#- pg_data:/var/lib/postgresql/data/
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
container_name: pgadmin-pg
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: root@root.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: root
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
||||||
|
|
||||||
|
#volumes:
|
||||||
|
# pg_data:
|
|
@ -0,0 +1,41 @@
|
||||||
|
SELECT 'CREATE DATABASE main'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
|
||||||
|
|
||||||
|
CREATE TABLE categories
|
||||||
|
(
|
||||||
|
name text COLLATE pg_catalog."default",
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||||
|
CONSTRAINT categories_pkey PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE customers
|
||||||
|
(
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||||
|
name text COLLATE pg_catalog."default",
|
||||||
|
email text COLLATE pg_catalog."default",
|
||||||
|
age integer,
|
||||||
|
"dateOfBirth" date,
|
||||||
|
CONSTRAINT customers_pkey PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE customer_category
|
||||||
|
(
|
||||||
|
customer_id integer,
|
||||||
|
category_id integer,
|
||||||
|
notes text COLLATE pg_catalog."default",
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||||
|
CONSTRAINT "Category" FOREIGN KEY (category_id)
|
||||||
|
REFERENCES public.categories (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
NOT VALID,
|
||||||
|
CONSTRAINT "Customer" FOREIGN KEY (customer_id)
|
||||||
|
REFERENCES public.customers (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
NOT VALID
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO customers (name, email, age) VALUES ('Mike', 'mike@mike.com', 30);
|
||||||
|
INSERT INTO categories (name) VALUES ('Books');
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
docker-compose down
|
||||||
|
docker volume prune -f
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const updateDotEnv = require("update-dotenv")
|
||||||
|
|
||||||
|
const arg = process.argv.slice(2)[0]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing multi tenancy sub domains locally.
|
||||||
|
*
|
||||||
|
* Relies on an entry in /etc/hosts e.g:
|
||||||
|
*
|
||||||
|
* 127.0.0.1 local.com
|
||||||
|
*
|
||||||
|
* and an entry for each tenant you wish to test locally e.g:
|
||||||
|
*
|
||||||
|
* 127.0.0.1 t1.local.com
|
||||||
|
* 127.0.0.1 t2.local.com
|
||||||
|
*/
|
||||||
|
updateDotEnv({
|
||||||
|
ACCOUNT_PORTAL_URL:
|
||||||
|
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
|
||||||
|
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
||||||
|
}).then(() => console.log("Updated worker!"))
|
|
@ -1,7 +1,32 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
|
const PostHog = require("posthog-node")
|
||||||
|
|
||||||
exports.isEnabled = async function (ctx) {
|
let posthogClient
|
||||||
|
|
||||||
|
if (env.POSTHOG_TOKEN && env.ENABLE_ANALYTICS && !env.SELF_HOSTED) {
|
||||||
|
posthogClient = new PostHog(env.POSTHOG_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isEnabled = async ctx => {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true",
|
enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.endUserPing = async ctx => {
|
||||||
|
if (!posthogClient) {
|
||||||
|
ctx.body = {
|
||||||
|
ping: false,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
posthogClient.capture("budibase:end_user_ping", {
|
||||||
|
userId: ctx.user && ctx.user._id,
|
||||||
|
appId: ctx.appId,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
ping: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const {
|
||||||
getDeployedApps,
|
getDeployedApps,
|
||||||
removeAppFromUserRoles,
|
removeAppFromUserRoles,
|
||||||
} = require("../../utilities/workerRequests")
|
} = require("../../utilities/workerRequests")
|
||||||
const { clientLibraryPath } = require("../../utilities")
|
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
||||||
const { getAllLocks } = require("../../utilities/redis")
|
const { getAllLocks } = require("../../utilities/redis")
|
||||||
const {
|
const {
|
||||||
updateClientLibrary,
|
updateClientLibrary,
|
||||||
|
@ -82,7 +82,7 @@ async function getAppUrlIfNotInUse(ctx) {
|
||||||
if (!env.SELF_HOSTED) {
|
if (!env.SELF_HOSTED) {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
const deployedApps = await getDeployedApps(ctx)
|
const deployedApps = await getDeployedApps()
|
||||||
if (
|
if (
|
||||||
url &&
|
url &&
|
||||||
deployedApps[url] != null &&
|
deployedApps[url] != null &&
|
||||||
|
@ -114,8 +114,13 @@ async function createInstance(template) {
|
||||||
|
|
||||||
// replicate the template data to the instance DB
|
// replicate the template data to the instance DB
|
||||||
// this is currently very hard to test, downloading and importing template files
|
// this is currently very hard to test, downloading and importing template files
|
||||||
/* istanbul ignore next */
|
if (template && template.templateString) {
|
||||||
if (template && template.useTemplate === "true") {
|
const { ok } = await db.load(stringToReadStream(template.templateString))
|
||||||
|
if (!ok) {
|
||||||
|
throw "Error loading database dump from memory."
|
||||||
|
}
|
||||||
|
} else if (template && template.useTemplate === "true") {
|
||||||
|
/* istanbul ignore next */
|
||||||
const { ok } = await db.load(await getTemplateStream(template))
|
const { ok } = await db.load(await getTemplateStream(template))
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
throw "Error loading database dump from template."
|
throw "Error loading database dump from template."
|
||||||
|
@ -191,10 +196,11 @@ exports.fetchAppPackage = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function (ctx) {
|
exports.create = async function (ctx) {
|
||||||
const { useTemplate, templateKey } = ctx.request.body
|
const { useTemplate, templateKey, templateString } = ctx.request.body
|
||||||
const instanceConfig = {
|
const instanceConfig = {
|
||||||
useTemplate,
|
useTemplate,
|
||||||
key: templateKey,
|
key: templateKey,
|
||||||
|
templateString,
|
||||||
}
|
}
|
||||||
if (ctx.request.files && ctx.request.files.templateFile) {
|
if (ctx.request.files && ctx.request.files.templateFile) {
|
||||||
instanceConfig.file = ctx.request.files.templateFile
|
instanceConfig.file = ctx.request.files.templateFile
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
const env = require("../../environment")
|
||||||
|
const { getAllApps } = require("@budibase/auth/db")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const {
|
||||||
|
exportDB,
|
||||||
|
sendTempFile,
|
||||||
|
readFileSync,
|
||||||
|
} = require("../../utilities/fileSystem")
|
||||||
|
const { stringToReadStream } = require("../../utilities")
|
||||||
|
const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
|
const { create } = require("./application")
|
||||||
|
const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils")
|
||||||
|
|
||||||
|
async function createApp(appName, appImport) {
|
||||||
|
const ctx = {
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
templateString: appImport,
|
||||||
|
name: appName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return create(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.exportApps = async ctx => {
|
||||||
|
if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
|
||||||
|
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
|
||||||
|
}
|
||||||
|
const apps = await getAllApps(CouchDB, { all: true })
|
||||||
|
const globalDBString = await exportDB(getGlobalDBName())
|
||||||
|
let allDBs = {
|
||||||
|
global: globalDBString,
|
||||||
|
}
|
||||||
|
for (let app of apps) {
|
||||||
|
// only export the dev apps as they will be the latest, the user can republish the apps
|
||||||
|
// in their self hosted environment
|
||||||
|
if (isDevAppID(app._id)) {
|
||||||
|
allDBs[app.name] = await exportDB(app._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filename = `cloud-export-${new Date().getTime()}.txt`
|
||||||
|
ctx.attachment(filename)
|
||||||
|
ctx.body = sendTempFile(JSON.stringify(allDBs))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllDocType(db, docType) {
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getDocParams(docType, null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return response.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importApps = async ctx => {
|
||||||
|
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
||||||
|
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
||||||
|
}
|
||||||
|
const apps = await getAllApps(CouchDB, { all: true })
|
||||||
|
if (
|
||||||
|
apps.length !== 0 ||
|
||||||
|
!ctx.request.files ||
|
||||||
|
!ctx.request.files.importFile
|
||||||
|
) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
"Import file is required and environment must be fresh to import apps."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const importFile = ctx.request.files.importFile
|
||||||
|
const importString = readFileSync(importFile.path)
|
||||||
|
const dbs = JSON.parse(importString)
|
||||||
|
const globalDbImport = dbs.global
|
||||||
|
// remove from the list of apps
|
||||||
|
delete dbs.global
|
||||||
|
const globalDb = getGlobalDB()
|
||||||
|
// load the global db first
|
||||||
|
await globalDb.load(stringToReadStream(globalDbImport))
|
||||||
|
for (let [appName, appImport] of Object.entries(dbs)) {
|
||||||
|
await createApp(appName, appImport)
|
||||||
|
}
|
||||||
|
// once apps are created clean up the global db
|
||||||
|
let users = await getAllDocType(globalDb, DocumentTypes.USER)
|
||||||
|
for (let user of users) {
|
||||||
|
delete user.tenantId
|
||||||
|
}
|
||||||
|
await globalDb.bulkDocs(users)
|
||||||
|
ctx.body = {
|
||||||
|
message: "Apps successfully imported.",
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ async function storeDeploymentHistory(deployment) {
|
||||||
|
|
||||||
async function initDeployedApp(prodAppId) {
|
async function initDeployedApp(prodAppId) {
|
||||||
const db = new CouchDB(prodAppId)
|
const db = new CouchDB(prodAppId)
|
||||||
|
console.log("Reading automation docs")
|
||||||
const automations = (
|
const automations = (
|
||||||
await db.allDocs(
|
await db.allDocs(
|
||||||
getAutomationParams(null, {
|
getAutomationParams(null, {
|
||||||
|
@ -71,12 +72,17 @@ async function initDeployedApp(prodAppId) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).rows.map(row => row.doc)
|
).rows.map(row => row.doc)
|
||||||
|
console.log("You have " + automations.length + " automations")
|
||||||
const promises = []
|
const promises = []
|
||||||
|
console.log("Disabling prod crons..")
|
||||||
await disableAllCrons(prodAppId)
|
await disableAllCrons(prodAppId)
|
||||||
|
console.log("Prod Cron triggers disabled..")
|
||||||
|
console.log("Enabling cron triggers for deployed app..")
|
||||||
for (let automation of automations) {
|
for (let automation of automations) {
|
||||||
promises.push(enableCronTrigger(prodAppId, automation))
|
promises.push(enableCronTrigger(prodAppId, automation))
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
console.log("Enabled cron triggers for deployed app..")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deployApp(deployment) {
|
async function deployApp(deployment) {
|
||||||
|
@ -88,13 +94,18 @@ async function deployApp(deployment) {
|
||||||
target: productionAppId,
|
target: productionAppId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("Replication object created")
|
||||||
|
|
||||||
await replication.replicate()
|
await replication.replicate()
|
||||||
|
console.log("replication complete.. replacing app meta doc")
|
||||||
const db = new CouchDB(productionAppId)
|
const db = new CouchDB(productionAppId)
|
||||||
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
||||||
appDoc.appId = productionAppId
|
appDoc.appId = productionAppId
|
||||||
appDoc.instance._id = productionAppId
|
appDoc.instance._id = productionAppId
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
|
console.log("New app doc written successfully.")
|
||||||
|
|
||||||
|
console.log("Setting up live repl between dev and prod")
|
||||||
// Set up live sync between the live and dev instances
|
// Set up live sync between the live and dev instances
|
||||||
const liveReplication = new Replication({
|
const liveReplication = new Replication({
|
||||||
source: productionAppId,
|
source: productionAppId,
|
||||||
|
@ -105,8 +116,11 @@ async function deployApp(deployment) {
|
||||||
return doc._id !== DocumentTypes.APP_METADATA
|
return doc._id !== DocumentTypes.APP_METADATA
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
console.log("Set up live repl between dev and prod")
|
||||||
|
|
||||||
|
console.log("Initialising deployed app")
|
||||||
await initDeployedApp(productionAppId)
|
await initDeployedApp(productionAppId)
|
||||||
|
console.log("Init complete, setting deployment to successful")
|
||||||
deployment.setStatus(DeploymentStatus.SUCCESS)
|
deployment.setStatus(DeploymentStatus.SUCCESS)
|
||||||
await storeDeploymentHistory(deployment)
|
await storeDeploymentHistory(deployment)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -153,9 +167,13 @@ exports.deploymentProgress = async function (ctx) {
|
||||||
|
|
||||||
exports.deployApp = async function (ctx) {
|
exports.deployApp = async function (ctx) {
|
||||||
let deployment = new Deployment(ctx.appId)
|
let deployment = new Deployment(ctx.appId)
|
||||||
|
console.log("Deployment object created")
|
||||||
deployment.setStatus(DeploymentStatus.PENDING)
|
deployment.setStatus(DeploymentStatus.PENDING)
|
||||||
|
console.log("Deployment object set to pending")
|
||||||
deployment = await storeDeploymentHistory(deployment)
|
deployment = await storeDeploymentHistory(deployment)
|
||||||
|
console.log("Stored deployment history")
|
||||||
|
|
||||||
|
console.log("Deploying app...")
|
||||||
await deployApp(deployment)
|
await deployApp(deployment)
|
||||||
|
|
||||||
ctx.body = deployment
|
ctx.body = deployment
|
||||||
|
|
|
@ -18,5 +18,5 @@ exports.fetchUrls = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getDeployedApps = async ctx => {
|
exports.getDeployedApps = async ctx => {
|
||||||
ctx.body = await getDeployedApps(ctx)
|
ctx.body = await getDeployedApps()
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,7 +178,12 @@ module External {
|
||||||
manyRelationships: ManyRelationship[] = []
|
manyRelationships: ManyRelationship[] = []
|
||||||
for (let [key, field] of Object.entries(table.schema)) {
|
for (let [key, field] of Object.entries(table.schema)) {
|
||||||
// if set already, or not set just skip it
|
// if set already, or not set just skip it
|
||||||
if (!row[key] || newRow[key] || field.autocolumn) {
|
if ((!row[key] && row[key] !== "") || newRow[key] || field.autocolumn) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if its an empty string then it means return the column to null (if possible)
|
||||||
|
if (row[key] === "") {
|
||||||
|
newRow[key] = null
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// parse floats/numbers
|
// parse floats/numbers
|
||||||
|
@ -205,9 +210,13 @@ module External {
|
||||||
} else {
|
} else {
|
||||||
// we're not inserting a doc, will be a bunch of update calls
|
// we're not inserting a doc, will be a bunch of update calls
|
||||||
const isUpdate = !field.through
|
const isUpdate = !field.through
|
||||||
const thisKey: string = isUpdate ? "id" : linkTablePrimary
|
const thisKey: string = isUpdate
|
||||||
|
? "id"
|
||||||
|
: field.throughTo || linkTablePrimary
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const otherKey: string = isUpdate ? field.fieldName : tablePrimary
|
const otherKey: string = isUpdate
|
||||||
|
? field.fieldName
|
||||||
|
: field.throughFrom || tablePrimary
|
||||||
row[key].map((relationship: any) => {
|
row[key].map((relationship: any) => {
|
||||||
// we don't really support composite keys for relationships, this is why [0] is used
|
// we don't really support composite keys for relationships, this is why [0] is used
|
||||||
manyRelationships.push({
|
manyRelationships.push({
|
||||||
|
@ -328,12 +337,11 @@ module External {
|
||||||
if (!table.primary || !linkTable.primary) {
|
if (!table.primary || !linkTable.primary) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const definition = {
|
const definition: any = {
|
||||||
// if no foreign key specified then use the name of the field in other table
|
// if no foreign key specified then use the name of the field in other table
|
||||||
from: field.foreignKey || table.primary[0],
|
from: field.foreignKey || table.primary[0],
|
||||||
to: field.fieldName,
|
to: field.fieldName,
|
||||||
tableName: linkTableName,
|
tableName: linkTableName,
|
||||||
through: undefined,
|
|
||||||
// need to specify where to put this back into
|
// need to specify where to put this back into
|
||||||
column: fieldName,
|
column: fieldName,
|
||||||
}
|
}
|
||||||
|
@ -343,8 +351,10 @@ module External {
|
||||||
)
|
)
|
||||||
definition.through = throughTableName
|
definition.through = throughTableName
|
||||||
// don't support composite keys for relationships
|
// don't support composite keys for relationships
|
||||||
definition.from = table.primary[0]
|
definition.from = field.throughFrom || table.primary[0]
|
||||||
definition.to = linkTable.primary[0]
|
definition.to = field.throughTo || linkTable.primary[0]
|
||||||
|
definition.fromPrimary = table.primary[0]
|
||||||
|
definition.toPrimary = linkTable.primary[0]
|
||||||
}
|
}
|
||||||
relationships.push(definition)
|
relationships.push(definition)
|
||||||
}
|
}
|
||||||
|
@ -369,7 +379,8 @@ module External {
|
||||||
}
|
}
|
||||||
const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
const tableId = isMany ? field.through : field.tableId
|
const tableId = isMany ? field.through : field.tableId
|
||||||
const fieldName = isMany ? primaryKey : field.fieldName
|
const manyKey = field.throughFrom || primaryKey
|
||||||
|
const fieldName = isMany ? manyKey : field.fieldName
|
||||||
const response = await makeExternalQuery(this.appId, {
|
const response = await makeExternalQuery(this.appId, {
|
||||||
endpoint: getEndpoint(tableId, DataSourceOperation.READ),
|
endpoint: getEndpoint(tableId, DataSourceOperation.READ),
|
||||||
filters: {
|
filters: {
|
||||||
|
|
|
@ -2,6 +2,7 @@ const {
|
||||||
DataSourceOperation,
|
DataSourceOperation,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
FieldTypes,
|
FieldTypes,
|
||||||
|
NoEmptyFilterStrings,
|
||||||
} = require("../../../constants")
|
} = require("../../../constants")
|
||||||
const {
|
const {
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
|
@ -11,6 +12,19 @@ const ExternalRequest = require("./ExternalRequest")
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
|
|
||||||
async function handleRequest(appId, operation, tableId, opts = {}) {
|
async function handleRequest(appId, operation, tableId, opts = {}) {
|
||||||
|
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
|
||||||
|
if (opts && opts.filters) {
|
||||||
|
for (let filterField of NoEmptyFilterStrings) {
|
||||||
|
if (!opts.filters[filterField]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let [key, value] of Object.entries(opts.filters[filterField])) {
|
||||||
|
if (!value || value === "") {
|
||||||
|
delete opts.filters[filterField][key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ExternalRequest(appId, operation, tableId, opts.datasource).run(
|
return new ExternalRequest(appId, operation, tableId, opts.datasource).run(
|
||||||
opts
|
opts
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@ const {
|
||||||
generateRowID,
|
generateRowID,
|
||||||
DocumentTypes,
|
DocumentTypes,
|
||||||
InternalTables,
|
InternalTables,
|
||||||
generateMemoryViewID,
|
|
||||||
} = require("../../../db/utils")
|
} = require("../../../db/utils")
|
||||||
const userController = require("../user")
|
const userController = require("../user")
|
||||||
const {
|
const {
|
||||||
|
@ -20,7 +19,12 @@ const { fullSearch, paginatedSearch } = require("./internalSearch")
|
||||||
const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
|
const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
|
||||||
const inMemoryViews = require("../../../db/inMemoryView")
|
const inMemoryViews = require("../../../db/inMemoryView")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
const { migrateToInMemoryView } = require("../view/utils")
|
const {
|
||||||
|
migrateToInMemoryView,
|
||||||
|
migrateToDesignView,
|
||||||
|
getFromDesignDoc,
|
||||||
|
getFromMemoryDoc,
|
||||||
|
} = require("../view/utils")
|
||||||
|
|
||||||
const CALCULATION_TYPES = {
|
const CALCULATION_TYPES = {
|
||||||
SUM: "sum",
|
SUM: "sum",
|
||||||
|
@ -74,33 +78,24 @@ async function getRawTableData(ctx, db, tableId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getView(db, viewName) {
|
async function getView(db, viewName) {
|
||||||
let viewInfo
|
let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc
|
||||||
async function getFromDesignDoc() {
|
let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc
|
||||||
const designDoc = await db.get("_design/database")
|
let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView
|
||||||
viewInfo = designDoc.views[viewName]
|
let viewInfo,
|
||||||
return viewInfo
|
migrate = false
|
||||||
}
|
try {
|
||||||
let migrate = false
|
viewInfo = await mainGetter(db, viewName)
|
||||||
if (env.SELF_HOSTED) {
|
} catch (err) {
|
||||||
viewInfo = await getFromDesignDoc()
|
// check if it can be retrieved from design doc (needs migrated)
|
||||||
} else {
|
if (err.status !== 404) {
|
||||||
try {
|
viewInfo = null
|
||||||
viewInfo = await db.get(generateMemoryViewID(viewName))
|
} else {
|
||||||
if (viewInfo) {
|
viewInfo = await secondaryGetter(db, viewName)
|
||||||
viewInfo = viewInfo.view
|
migrate = !!viewInfo
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// check if it can be retrieved from design doc (needs migrated)
|
|
||||||
if (err.status !== 404) {
|
|
||||||
viewInfo = null
|
|
||||||
} else {
|
|
||||||
viewInfo = await getFromDesignDoc()
|
|
||||||
migrate = !!viewInfo
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
await migrateToInMemoryView(db, viewName)
|
await migration(db, viewName)
|
||||||
}
|
}
|
||||||
if (!viewInfo) {
|
if (!viewInfo) {
|
||||||
throw "View does not exist."
|
throw "View does not exist."
|
||||||
|
@ -408,16 +403,32 @@ exports.fetchEnrichedRow = async ctx => {
|
||||||
rowId,
|
rowId,
|
||||||
})
|
})
|
||||||
// look up the actual rows based on the ids
|
// look up the actual rows based on the ids
|
||||||
const response = await db.allDocs({
|
let response = (
|
||||||
include_docs: true,
|
await db.allDocs({
|
||||||
keys: linkVals.map(linkVal => linkVal.id),
|
include_docs: true,
|
||||||
})
|
keys: linkVals.map(linkVal => linkVal.id),
|
||||||
// need to include the IDs in these rows for any links they may have
|
})
|
||||||
let linkedRows = await outputProcessing(
|
).rows.map(row => row.doc)
|
||||||
ctx,
|
// group responses by table
|
||||||
table,
|
let groups = {},
|
||||||
response.rows.map(row => row.doc)
|
tables = {}
|
||||||
)
|
for (let row of response) {
|
||||||
|
const linkedTableId = row.tableId
|
||||||
|
if (groups[linkedTableId] == null) {
|
||||||
|
groups[linkedTableId] = [row]
|
||||||
|
tables[linkedTableId] = await db.get(linkedTableId)
|
||||||
|
} else {
|
||||||
|
groups[linkedTableId].push(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let linkedRows = []
|
||||||
|
for (let [tableId, rows] of Object.entries(groups)) {
|
||||||
|
// need to include the IDs in these rows for any links they may have
|
||||||
|
linkedRows = linkedRows.concat(
|
||||||
|
await outputProcessing(ctx, tables[tableId], rows)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// insert the link rows in the correct place throughout the main row
|
// insert the link rows in the correct place throughout the main row
|
||||||
for (let fieldName of Object.keys(table.schema)) {
|
for (let fieldName of Object.keys(table.schema)) {
|
||||||
let field = table.schema[fieldName]
|
let field = table.schema[fieldName]
|
||||||
|
|
|
@ -40,7 +40,7 @@ async function prepareUpload({ s3Key, bucket, metadata, file }) {
|
||||||
async function checkForSelfHostedURL(ctx) {
|
async function checkForSelfHostedURL(ctx) {
|
||||||
// the "appId" component of the URL may actually be a specific self hosted URL
|
// the "appId" component of the URL may actually be a specific self hosted URL
|
||||||
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
|
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
|
||||||
const apps = await getDeployedApps(ctx)
|
const apps = await getDeployedApps()
|
||||||
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
|
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
|
||||||
return apps[possibleAppUrl].appId
|
return apps[possibleAppUrl].appId
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -107,3 +107,30 @@ exports.migrateToInMemoryView = async (db, viewName) => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
await exports.saveView(db, null, viewName, view)
|
await exports.saveView(db, null, viewName, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.migrateToDesignView = async (db, viewName) => {
|
||||||
|
let view = await db.get(generateMemoryViewID(viewName))
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
designDoc.views[viewName] = view.view
|
||||||
|
await db.put(designDoc)
|
||||||
|
await db.remove(view._id, view._rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFromDesignDoc = async (db, viewName) => {
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
let view = designDoc.views[viewName]
|
||||||
|
if (view == null) {
|
||||||
|
throw { status: 404, message: "Unable to get view" }
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFromMemoryDoc = async (db, viewName) => {
|
||||||
|
let view = await db.get(generateMemoryViewID(viewName))
|
||||||
|
if (view) {
|
||||||
|
view = view.view
|
||||||
|
} else {
|
||||||
|
throw { status: 404, message: "Unable to get view" }
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue