Merge remote-tracking branch 'origin/develop' into feature/new-screen-addition-ui

This commit is contained in:
Dean 2022-04-25 09:06:54 +01:00
commit 85928e72b2
227 changed files with 6601 additions and 8336 deletions

View File

@ -93,6 +93,8 @@ then `cd ` into your local copy.
#### 3. Install and Build #### 3. Install and Build
| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash)
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
##### Quick method ##### Quick method

View File

@ -7,6 +7,15 @@ assignees: ''
--- ---
**Hosting**
<!-- Delete as appropriate -->
- Self
- Method: <method> <!-- One of: k8s, docker single image, docker compose, digital ocean: -->
- Budibase Version: <version> <!-- e.g. 1.0.105 -->
- App Version: <version> <!-- Indicate app version if bug is related to an application -->
- Cloud
- Tenant ID: <tenantId> <!-- shown in URL as <tenantID>.budibase.app -->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@ -12,6 +12,11 @@ on:
- master - master
- develop - develop
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -27,6 +32,10 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn bootstrap
- run: yarn lint - run: yarn lint

View File

@ -19,6 +19,7 @@ env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
jobs: jobs:
release: release:
@ -29,6 +30,10 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro
run: yarn install:pro develop
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn bootstrap
- run: yarn lint - run: yarn lint
@ -46,9 +51,9 @@ jobs:
env: env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: | run: |
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default # setup the username and email.
git config user.name "Budibase Staging Release Bot" git config --global user.name "Budibase Staging Release Bot"
git config user.email "<>" git config --global user.email "<>"
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release:develop yarn release:develop

View File

@ -20,6 +20,7 @@ env:
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
jobs: jobs:
release: release:
@ -30,6 +31,10 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro
run: yarn install:pro master
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn bootstrap
- run: yarn lint - run: yarn lint

View File

@ -28,6 +28,7 @@ jobs:
- name: Cypress run - name: Cypress run
id: cypress id: cypress
continue-on-error: true
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
install: false install: false

View File

@ -11,7 +11,7 @@ sources:
- https://github.com/Budibase/budibase - https://github.com/Budibase/budibase
- https://budibase.com - https://budibase.com
type: application type: application
version: 0.2.8 version: 0.2.9
appVersion: 1.0.48 appVersion: 1.0.48
dependencies: dependencies:
- name: couchdb - name: couchdb

View File

@ -98,10 +98,6 @@ spec:
value: http://worker-service:{{ .Values.services.worker.port }} value: http://worker-service:{{ .Values.services.worker.port }}
- name: PLATFORM_URL - name: PLATFORM_URL
value: {{ .Values.globals.platformUrl | quote }} value: {{ .Values.globals.platformUrl | quote }}
- name: USE_QUOTAS
value: {{ .Values.globals.useQuotas | quote }}
- name: EXCLUDE_QUOTAS_TENANTS
value: {{ .Values.globals.excludeQuotasTenants | quote }}
- name: ACCOUNT_PORTAL_URL - name: ACCOUNT_PORTAL_URL
value: {{ .Values.globals.accountPortalUrl | quote }} value: {{ .Values.globals.accountPortalUrl | quote }}
- name: ACCOUNT_PORTAL_API_KEY - name: ACCOUNT_PORTAL_API_KEY
@ -114,12 +110,23 @@ spec:
value: {{ .Values.globals.google.clientId | quote }} value: {{ .Values.globals.google.clientId | quote }}
- name: GOOGLE_CLIENT_SECRET - name: GOOGLE_CLIENT_SECRET
value: {{ .Values.globals.google.secret | quote }} value: {{ .Values.globals.google.secret | quote }}
- name: AUTOMATION_MAX_ITERATIONS
value: {{ .Values.globals.automationMaxIterations | quote }}
image: budibase/apps:{{ .Values.globals.appVersion }} image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: bbapps name: bbapps
ports: ports:
- containerPort: {{ .Values.services.apps.port }} - containerPort: {{ .Values.services.apps.port }}
resources: {} resources: {}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
status: {} status: {}

View File

@ -39,5 +39,13 @@ spec:
imagePullPolicy: Always imagePullPolicy: Always
name: couchdb-backup name: couchdb-backup
resources: {} resources: {}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
status: {} status: {}
{{- end }} {{- end }}

View File

@ -60,6 +60,14 @@ spec:
volumeMounts: volumeMounts:
- mountPath: /data - mountPath: /data
name: minio-data name: minio-data
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:

View File

@ -32,6 +32,14 @@ spec:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
resources: {} resources: {}
volumeMounts: volumeMounts:
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:

View File

@ -39,6 +39,14 @@ spec:
volumeMounts: volumeMounts:
- mountPath: /data - mountPath: /data
name: redis-data name: redis-data
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:

View File

@ -121,6 +121,14 @@ spec:
ports: ports:
- containerPort: {{ .Values.services.worker.port }} - containerPort: {{ .Values.services.worker.port }}
resources: {} resources: {}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
status: {} status: {}

View File

@ -93,8 +93,6 @@ globals:
logLevel: info logLevel: info
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
useQuotas: "0"
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
accountPortalUrl: "" accountPortalUrl: ""
accountPortalApiKey: "" accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
@ -103,6 +101,7 @@ globals:
google: google:
clientId: "" clientId: ""
secret: "" secret: ""
automationMaxIterations: "500"
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
@ -230,6 +229,8 @@ couchdb:
## Optional tolerations ## Optional tolerations
tolerations: [] tolerations: []
affinity: {}
service: service:
# annotations: # annotations:
enabled: true enabled: true

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.105-alpha.8", "version": "1.0.105-alpha.35",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -21,18 +21,17 @@
}, },
"scripts": { "scripts": {
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", "setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna link && lerna bootstrap", "bootstrap": "lerna link && lerna bootstrap && ./scripts/link-dependencies.sh",
"build": "lerna run build", "build": "lerna run build",
"publishdev": "lerna run publishdev", "release": "lerna publish patch --yes --force-publish && yarn release:pro",
"publishnpm": "yarn build && lerna publish --force-publish", "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
"release": "lerna publish patch --yes --force-publish", "release:pro": "bash scripts/pro/release.sh",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop", "release:pro:develop": "bash scripts/pro/release.sh develop",
"restore": "yarn run clean && yarn run bootstrap && yarn run build", "restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore", "nuke:packages": "yarn run restore",
"nuke:docker": "lerna run --parallel dev:stack:nuke", "nuke:docker": "lerna run --parallel dev:stack:nuke",
"clean": "lerna clean", "clean": "lerna clean",
"kill-port": "kill-port 4001",
"kill-builder": "kill-port 3000", "kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002", "kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server", "kill-all": "yarn run kill-builder && yarn run kill-server",
@ -74,6 +73,7 @@
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable", "mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
"mode:account": "yarn mode:cloud && yarn env:account:enable", "mode:account": "yarn mode:cloud && yarn env:account:enable",
"security:audit": "node scripts/audit.js", "security:audit": "node scripts/audit.js",
"postinstall": "husky install" "postinstall": "husky install",
"install:pro": "bash scripts/pro/install.sh"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.105-alpha.8", "version": "1.0.105-alpha.35",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -13,9 +13,11 @@ exports.Cookies = {
exports.Headers = { exports.Headers = {
API_KEY: "x-budibase-api-key", API_KEY: "x-budibase-api-key",
LICENSE_KEY: "x-budibase-license-key",
API_VER: "x-budibase-api-version", API_VER: "x-budibase-api-version",
APP_ID: "x-budibase-app-id", APP_ID: "x-budibase-app-id",
TYPE: "x-budibase-type", TYPE: "x-budibase-type",
PREVIEW_ROLE: "x-budibase-role",
TENANT_ID: "x-budibase-tenant-id", TENANT_ID: "x-budibase-tenant-id",
TOKEN: "x-budibase-token", TOKEN: "x-budibase-token",
CSRF_TOKEN: "x-csrf-token", CSRF_TOKEN: "x-csrf-token",

View File

@ -23,6 +23,7 @@ exports.StaticDatabases = {
docs: { docs: {
apiKeys: "apikeys", apiKeys: "apikeys",
usageQuota: "usage_quota", usageQuota: "usage_quota",
licenseInfo: "license_info",
}, },
}, },
// contains information about tenancy and so on // contains information about tenancy and so on

View File

@ -27,6 +27,7 @@ const UNICODE_MAX = "\ufff0"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
BY_API_KEY: "by_api_key", BY_API_KEY: "by_api_key",
USER_BY_BUILDERS: "by_builders",
} }
exports.StaticDatabases = StaticDatabases exports.StaticDatabases = StaticDatabases
@ -429,34 +430,9 @@ async function getScopedConfig(db, params) {
return configDoc && configDoc.config ? configDoc.config : configDoc return configDoc && configDoc.config ? configDoc.config : configDoc
} }
function generateNewUsageQuotaDoc() {
return {
_id: StaticDatabases.GLOBAL.docs.usageQuota,
quotaReset: Date.now() + 2592000000,
usageQuota: {
automationRuns: 0,
rows: 0,
storage: 0,
apps: 0,
users: 0,
views: 0,
emails: 0,
},
usageLimits: {
automationRuns: 1000,
rows: 4000,
apps: 4,
storage: 1000,
users: 10,
emails: 50,
},
}
}
exports.Replication = Replication exports.Replication = Replication
exports.getScopedConfig = getScopedConfig exports.getScopedConfig = getScopedConfig
exports.generateConfigID = generateConfigID exports.generateConfigID = generateConfigID
exports.getConfigParams = getConfigParams exports.getConfigParams = getConfigParams
exports.getScopedFullConfig = getScopedFullConfig exports.getScopedFullConfig = getScopedFullConfig
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
exports.generateDevInfoID = generateDevInfoID exports.generateDevInfoID = generateDevInfoID

View File

@ -56,10 +56,34 @@ exports.createApiKeyView = async () => {
await db.put(designDoc) await db.put(designDoc)
} }
exports.createUserBuildersView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc.builder && doc.builder.global === true) {
emit(doc._id, doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewNames.USER_BY_BUILDERS]: view,
}
await db.put(designDoc)
}
exports.queryGlobalView = async (viewName, params, db = null) => { exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = { const CreateFuncByName = {
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView, [ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
[ViewNames.BY_API_KEY]: exports.createApiKeyView, [ViewNames.BY_API_KEY]: exports.createApiKeyView,
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
} }
// can pass DB in if working with something specific // can pass DB in if working with something specific
if (!db) { if (!db) {

View File

@ -28,6 +28,7 @@ module.exports = {
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,
PLATFORM_URL: process.env.PLATFORM_URL, PLATFORM_URL: process.env.PLATFORM_URL,
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
isTest, isTest,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

@ -0,0 +1,11 @@
class BudibaseError extends Error {
constructor(message, type, code) {
super(message)
this.type = type
this.code = code
}
}
module.exports = {
BudibaseError,
}

View File

@ -0,0 +1,41 @@
const licensing = require("./licensing")
const codes = {
...licensing.codes,
}
const types = {
...licensing.types,
}
const context = {
...licensing.context,
}
const getPublicError = err => {
let error
if (err.code || err.type) {
// add generic error information
error = {
code: err.code,
type: err.type,
}
if (err.code && context[err.code]) {
error = {
...error,
// get any additional context from this error
...context[err.code](err),
}
}
}
return error
}
module.exports = {
codes,
types,
UsageLimitError: licensing.UsageLimitError,
getPublicError,
}

View File

@ -0,0 +1,32 @@
const { BudibaseError } = require("./base")
const types = {
LICENSE_ERROR: "license_error",
}
const codes = {
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
}
const context = {
[codes.USAGE_LIMIT_EXCEEDED]: err => {
return {
limitName: err.limitName,
}
},
}
class UsageLimitError extends BudibaseError {
constructor(message, limitName) {
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
this.limitName = limitName
this.status = 400
}
}
module.exports = {
types,
codes,
context,
UsageLimitError,
}

View File

@ -0,0 +1,52 @@
const env = require("../environment")
const tenancy = require("../tenancy")
/**
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
* The env var is formatted as:
* tenant1:feature1:feature2,tenant2:feature1
*/
const getFeatureFlags = () => {
if (!env.TENANT_FEATURE_FLAGS) {
return
}
const tenantFeatureFlags = {}
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => {
const [tenantId, ...features] = tenantToFeatures.split(":")
features.forEach(feature => {
if (!tenantFeatureFlags[tenantId]) {
tenantFeatureFlags[tenantId] = []
}
tenantFeatureFlags[tenantId].push(feature)
})
})
return tenantFeatureFlags
}
const TENANT_FEATURE_FLAGS = getFeatureFlags()
exports.isEnabled = featureFlag => {
const tenantId = tenancy.getTenantId()
return (
TENANT_FEATURE_FLAGS &&
TENANT_FEATURE_FLAGS[tenantId] &&
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
)
}
exports.getTenantFeatureFlags = tenantId => {
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
return TENANT_FEATURE_FLAGS[tenantId]
}
return []
}
exports.FeatureFlag = {
LICENSING: "LICENSING",
}

View File

@ -15,4 +15,9 @@ module.exports = {
auth: require("../auth"), auth: require("../auth"),
constants: require("../constants"), constants: require("../constants"),
migrations: require("../migrations"), migrations: require("../migrations"),
errors: require("./errors"),
env: require("./environment"),
accounts: require("./cloud/accounts"),
tenancy: require("./tenancy"),
featureFlags: require("./featureFlags"),
} }

View File

@ -2,7 +2,8 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
async function authenticate(accessToken, refreshToken, profile, done) { const buildVerifyFn = async saveUserFn => {
return (accessToken, refreshToken, profile, done) => {
const thirdPartyUser = { const thirdPartyUser = {
provider: profile.provider, // should always be 'google' provider: profile.provider, // should always be 'google'
providerType: "google", providerType: "google",
@ -18,8 +19,10 @@ async function authenticate(accessToken, refreshToken, profile, done) {
return authenticateThirdParty( return authenticateThirdParty(
thirdPartyUser, thirdPartyUser,
true, // require local accounts to exist true, // require local accounts to exist
done done,
saveUserFn
) )
}
} }
/** /**
@ -27,11 +30,7 @@ async function authenticate(accessToken, refreshToken, profile, done) {
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport Google Strategy * @returns Dynamically configured Passport Google Strategy
*/ */
exports.strategyFactory = async function ( exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
config,
callbackUrl,
verify = authenticate
) {
try { try {
const { clientID, clientSecret } = config const { clientID, clientSecret } = config
@ -41,6 +40,7 @@ exports.strategyFactory = async function (
) )
} }
const verify = buildVerifyFn(saveUserFn)
return new GoogleStrategy( return new GoogleStrategy(
{ {
clientID: config.clientID, clientID: config.clientID,
@ -58,4 +58,4 @@ exports.strategyFactory = async function (
} }
} }
// expose for testing // expose for testing
exports.authenticate = authenticate exports.buildVerifyFn = buildVerifyFn

View File

@ -2,7 +2,8 @@ const fetch = require("node-fetch")
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
/** const buildVerifyFn = saveUserFn => {
/**
* @param {*} issuer The identity provider base URL * @param {*} issuer The identity provider base URL
* @param {*} sub The user ID * @param {*} sub The user ID
* @param {*} profile The user profile information. Created by passport from the /userinfo response * @param {*} profile The user profile information. Created by passport from the /userinfo response
@ -13,7 +14,7 @@ const { authenticateThirdParty } = require("./third-party-common")
* @param {*} params The response body from requesting an access_token * @param {*} params The response body from requesting an access_token
* @param {*} done The passport callback: err, user, info * @param {*} done The passport callback: err, user, info
*/ */
async function authenticate( return async (
issuer, issuer,
sub, sub,
profile, profile,
@ -23,7 +24,7 @@ async function authenticate(
idToken, idToken,
params, params,
done done
) { ) => {
const thirdPartyUser = { const thirdPartyUser = {
// store the issuer info to enable sync in future // store the issuer info to enable sync in future
provider: issuer, provider: issuer,
@ -40,8 +41,10 @@ async function authenticate(
return authenticateThirdParty( return authenticateThirdParty(
thirdPartyUser, thirdPartyUser,
false, // don't require local accounts to exist false, // don't require local accounts to exist
done done,
saveUserFn
) )
}
} }
/** /**
@ -86,7 +89,7 @@ function validEmail(value) {
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport OIDC Strategy * @returns Dynamically configured Passport OIDC Strategy
*/ */
exports.strategyFactory = async function (config, callbackUrl) { exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
try { try {
const { clientID, clientSecret, configUrl } = config const { clientID, clientSecret, configUrl } = config
@ -106,6 +109,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
const body = await response.json() const body = await response.json()
const verify = buildVerifyFn(saveUserFn)
return new OIDCStrategy( return new OIDCStrategy(
{ {
issuer: body.issuer, issuer: body.issuer,
@ -116,7 +120,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
clientSecret: clientSecret, clientSecret: clientSecret,
callbackURL: callbackUrl, callbackURL: callbackUrl,
}, },
authenticate verify
) )
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -125,4 +129,4 @@ exports.strategyFactory = async function (config, callbackUrl) {
} }
// expose for testing // expose for testing
exports.authenticate = authenticate exports.buildVerifyFn = buildVerifyFn

View File

@ -58,8 +58,10 @@ describe("google", () => {
it("delegates authentication to third party common", async () => { it("delegates authentication to third party common", async () => {
const google = require("../google") const google = require("../google")
const mockSaveUserFn = jest.fn()
const authenticate = await google.buildVerifyFn(mockSaveUserFn)
await google.authenticate( await authenticate(
data.accessToken, data.accessToken,
data.refreshToken, data.refreshToken,
profile, profile,
@ -69,7 +71,8 @@ describe("google", () => {
expect(authenticateThirdParty).toHaveBeenCalledWith( expect(authenticateThirdParty).toHaveBeenCalledWith(
user, user,
true, true,
mockDone) mockDone,
mockSaveUserFn)
}) })
}) })
}) })

View File

@ -83,8 +83,10 @@ describe("oidc", () => {
async function doAuthenticate() { async function doAuthenticate() {
const oidc = require("../oidc") const oidc = require("../oidc")
const mockSaveUserFn = jest.fn()
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
await oidc.authenticate( await authenticate(
issuer, issuer,
sub, sub,
profile, profile,

View File

@ -1,7 +1,6 @@
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")
@ -16,8 +15,11 @@ exports.authenticateThirdParty = async function (
thirdPartyUser, thirdPartyUser,
requireLocalAccount = true, requireLocalAccount = true,
done, done,
saveUserFn = saveUser saveUserFn
) { ) {
if (!saveUserFn) {
throw new Error("Save user function must be provided")
}
if (!thirdPartyUser.provider) { if (!thirdPartyUser.provider) {
return authError(done, "third party user provider required") return authError(done, "third party user provider required")
} }

View File

@ -17,6 +17,7 @@ exports.Databases = {
FLAGS: "flags", FLAGS: "flags",
APP_METADATA: "appMetadata", APP_METADATA: "appMetadata",
QUERY_VARS: "queryVars", QUERY_VARS: "queryVars",
LICENSES: "license",
} }
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR

View File

@ -176,6 +176,13 @@ exports.getGlobalUserByEmail = async email => {
}) })
} }
exports.getBuildersCount = async () => {
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
include_docs: false,
})
return builders ? builders.length : 0
}
exports.saveUser = async ( exports.saveUser = async (
user, user,
tenantId, tenantId,
@ -289,4 +296,5 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
userId, userId,
sessions.map(({ sessionId }) => sessionId) sessions.map(({ sessionId }) => sessionId)
) )
await userCache.invalidateUser(userId)
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.105-alpha.8", "version": "1.0.105-alpha.35",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.105-alpha.8", "@budibase/string-templates": "^1.0.105-alpha.35",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -80,8 +80,4 @@
.active svg { .active svg {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);
} }
.spectrum-ActionButton-label {
padding-bottom: 2px;
}
</style> </style>

View File

@ -36,6 +36,10 @@
padding-left: var(--spacing-l); padding-left: var(--spacing-l);
padding-right: var(--spacing-l); padding-right: var(--spacing-l);
} }
.paddingX-XL {
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
}
.paddingY-S { .paddingY-S {
padding-top: var(--spacing-s); padding-top: var(--spacing-s);
padding-bottom: var(--spacing-s); padding-bottom: var(--spacing-s);
@ -48,6 +52,10 @@
padding-top: var(--spacing-l); padding-top: var(--spacing-l);
padding-bottom: var(--spacing-l); padding-bottom: var(--spacing-l);
} }
.paddingY-XL {
padding-top: var(--spacing-xl);
padding-bottom: var(--spacing-xl);
}
.gap-XXS { .gap-XXS {
grid-gap: var(--spacing-xs); grid-gap: var(--spacing-xs);
} }

View File

@ -1,8 +1,9 @@
<script> <script>
export let wide = false export let wide = false
export let maxWidth = "80ch"
</script> </script>
<div class:wide> <div style="--max-width: {maxWidth}" class:wide>
<slot /> <slot />
</div> </div>
@ -12,7 +13,7 @@
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
max-width: 80ch; max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
padding: calc(var(--spacing-xl) * 2); padding: calc(var(--spacing-xl) * 2);
min-height: calc(100% - var(--spacing-xl) * 4); min-height: calc(100% - var(--spacing-xl) * 4);

View File

@ -16,11 +16,11 @@
easing: easing, easing: easing,
}) })
$: if (value) $progress = value $: if (value || value === 0) $progress = value
</script> </script>
<div <div
class:spectrum-ProgressBar--indeterminate={!value} class:spectrum-ProgressBar--indeterminate={!value && value !== 0}
class:spectrum-ProgressBar--sideLabel={sideLabel} class:spectrum-ProgressBar--sideLabel={sideLabel}
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}" class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
value={$progress} value={$progress}
@ -28,7 +28,7 @@
aria-valuenow={$progress} aria-valuenow={$progress}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
style={width ? `width: ${width}px;` : ""} style={width ? `width: ${width};` : ""}
> >
{#if $$slots} {#if $$slots}
<div <div
@ -37,7 +37,7 @@
<slot /> <slot />
</div> </div>
{/if} {/if}
{#if value} {#if value || value === 0}
<div <div
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}" class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
> >
@ -47,7 +47,7 @@
<div class="spectrum-ProgressBar-track"> <div class="spectrum-ProgressBar-track">
<div <div
class="spectrum-ProgressBar-fill" class="spectrum-ProgressBar-fill"
style={value ? `width: ${$progress}%` : ""} style={value || value === 0 ? `width: ${$progress}%` : ""}
/> />
</div> </div>
<div class="spectrum-ProgressBar-label" hidden="" /> <div class="spectrum-ProgressBar-label" hidden="" />

View File

@ -1,42 +1,21 @@
<script> <script>
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import { copyToClipboard } from "../helpers"
import { notifications } from "../Stores/notifications" import { notifications } from "../Stores/notifications"
export let value export let value
const onClick = e => { const onClick = async e => {
e.stopPropagation() e.stopPropagation()
copyToClipboard(value) try {
} await copyToClipboard(value)
const copyToClipboard = value => {
return new Promise(res => {
if (navigator.clipboard && window.isSecureContext) {
// Try using the clipboard API first
navigator.clipboard.writeText(value).then(res)
} else {
// Fall back to the textarea hack
let textArea = document.createElement("textarea")
textArea.value = value
textArea.style.position = "fixed"
textArea.style.left = "-9999px"
textArea.style.top = "-9999px"
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
document.execCommand("copy")
textArea.remove()
res()
}
})
.then(() => {
notifications.success("Copied to clipboard") notifications.success("Copied to clipboard")
}) } catch (error) {
.catch(() => {
notifications.error( notifications.error(
"Failed to copy to clipboard. Check the dev console for the value." "Failed to copy to clipboard. Check the dev console for the value."
) )
console.warn("Failed to copy the value", value) console.warn("Failed to copy the value", value)
}) }
} }
</script> </script>

View File

@ -36,6 +36,7 @@
export let disableSorting = false export let disableSorting = false
export let autoSortColumns = true export let autoSortColumns = true
export let compact = false export let compact = false
export let customPlaceholder = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -387,13 +388,24 @@
</div> </div>
{/each} {/each}
{:else} {:else}
<div class="placeholder" class:placeholder--no-fields={!fields?.length}> <div
class="placeholder"
class:placeholder--custom={customPlaceholder}
class:placeholder--no-fields={!fields?.length}
>
{#if customPlaceholder}
<slot name="placeholder" />
{:else}
<div class="placeholder-content"> <div class="placeholder-content">
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false"> <svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" /> <use xlink:href="#spectrum-icon-18-Table" />
</svg> </svg>
<div>No rows found</div> <div>No rows found</div>
</div> </div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@ -458,6 +470,13 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
user-select: none; user-select: none;
border-top: var(--table-border);
}
.spectrum-Table-headCell:first-of-type {
border-left: var(--table-border);
}
.spectrum-Table-headCell:last-of-type {
border-right: var(--table-border);
} }
.spectrum-Table-headCell--alignCenter { .spectrum-Table-headCell--alignCenter {
justify-content: center; justify-content: center;
@ -576,16 +595,19 @@
border-top: none; border-top: none;
grid-column: 1 / -1; grid-column: 1 / -1;
background-color: var(--table-bg); background-color: var(--table-bg);
padding: 40px;
} }
.placeholder--no-fields { .placeholder--no-fields {
border-top: var(--table-border); border-top: var(--table-border);
} }
.placeholder--custom {
justify-content: flex-start;
}
.wrapper--quiet .placeholder { .wrapper--quiet .placeholder {
border-left: none; border-left: none;
border-right: none; border-right: none;
} }
.placeholder-content { .placeholder-content {
padding: 40px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View File

@ -108,7 +108,7 @@
padding-left: var(--spacing-xl); padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl); padding-right: var(--spacing-xl);
position: relative; position: relative;
border-bottom: var(--border-light); border-bottom: 1px solid var(--spectrum-global-color-gray-300);
} }
.spectrum-Tabs-content { .spectrum-Tabs-content {
margin-top: var(--spectrum-global-dimension-static-size-150); margin-top: var(--spectrum-global-dimension-static-size-150);

View File

@ -5,12 +5,14 @@
export let serif = false export let serif = false
export let weight = null export let weight = null
export let textAlign = null export let textAlign = null
export let color = null
</script> </script>
<p <p
style={` style={`
${weight ? `font-weight:${weight};` : ""} ${weight ? `font-weight:${weight};` : ""}
${textAlign ? `text-align:${textAlign};` : ""} ${textAlign ? `text-align:${textAlign};` : ""}
${color ? `color:${color};` : ""}
`} `}
class="spectrum-Body spectrum-Body--size{size}" class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif} class:spectrum-Body--serif={serif}

View File

@ -5,12 +5,13 @@
export let size = "M" export let size = "M"
export let textAlign export let textAlign
export let noPadding = false export let noPadding = false
export let weight = "default" // light, heavy, default
</script> </script>
<h1 <h1
style={textAlign ? `text-align:${textAlign}` : ``} style={textAlign ? `text-align:${textAlign}` : ``}
class:noPadding class:noPadding
class="spectrum-Heading spectrum-Heading--size{size}" class="spectrum-Heading spectrum-Heading--size{size} spectrum-Heading--{weight}"
> >
<slot /> <slot />
</h1> </h1>

View File

@ -106,3 +106,29 @@ export const deepSet = (obj, key, value) => {
export const cloneDeep = obj => { export const cloneDeep = obj => {
return JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj))
} }
/**
* Copies a value to the clipboard
* @param value the value to copy
*/
export const copyToClipboard = value => {
return new Promise(res => {
if (navigator.clipboard && window.isSecureContext) {
// Try using the clipboard API first
navigator.clipboard.writeText(value).then(res)
} else {
// Fall back to the textarea hack
let textArea = document.createElement("textarea")
textArea.value = value
textArea.style.position = "fixed"
textArea.style.left = "-9999px"
textArea.style.top = "-9999px"
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
document.execCommand("copy")
textArea.remove()
res()
}
})
}

File diff suppressed because it is too large Load Diff

View File

@ -25,9 +25,13 @@ filterTests(['smoke', 'all'], () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
if (Cypress.env("TEST_ENV")) { cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true}) cy.get(".spectrum-Button").contains("Templates").click({force: true})
} }
})
cy.get(".template-category-filters").should("exist") cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist") cy.get(".template-categories").should("exist")

View File

@ -20,7 +20,6 @@ filterTests(['smoke', 'all'], () => {
}) })
// Setup trigger // Setup trigger
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click() cy.get(".spectrum-Picker-label").click()
cy.wait(500) cy.wait(500)
cy.contains("dog").click() cy.contains("dog").click()
@ -32,7 +31,6 @@ filterTests(['smoke', 'all'], () => {
cy.contains("Create Row").trigger('mouseover').click().click() cy.contains("Create Row").trigger('mouseover').click().click()
cy.get(".spectrum-Button--cta").click() cy.get(".spectrum-Button--cta").click()
}) })
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").eq(1).click() cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("dog").click() cy.contains("dog").click()
cy.get(".spectrum-Textfield-input") cy.get(".spectrum-Textfield-input")

View File

@ -55,13 +55,14 @@ filterTests(["smoke", "all"], () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
// No Pagination in CI - Test env only for the next two tests // No Pagination in CI - Test env only for the next two tests
it("Adds 15 rows and checks pagination", () => { xit("Adds 15 rows and checks pagination", () => {
// 10 rows per page, 15 rows should create 2 pages within table // 10 rows per page, 15 rows should create 2 pages within table
const totalRows = 16 const totalRows = 16
for (let i = 1; i < totalRows; i++) { for (let i = 1; i < totalRows; i++) {
cy.addRow([i]) cy.addRow([i])
} }
cy.wait(1000) cy.reload()
cy.wait(2000)
cy.get(".spectrum-Pagination").within(() => { cy.get(".spectrum-Pagination").within(() => {
cy.get(".spectrum-ActionButton").eq(1).click() cy.get(".spectrum-ActionButton").eq(1).click()
}) })
@ -70,13 +71,13 @@ filterTests(["smoke", "all"], () => {
}) })
}) })
it("Deletes rows and checks pagination", () => { xit("Deletes rows and checks pagination", () => {
// Delete rows, removing second page of rows from table // Delete rows, removing second page from table
const deleteRows = 5
cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.get(".spectrum-Checkbox-input").check({ force: true })
cy.get(".spectrum-Table") cy.get(".popovers").within(() => {
cy.contains("Delete 5 row(s)").click() cy.get(".spectrum-Button").click({ force: true })
cy.get(".spectrum-Modal").contains("Delete").click() })
cy.get(".spectrum-Dialog-grid").contains("Delete").click({ force: true })
cy.wait(1000) cy.wait(1000)
// Confirm table only has one page // Confirm table only has one page

View File

@ -19,6 +19,7 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Save and fetch tables")
.click({ force: true }) .click({ force: true })
cy.wait(500)
// Intercept Request after button click & apply assertions // Intercept Request after button click & apply assertions
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource") cy.get("@datasource")
@ -31,6 +32,7 @@ filterTests(["all"], () => {
cy.get("@datasource") cy.get("@datasource")
.its("response.body") .its("response.body")
.should("have.property", "status", 500) .should("have.property", "status", 500)
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
it("should add MySQL data source and fetch tables", () => { it("should add MySQL data source and fetch tables", () => {
@ -72,10 +74,13 @@ filterTests(["all"], () => {
cy.get(".spectrum-Popover").contains("COUNTRIES").click() cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click() cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
}) })
// Save relationship & reload page
cy.get(".spectrum-ButtonGroup").within(() => {
cy.get(".spectrum-Button").contains("Save").click({ force: true })
})
cy.reload()
// Confirm table length & column name // Confirm table length & column name
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(1) .eq(1)
@ -131,7 +136,7 @@ filterTests(["all"], () => {
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(1) .eq(1)
.within(() => { .within(() => {
cy.get(".spectrum-Table-row").eq(0).click() cy.get(".spectrum-Table-row").eq(0).click({ force: true })
cy.wait(500) cy.wait(500)
}) })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
@ -175,11 +180,12 @@ filterTests(["all"], () => {
}) })
it("should duplicate a query", () => { it("should duplicate a query", () => {
// Get last nav item - The query /// Get query nav item - QueryName
cy.get(".nav-item") cy.get(".nav-item")
.last() .contains(queryName)
.parent()
.within(() => { .within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".spectrum-Icon").eq(1).click({ force: true })
}) })
// Select and confirm duplication // Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
@ -199,12 +205,12 @@ filterTests(["all"], () => {
}) })
it("should delete a query", () => { it("should delete a query", () => {
// Get last nav item - The query // Get query nav item - QueryName
for (let i = 0; i < 2; i++) {
cy.get(".nav-item") cy.get(".nav-item")
.last() .contains(queryName)
.parent()
.within(() => { .within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".spectrum-Icon").eq(1).click({ force: true })
}) })
// Select Delete // Select Delete
cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Menu").contains("Delete").click()
@ -212,10 +218,8 @@ filterTests(["all"], () => {
.contains("Delete Query") .contains("Delete Query")
.click({ force: true }) .click({ force: true })
cy.wait(1000) cy.wait(1000)
}
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should("not.contain", queryName) cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should("not.contain", queryRename)
}) })
} }
}) })

View File

@ -46,9 +46,10 @@ filterTests(["all"], () => {
cy.get("@datasource") cy.get("@datasource")
.its("response.body") .its("response.body")
.should("have.property", "status", 500) .should("have.property", "status", 500)
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
it("should add Oracle data source and fetch tables", () => { xit("should add Oracle data source and fetch tables", () => {
// Add & configure Oracle data source // Add & configure Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
@ -64,7 +65,7 @@ filterTests(["all"], () => {
.should("be.gt", 0) .should("be.gt", 0)
}) })
it("should define a One relationship type", () => { xit("should define a One relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Define relationship") .contains("Define relationship")
@ -93,7 +94,7 @@ filterTests(["all"], () => {
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS") cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
}) })
it("should define a Many relationship type", () => { xit("should define a Many relationship type", () => {
// Select relationship type & configure // Select relationship type & configure
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Define relationship") .contains("Define relationship")
@ -127,7 +128,7 @@ filterTests(["all"], () => {
) )
}) })
it("should delete relationships", () => { xit("should delete relationships", () => {
// Delete both relationships // Delete both relationships
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(1) .eq(1)
@ -156,7 +157,7 @@ filterTests(["all"], () => {
}) })
}) })
it("should add a query", () => { xit("should add a query", () => {
// Add query // Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true }) cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item") cy.get(".spectrum-Form-item")
@ -181,7 +182,7 @@ filterTests(["all"], () => {
cy.get(".nav-item").should("contain", queryName) cy.get(".nav-item").should("contain", queryName)
}) })
it("should duplicate a query", () => { xit("should duplicate a query", () => {
// Get query nav item // Get query nav item
cy.get(".nav-item") cy.get(".nav-item")
.contains(queryName) .contains(queryName)
@ -194,7 +195,7 @@ filterTests(["all"], () => {
cy.get(".nav-item").should("contain", queryName + " (1)") cy.get(".nav-item").should("contain", queryName + " (1)")
}) })
it("should edit a query name", () => { xit("should edit a query name", () => {
// Rename query // Rename query
cy.get(".spectrum-Form-item") cy.get(".spectrum-Form-item")
.eq(0) .eq(0)
@ -206,7 +207,7 @@ filterTests(["all"], () => {
cy.get(".nav-item").should("contain", queryRename) cy.get(".nav-item").should("contain", queryRename)
}) })
it("should delete a query", () => { xit("should delete a query", () => {
// Get query nav item - QueryName // Get query nav item - QueryName
cy.get(".nav-item") cy.get(".nav-item")
.contains(queryName) .contains(queryName)

View File

@ -21,16 +21,10 @@ filterTests(["all"], () => {
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions // Intercept Request after button click & apply assertions
cy.wait("@datasource") cy.wait("@datasource")
cy.get("@datasource")
.its("response.body")
.should(
"have.property",
"message",
"connect ECONNREFUSED 127.0.0.1:5432"
)
cy.get("@datasource") cy.get("@datasource")
.its("response.body") .its("response.body")
.should("have.property", "status", 500) .should("have.property", "status", 500)
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
it("should add PostgreSQL data source and fetch tables", () => { it("should add PostgreSQL data source and fetch tables", () => {
@ -113,13 +107,13 @@ filterTests(["all"], () => {
}) })
it("should delete a relationship", () => { it("should delete a relationship", () => {
cy.get(".hierarchy-items-container").contains(datasource).click() cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
cy.reload() cy.reload()
// Delete one relationship // Delete one relationship
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(1) .eq(1)
.within(() => { .within(() => {
cy.get(".spectrum-Table-row").eq(0).click() cy.get(".spectrum-Table-row").eq(0).click({ force: true })
cy.wait(500) cy.wait(500)
}) })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
@ -161,7 +155,7 @@ filterTests(["all"], () => {
it("should switch to schema with no tables", () => { it("should switch to schema with no tables", () => {
// Switch Schema - To one without any tables // Switch Schema - To one without any tables
cy.get(".hierarchy-items-container").contains(datasource).click() cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
switchSchema("randomText") switchSchema("randomText")
// No tables displayed // No tables displayed
@ -208,11 +202,12 @@ filterTests(["all"], () => {
}) })
it("should duplicate a query", () => { it("should duplicate a query", () => {
// Get last nav item - The query // Locate previously created query
cy.get(".nav-item") cy.get(".nav-item")
.last() .contains(queryName)
.siblings(".actions")
.within(() => { .within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".icon").click({ force: true })
}) })
// Select and confirm duplication // Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
@ -240,12 +235,12 @@ filterTests(["all"], () => {
}) })
it("should delete a query", () => { it("should delete a query", () => {
// Get last nav item - The query // Get query nav item - QueryName
for (let i = 0; i < 2; i++) {
cy.get(".nav-item") cy.get(".nav-item")
.last() .contains(queryName)
.parent()
.within(() => { .within(() => {
cy.get(".icon").eq(1).click({ force: true }) cy.get(".spectrum-Icon").eq(1).click({ force: true })
}) })
// Select Delete // Select Delete
cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Menu").contains("Delete").click()
@ -253,10 +248,8 @@ filterTests(["all"], () => {
.contains("Delete Query") .contains("Delete Query")
.click({ force: true }) .click({ force: true })
cy.wait(1000) cy.wait(1000)
}
// Confirm deletion // Confirm deletion
cy.get(".nav-item").should("not.contain", queryName) cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should("not.contain", queryRename)
}) })
const switchSchema = schema => { const switchSchema = schema => {

View File

@ -72,6 +72,8 @@ Cypress.Commands.add("deleteApp", name => {
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
const findAppName = val.some(val => val.name == name)
if (findAppName) {
if (val.length > 0) { if (val.length > 0) {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
cy.searchForApplication(name) cy.searchForApplication(name)
@ -112,6 +114,9 @@ Cypress.Commands.add("deleteApp", name => {
} else { } else {
return return
} }
} else {
return
}
}) })
}) })
@ -488,7 +493,9 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
if (datasource == "Oracle") { if (datasource == "Oracle") {
cy.get("input").clear().type(Cypress.env("oracle").HOST) cy.get("input").clear().type(Cypress.env("oracle").HOST)
} else { } else {
cy.get("input").clear().type(Cypress.env("HOST_IP")) cy.get("input")
.clear({ force: true })
.type(Cypress.env("mysql").HOST, { force: true })
} }
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.105-alpha.8", "version": "1.0.105-alpha.35",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.105-alpha.8", "@budibase/bbui": "^1.0.105-alpha.35",
"@budibase/client": "^1.0.105-alpha.8", "@budibase/client": "^1.0.105-alpha.35",
"@budibase/frontend-core": "^1.0.105-alpha.8", "@budibase/frontend-core": "^1.0.105-alpha.35",
"@budibase/string-templates": "^1.0.105-alpha.8", "@budibase/string-templates": "^1.0.105-alpha.35",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -654,7 +654,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
* Builds a form schema given a form component. * Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form. * A form schema is a schema of all the fields nested anywhere within a form.
*/ */
const buildFormSchema = component => { export const buildFormSchema = component => {
let schema = {} let schema = {}
if (!component) { if (!component) {
return schema return schema

View File

@ -2,9 +2,15 @@ export default function (url) {
return url return url
.split("/") .split("/")
.map(part => { .map(part => {
// if parameter, then use as is part = decodeURIComponent(part)
if (part.startsWith(":")) return part part = part.replace(/ /g, "-")
return encodeURIComponent(part.replace(/ /g, "-"))
// If parameter, then use as is
if (!part.startsWith(":")) {
part = encodeURIComponent(part)
}
return part
}) })
.join("/") .join("/")
.toLowerCase() .toLowerCase()

View File

@ -39,6 +39,7 @@
if (v.internal) { if (v.internal) {
acc[k] = v acc[k] = v
} }
delete acc.LOOP
return acc return acc
}, {}) }, {})

View File

@ -72,7 +72,9 @@
animate:flip={{ duration: 500 }} animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }} in:fly|local={{ x: 500, duration: 1500 }}
> >
{#if block.stepId !== "LOOP"}
<FlowItem {testDataModal} {block} /> <FlowItem {testDataModal} {block} />
{/if}
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -9,8 +9,8 @@
Modal, Modal,
Button, Button,
StatusLight, StatusLight,
ActionButton,
Select, Select,
ActionButton,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
@ -25,8 +25,8 @@
let webhookModal let webhookModal
let actionModal let actionModal
let resultsModal let resultsModal
let setupToggled
let blockComplete let blockComplete
let showLooping = false
$: rowControl = $automationStore.selectedAutomation.automation.rowControl $: rowControl = $automationStore.selectedAutomation.automation.rowControl
$: showBindingPicker = $: showBindingPicker =
@ -52,8 +52,21 @@
block.schema?.inputs?.properties || {} block.schema?.inputs?.properties || {}
).every(x => block?.inputs[x]) ).every(x => block?.inputs[x])
$: loopingSelected =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
async function deleteStep() { async function deleteStep() {
let loopBlock =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
try { try {
if (loopBlock) {
automationStore.actions.deleteAutomationBlock(loopBlock)
}
automationStore.actions.deleteAutomationBlock(block) automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save( await automationStore.actions.save(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
@ -76,6 +89,23 @@
) )
} }
async function addLooping() {
loopingSelected = true
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
"LOOP",
loopDefinition
)
loopBlock.blockToLoop = block.id
block.loopBlock = loopBlock.id
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
async function onSelect(block) { async function onSelect(block) {
await automationStore.update(state => { await automationStore.update(state => {
state.selectedBlock = block state.selectedBlock = block
@ -84,13 +114,68 @@
} }
</script> </script>
<div <div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
class={`block ${block.type} hoverable`} {#if loopingSelected}
class:selected <div class="blockSection">
<div
on:click={() => {
showLooping = !showLooping
}}
class="splitHeader"
>
<div class="center-items">
<svg
width="28px"
height="28px"
class="spectrum-Icon"
style="color:grey;"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Reuse" />
</svg>
<div class="iconAlign">
<Detail size="S">Looping</Detail>
</div>
</div>
<div class="blockTitle">
<div
style="margin-left: 10px;"
on:click={() => { on:click={() => {
onSelect(block) onSelect(block)
}} }}
> >
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div>
</div>
<Divider noMargin />
{#if !showLooping}
<div class="blockSection">
<div class="block-options">
<div class="delete-padding" on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
</div>
</div>
<Layout noPadding gap="S">
<AutomationBlockSetup
schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
)}
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)}
{webhookModal}
/>
</Layout>
</div>
<Divider noMargin />
{/if}
{/if}
<div class="blockSection"> <div class="blockSection">
<div <div
on:click={() => { on:click={() => {
@ -127,55 +212,57 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> <Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div> </div>
</div> </div>
<div class="blockTitle">
{#if testResult && testResult[0]} {#if testResult && testResult[0]}
<span on:click={() => resultsModal.show()}> <div style="float: right;" on:click={() => resultsModal.show()}>
<StatusLight <StatusLight
positive={isTrigger || testResult[0].outputs?.success} positive={isTrigger || testResult[0].outputs?.success}
negative={!testResult[0].outputs?.success} negative={!testResult[0].outputs?.success}
><Body size="XS">View response</Body></StatusLight ><Body size="XS">View response</Body></StatusLight
> >
</span> </div>
{/if} {/if}
<div
style="margin-left: 10px;"
on:click={() => {
onSelect(block)
}}
>
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div> </div>
</div> </div>
{#if !blockComplete} {#if !blockComplete}
<Divider noMargin /> <Divider noMargin />
<div class="blockSection"> <div class="blockSection">
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div class="splitHeader">
<ActionButton
on:click={() => {
onSelect(block)
setupToggled = !setupToggled
}}
quiet
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Setup</Detail>
</ActionButton>
{#if !isTrigger} {#if !isTrigger}
<div class="block-options">
{#if showBindingPicker}
<div> <div>
<div class="block-options">
{#if !loopingSelected}
<ActionButton on:click={() => addLooping()} icon="Reuse"
>Add Looping</ActionButton
>
{/if}
{#if showBindingPicker}
<Select <Select
on:change={toggleFieldControl} on:change={toggleFieldControl}
quiet
defaultValue="Use values" defaultValue="Use values"
autoWidth autoWidth
value={rowControl ? "Use bindings" : "Use values"} value={rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]} options={["Use values", "Use bindings"]}
placeholder={null} placeholder={null}
/> />
</div>
{/if} {/if}
<div class="delete-padding" on:click={() => deleteStep()}> <ActionButton
<Icon name="DeleteOutline" /> on:click={() => deleteStep()}
icon="DeleteOutline"
/>
</div> </div>
</div> </div>
{/if} {/if}
</div>
{#if setupToggled}
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries(block.schema.inputs.properties)} schemaProperties={Object.entries(block.schema.inputs.properties)}
{block} {block}
@ -186,7 +273,6 @@
>Finish and test automation</Button >Finish and test automation</Button
> >
{/if} {/if}
{/if}
</Layout> </Layout>
</div> </div>
{/if} {/if}
@ -220,8 +306,10 @@
padding-left: 30px; padding-left: 30px;
} }
.block-options { .block-options {
display: flex; justify-content: flex-end;
align-items: center; align-items: center;
display: flex;
gap: var(--spacing-m);
} }
.center-items { .center-items {
display: flex; display: flex;
@ -256,4 +344,9 @@
/* center horizontally */ /* center horizontally */
align-self: center; align-self: center;
} }
.blockTitle {
display: flex;
align-items: center;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui" import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
export let testResult export let testResult
export let isTrigger export let isTrigger
@ -10,7 +10,7 @@
<ModalContent <ModalContent
showCloseIcon={false} showCloseIcon={false}
showConfirmButton={false} showConfirmButton={false}
title="Test Automation" title="Test Results"
cancelText="Close" cancelText="Close"
> >
<div slot="header"> <div slot="header">
@ -26,7 +26,18 @@
{/if} {/if}
</div> </div>
</div> </div>
<span>
{#if testResult[0].outputs.iterations}
<div style="display: flex;">
<Icon name="Reuse" />
<div style="margin-left: 10px;">
<Label>
This loop ran {testResult[0].outputs.iterations} times.</Label
>
</div>
</div>
{/if}
</span>
<div <div
on:click={() => { on:click={() => {
inputToggled = !inputToggled inputToggled = !inputToggled

View File

@ -88,33 +88,65 @@
if (!block || !automation) { if (!block || !automation) {
return [] return []
} }
// Find previous steps to the selected one // Find previous steps to the selected one
let allSteps = [...automation.steps] let allSteps = [...automation.steps]
if (automation.trigger) { if (automation.trigger) {
allSteps = [automation.trigger, ...allSteps] allSteps = [automation.trigger, ...allSteps]
} }
const blockIdx = allSteps.findIndex(step => step.id === block.id) let blockIdx = allSteps.findIndex(step => step.id === block.id)
// Extract all outputs from all previous steps as available bindings // Extract all outputs from all previous steps as available bindins
let bindings = [] let bindings = []
for (let idx = 0; idx < blockIdx; idx++) { for (let idx = 0; idx < blockIdx; idx++) {
const outputs = Object.entries( let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
allSteps[idx].schema?.outputs?.properties ?? {} let isLoopBlock =
) allSteps[idx]?.stepId === "LOOP" &&
allSteps.find(x => x.blockToLoop === block.id)
// If the previous block was a loop block, decerement the index so the following
// steps are in the correct order
if (wasLoopBlock) {
blockIdx--
}
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
// If its a Loop Block, we need to add this custom schema
if (isLoopBlock) {
schema = {
currentItem: {
type: "string",
description: "the item currently being executed",
},
}
}
const outputs = Object.entries(schema)
bindings = bindings.concat( bindings = bindings.concat(
outputs.map(([name, value]) => { outputs.map(([name, value]) => {
const runtime = idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}` let runtimeName = isLoopBlock
? `loop.${name}`
: block.name.startsWith("JS")
? `steps[${idx}].${name}`
: `steps.${idx}.${name}`
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
return { return {
label: runtime, label: runtime,
type: value.type, type: value.type,
description: value.description, description: value.description,
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`, category:
idx === 0
? "Trigger outputs"
: isLoopBlock
? "Loop Outputs"
: `Step ${idx} outputs`,
path: runtime, path: runtime,
} }
}) })
) )
} }
return bindings return bindings
} }
@ -261,6 +293,14 @@
value={inputData[key]} value={inputData[key]}
/> />
</CodeEditorModal> </CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"} {:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal} {#if isTestModal}
<ModalBindableInput <ModalBindableInput

View File

@ -14,7 +14,7 @@
import Table from "./Table.svelte" import Table from "./Table.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
import { Pagination } from "@budibase/bbui" import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
@ -27,6 +27,8 @@
$: enrichedSchema = enrichSchema($tables.selected?.schema) $: enrichedSchema = enrichSchema($tables.selected?.schema)
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: fetch = createFetch(id) $: fetch = createFetch(id)
$: hasCols = checkHasCols(schema)
$: hasRows = !!$fetch.rows?.length
const enrichSchema = schema => { const enrichSchema = schema => {
let tempSchema = { ...schema } let tempSchema = { ...schema }
@ -47,6 +49,20 @@
return tempSchema return tempSchema
} }
const checkHasCols = schema => {
if (!schema || Object.keys(schema).length === 0) {
return false
}
let fields = Object.values(schema)
for (let field of fields) {
if (!field.autocolumn) {
return true
}
}
return false
}
// Fetches new data whenever the table changes // Fetches new data whenever the table changes
const createFetch = tableId => { const createFetch = tableId => {
return fetchData({ return fetchData({
@ -104,19 +120,28 @@
disableSorting disableSorting
on:updatecolumns={onUpdateColumns} on:updatecolumns={onUpdateColumns}
on:updaterows={onUpdateRows} on:updaterows={onUpdateRows}
customPlaceholder
> >
<CreateColumnButton on:updatecolumns={onUpdateColumns} /> <div class="buttons">
{#if schema && Object.keys(schema).length > 0} <div class="left-buttons">
<CreateColumnButton
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
on:updatecolumns={onUpdateColumns}
/>
{#if !isUsersTable} {#if !isUsersTable}
<CreateRowButton <CreateRowButton
on:updaterows={onUpdateRows} on:updaterows={onUpdateRows}
title={"Create row"} title={"Create row"}
modalContentComponent={CreateEditRow} modalContentComponent={CreateEditRow}
disabled={!hasCols}
highlighted={$fetch.loaded && hasCols && !hasRows}
/> />
{/if} {/if}
{#if isInternal} {#if isInternal}
<CreateViewButton /> <CreateViewButton disabled={!hasCols || !hasRows} />
{/if} {/if}
</div>
<div class="right-buttons">
<ManageAccessButton resourceId={$tables.selected?._id} /> <ManageAccessButton resourceId={$tables.selected?._id} />
{#if isUsersTable} {#if isUsersTable}
<EditRolesButton /> <EditRolesButton />
@ -128,16 +153,40 @@
/> />
{/if} {/if}
<HideAutocolumnButton bind:hideAutocolumns /> <HideAutocolumnButton bind:hideAutocolumns />
<!-- always have the export last -->
<ExportButton view={$tables.selected?._id} />
<ImportButton <ImportButton
tableId={$tables.selected?._id} tableId={$tables.selected?._id}
on:updaterows={onUpdateRows} on:updaterows={onUpdateRows}
/> />
<ExportButton
disabled={!hasRows || !hasCols}
view={$tables.selected?._id}
/>
{#key id} {#key id}
<TableFilterButton {schema} on:change={onFilter} /> <TableFilterButton
{schema}
on:change={onFilter}
disabled={!hasCols || !hasRows}
/>
{/key} {/key}
</div>
</div>
<div slot="placeholder">
<Layout gap="S">
{#if !hasCols}
<Heading>Let's create some columns</Heading>
<Body>
Start building out your table structure<br />
by adding some columns
</Body>
{:else}
<Heading>Now let's add a row</Heading>
<Body>
Add some data to your table<br />
by adding some rows
</Body>
{/if} {/if}
</Layout>
</div>
</Table> </Table>
{#key id} {#key id}
<div in:fade={{ delay: 200, duration: 100 }}> <div in:fade={{ delay: 200, duration: 100 }}>
@ -162,4 +211,20 @@
align-items: center; align-items: center;
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
.buttons {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.left-buttons,
.right-buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
</style> </style>

View File

@ -34,10 +34,10 @@
$: label = meta.name ? capitalise(meta.name) : "" $: label = meta.name ? capitalise(meta.name) : ""
const timeStamp = resolveTimeStamp(value) const timeStamp = resolveTimeStamp(value)
const isTimeStamp = timeStamp ? true : false const isTimeStamp = !!timeStamp
</script> </script>
{#if type === "options"} {#if type === "options" && meta.constraints.inclusion.length !== 0}
<Select <Select
{label} {label}
data-cy="{meta.name}-select" data-cy="{meta.name}-select"
@ -51,7 +51,7 @@
<Dropzone {label} bind:value /> <Dropzone {label} bind:value />
{:else if type === "boolean"} {:else if type === "boolean"}
<Toggle text={label} bind:value data-cy="{meta.name}-input" /> <Toggle text={label} bind:value data-cy="{meta.name}-input" />
{:else if type === "array"} {:else if type === "array" && meta.constraints.inclusion.length !== 0}
<Multiselect bind:value {label} options={meta.constraints.inclusion} /> <Multiselect bind:value {label} options={meta.constraints.inclusion} />
{:else if type === "link"} {:else if type === "link"}
<LinkedRowSelector bind:linkedRows={value} schema={meta} /> <LinkedRowSelector bind:linkedRows={value} schema={meta} />

View File

@ -25,6 +25,7 @@
export let rowCount export let rowCount
export let type export let type
export let disableSorting = false export let disableSorting = false
export let customPlaceholder = false
let selectedRows = [] let selectedRows = []
let editableColumn let editableColumn
@ -117,10 +118,10 @@
</script> </script>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div> <Layout noPadding gap="XS">
{#if title} {#if title}
<div class="table-title"> <div class="table-title">
<Heading size="S">{title}</Heading> <Heading size="M">{title}</Heading>
{#if loading} {#if loading}
<div transition:fade|local> <div transition:fade|local>
<Spinner size="10" /> <Spinner size="10" />
@ -134,7 +135,7 @@
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} /> <DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
{/if} {/if}
</div> </div>
</div> </Layout>
{#key tableId} {#key tableId}
<div class="table-wrapper"> <div class="table-wrapper">
<Table <Table
@ -144,6 +145,7 @@
{customRenderers} {customRenderers}
{rowCount} {rowCount}
{disableSorting} {disableSorting}
{customPlaceholder}
bind:selectedRows bind:selectedRows
allowSelectRows={allowEditing && !isUsersTable} allowSelectRows={allowEditing && !isUsersTable}
allowEditRows={allowEditing} allowEditRows={allowEditing}
@ -153,7 +155,9 @@
on:editrow={e => editRow(e.detail)} on:editrow={e => editRow(e.detail)}
on:clickrelationship={e => selectRelationship(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)}
on:sort on:sort
/> >
<slot slot="placeholder" name="placeholder" />
</Table>
</div> </div>
{/key} {/key}
</Layout> </Layout>
@ -176,6 +180,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
margin-top: var(--spacing-m);
} }
.table-title > div { .table-title > div {
margin-left: var(--spacing-xs); margin-left: var(--spacing-xs);

View File

@ -2,10 +2,21 @@
import { ActionButton, Modal } from "@budibase/bbui" import { ActionButton, Modal } from "@budibase/bbui"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumn from "../modals/CreateEditColumn.svelte"
export let highlighted = false
export let disabled = false
let modal let modal
</script> </script>
<ActionButton icon="TableColumnAddRight" quiet size="S" on:click={modal.show}> <ActionButton
{disabled}
selected={highlighted}
emphasized={highlighted}
icon="TableColumnAddRight"
quiet
size="S"
on:click={modal.show}
>
Create column Create column
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -4,11 +4,21 @@
export let modalContentComponent = CreateEditRow export let modalContentComponent = CreateEditRow
export let title = "Create row" export let title = "Create row"
export let disabled = false
export let highlighted = false
let modal let modal
</script> </script>
<ActionButton icon="TableRowAddBottom" size="S" quiet on:click={modal.show}> <ActionButton
{disabled}
emphasized={highlighted}
selected={highlighted}
icon="TableRowAddBottom"
size="S"
quiet
on:click={modal.show}
>
{title} {title}
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -2,10 +2,18 @@
import { Modal, ActionButton } from "@budibase/bbui" import { Modal, ActionButton } from "@budibase/bbui"
import CreateViewModal from "../modals/CreateViewModal.svelte" import CreateViewModal from "../modals/CreateViewModal.svelte"
export let disabled = false
let modal let modal
</script> </script>
<ActionButton icon="CollectionAdd" size="S" quiet on:click={modal.show}> <ActionButton
{disabled}
icon="CollectionAdd"
size="S"
quiet
on:click={modal.show}
>
Create view Create view
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -3,11 +3,18 @@
import ExportModal from "../modals/ExportModal.svelte" import ExportModal from "../modals/ExportModal.svelte"
export let view export let view
export let disabled = false
let modal let modal
</script> </script>
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}> <ActionButton
{disabled}
icon="DataDownload"
size="S"
quiet
on:click={modal.show}
>
Export Export
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -8,6 +8,12 @@
} }
</script> </script>
<ActionButton icon="MagicWand" primary size="S" quiet on:click={hideOrUnhide}> <ActionButton
{#if hideAutocolumns}Show auto columns{:else}Hide auto columns{/if} icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
primary
size="S"
quiet
on:click={hideOrUnhide}
>
Auto columns
</ActionButton> </ActionButton>

View File

@ -5,6 +5,7 @@
export let schema export let schema
export let filters export let filters
export let disabled = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let modal let modal
@ -17,6 +18,7 @@
icon="Filter" icon="Filter"
size="S" size="S"
quiet quiet
{disabled}
on:click={modal.show} on:click={modal.show}
active={tempValue?.length > 0} active={tempValue?.length > 0}
> >

View File

@ -6,15 +6,10 @@
export let overlayEnabled = true export let overlayEnabled = true
let imageError = false let imageError = false
let imageLoaded = false
const imageRenderError = () => { const imageRenderError = () => {
imageError = true imageError = true
} }
const imageLoadSuccess = () => {
imageLoaded = true
}
</script> </script>
<div class="template-card" style="background-color:{backgroundColour};"> <div class="template-card" style="background-color:{backgroundColour};">
@ -23,8 +18,7 @@
alt={name} alt={name}
src={imageSrc} src={imageSrc}
on:error={imageRenderError} on:error={imageRenderError}
on:load={imageLoadSuccess} class:error={imageError}
class={`${imageLoaded ? "loaded" : ""}`}
/> />
<div style={`display:${imageError ? "block" : "none"}`}> <div style={`display:${imageError ? "block" : "none"}`}>
<svg <svg
@ -104,15 +98,14 @@
width: 100%; width: 100%;
} }
.template-card img.loaded {
display: block;
}
.template-card img { .template-card img {
display: none; display: block;
max-width: 100%; max-width: 100%;
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px; border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
} }
.template-card img.error {
display: none;
}
.template-card:hover { .template-card:hover {
background: var(--spectrum-alias-background-color-tertiary); background: var(--spectrum-alias-background-color-tertiary);

View File

@ -3,13 +3,14 @@
import PathTree from "./PathTree.svelte" import PathTree from "./PathTree.svelte"
let routes = {} let routes = {}
$: paths = Object.keys(routes || {}).sort() let paths = []
$: { $: allRoutes = $store.routes
const allRoutes = $store.routes $: selectedScreenId = $store.selectedScreenId
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId)
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => {
const sortedPaths = Object.keys(allRoutes || {}).sort() const sortedPaths = Object.keys(allRoutes || {}).sort()
const selectedRoleId = $selectedAccessRole
const selectedScreenId = $store.selectedScreenId
let found = false let found = false
let firstValidScreenId let firstValidScreenId
@ -41,11 +42,15 @@
}) })
}) })
}) })
routes = filteredRoutes routes = { ...filteredRoutes }
paths = Object.keys(routes || {}).sort()
// Select the correct role for the current screen ID // Select the correct role for the current screen ID
if (!found && screenRoleId) { if (!found && screenRoleId) {
selectedAccessRole.set(screenRoleId) selectedAccessRole.set(screenRoleId)
if (screenRoleId !== selectedRoleId) {
updatePaths(allRoutes, screenRoleId, selectedScreenId)
}
} }
// If the selected screen isn't in this filtered list, select the first one // If the selected screen isn't in this filtered list, select the first one

View File

@ -26,14 +26,6 @@
on:change={value => (parameters.rowId = value.detail)} on:change={value => (parameters.rowId = value.detail)}
/> />
<Label small>Row Rev</Label>
<DrawerBindableInput
{bindings}
title="Row rev to delete"
value={parameters.revId}
on:change={value => (parameters.revId = value.detail)}
/>
<Label small /> <Label small />
<Checkbox text="Require confirmation" bind:value={parameters.confirm} /> <Checkbox text="Require confirmation" bind:value={parameters.confirm} />

View File

@ -19,7 +19,7 @@
.filter(a => a.definition.trigger?.stepId === "APP") .filter(a => a.definition.trigger?.stepId === "APP")
.map(automation => { .map(automation => {
const schema = Object.entries( const schema = Object.entries(
automation.definition.trigger.inputs.fields automation.definition.trigger.inputs.fields || {}
).map(([name, type]) => ({ name, type })) ).map(([name, type]) => ({ name, type }))
return { return {

View File

@ -0,0 +1,78 @@
<script>
import { Select, Label, Combobox } from "@budibase/bbui"
import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { currentAsset, store } from "builderStore"
import {
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils"
export let parameters
export let bindings = []
const typeOptions = [
{
label: "Set value",
value: "set",
},
{
label: "Reset to default value",
value: "reset",
},
]
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
$: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents(
$currentAsset,
$store.selectedComponentId,
"ValidateForm"
)
onMount(() => {
if (!parameters.type) {
parameters.type = "set"
}
})
</script>
<div class="root">
<Label small>Form</Label>
<Select
bind:value={parameters.componentId}
options={actionProviders}
getOptionLabel={x => x._instanceName}
getOptionValue={x => x._id}
/>
<Label small>Type</Label>
<Select
placeholder={null}
bind:value={parameters.type}
options={typeOptions}
/>
<Label small>Field</Label>
<Combobox bind:value={parameters.field} options={fieldOptions} />
{#if parameters.type === "set"}
<Label small>Value</Label>
<DrawerBindableInput
{bindings}
value={parameters.value}
on:change={e => (parameters.value = e.detail)}
/>
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -14,3 +14,4 @@ export { default as DuplicateRow } from "./DuplicateRow.svelte"
export { default as S3Upload } from "./S3Upload.svelte" export { default as S3Upload } from "./S3Upload.svelte"
export { default as ExportData } from "./ExportData.svelte" export { default as ExportData } from "./ExportData.svelte"
export { default as ContinueIf } from "./ContinueIf.svelte" export { default as ContinueIf } from "./ContinueIf.svelte"
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"

View File

@ -42,25 +42,29 @@
"name": "Trigger Automation", "name": "Trigger Automation",
"component": "TriggerAutomation" "component": "TriggerAutomation"
}, },
{
"name": "Update Field Value",
"component": "UpdateFieldValue"
},
{ {
"name": "Validate Form", "name": "Validate Form",
"component": "ValidateForm" "component": "ValidateForm"
}, },
{ {
"name": "Log Out", "name": "Change Form Step",
"component": "LogOut" "component": "ChangeFormStep"
}, },
{ {
"name": "Clear Form", "name": "Clear Form",
"component": "ClearForm" "component": "ClearForm"
}, },
{ {
"name": "Close Screen Modal", "name": "Log Out",
"component": "CloseScreenModal" "component": "LogOut"
}, },
{ {
"name": "Change Form Step", "name": "Close Screen Modal",
"component": "ChangeFormStep" "component": "CloseScreenModal"
}, },
{ {
"name": "Refresh Data Provider", "name": "Refresh Data Provider",

View File

@ -52,7 +52,6 @@
.map(query => ({ .map(query => ({
label: query.name, label: query.name,
name: query.name, name: query.name,
tableId: query._id,
...query, ...query,
type: "query", type: "query",
})) }))

View File

@ -10,9 +10,15 @@
let drawer let drawer
let tempValue = value || [] let tempValue = value || []
const saveFilter = async () => { const saveOptions = async () => {
// Filter out incomplete options // Filter out incomplete options, default if needed
tempValue = tempValue.filter(option => option.value && option.label) tempValue = tempValue.filter(option => option.value || option.label)
for (let i = 0; i < tempValue.length; i++) {
let option = tempValue[i]
option.label = option.label ? option.label : option.value
option.value = option.value ? option.value : option.label
tempValue[i] = option
}
dispatch("change", tempValue) dispatch("change", tempValue)
drawer.hide() drawer.hide()
} }
@ -23,6 +29,6 @@
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define the options for this picker. Define the options for this picker.
</svelte:fragment> </svelte:fragment>
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> <Button cta slot="buttons" on:click={saveOptions}>Save</Button>
<OptionsDrawer bind:options={tempValue} slot="body" /> <OptionsDrawer bind:options={tempValue} slot="body" />
</Drawer> </Drawer>

View File

@ -3,6 +3,7 @@
import { roles } from "stores/backend" import { roles } from "stores/backend"
export let value export let value
export let error
</script> </script>
<Select <Select
@ -11,4 +12,5 @@
options={$roles} options={$roles}
getOptionLabel={role => role.name} getOptionLabel={role => role.name}
getOptionValue={role => role._id} getOptionValue={role => role._id}
{error}
/> />

View File

@ -15,16 +15,14 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, datasource, { $: schema = getSchemaForDatasource($currentAsset, datasource).schema
searchableSchema: true,
}).schema
$: options = getOptions(datasource, schema || {}) $: options = getOptions(datasource, schema || {})
$: boundValue = getSelectedOption(value, options) $: boundValue = getSelectedOption(value, options)
function getOptions(ds, dsSchema) { function getOptions(ds, dsSchema) {
let base = Object.values(dsSchema) let base = Object.values(dsSchema)
if (!ds?.tableId) { if (!ds?.tableId) {
return base return base.map(field => field.name)
} }
const currentTable = $tables.list.find(table => table._id === ds.tableId) const currentTable = $tables.list.find(table => table._id === ds.tableId)
return getFields(base, { allowLinks: currentTable?.sql }).map( return getFields(base, { allowLinks: currentTable?.sql }).map(

View File

@ -8,14 +8,50 @@
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" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { allScreens, selectedAccessRole } from "builderStore"
export let componentInstance export let componentInstance
export let bindings export let bindings
function setAssetProps(name, value, parser) { let errors = {}
if (parser && typeof parser === "function") {
const routeTaken = url => {
const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
const roleTaken = roleId => {
const url = get(currentAsset)?.routing.route
return get(allScreens).some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
const setAssetProps = (name, value, parser, validate) => {
if (parser) {
value = parser(value) value = parser(value)
} }
if (validate) {
const error = validate(value)
errors = {
...errors,
[name]: error,
}
if (error) {
return
}
} else {
errors = {
...errors,
[name]: null,
}
}
const selectedAsset = get(currentAsset) const selectedAsset = get(currentAsset)
store.update(state => { store.update(state => {
@ -38,7 +74,6 @@
} }
const screenSettings = [ const screenSettings = [
// { key: "description", label: "Description", control: Input },
{ {
key: "routing.route", key: "routing.route",
label: "Route", label: "Route",
@ -49,8 +84,26 @@
} }
return sanitizeUrl(val) return sanitizeUrl(val)
}, },
validate: val => {
const exisingValue = get(currentAsset)?.routing.route
if (val !== exisingValue && routeTaken(val)) {
return "That URL is already in use for this role"
}
return null
},
},
{
key: "routing.roleId",
label: "Access",
control: RoleSelect,
validate: val => {
const exisingValue = get(currentAsset)?.routing.roleId
if (val !== exisingValue && roleTaken(val)) {
return "That role is already in use for this URL"
}
return null
},
}, },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect }, { key: "layoutId", label: "Layout", control: LayoutSelect },
] ]
</script> </script>
@ -62,9 +115,11 @@
control={def.control} control={def.control}
label={def.label} label={def.label}
key={def.key} key={def.key}
error="asdasds"
value={deepGet($currentAsset, def.key)} value={deepGet($currentAsset, def.key)}
onChange={val => setAssetProps(def.key, val, def.parser)} onChange={val => setAssetProps(def.key, val, def.parser, def.validate)}
{bindings} {bindings}
props={{ error: errors[def.key] }}
/> />
{/each} {/each}
</DetailSummary> </DetailSummary>

View File

@ -0,0 +1,14 @@
import { auth } from "../stores/portal"
import { get } from "svelte/store"
export const FEATURE_FLAGS = {
LICENSING: "LICENSING",
}
export const isEnabled = featureFlag => {
const user = get(auth).user
if (user?.featureFlags?.includes(featureFlag)) {
return true
}
return false
}

View File

@ -14,7 +14,7 @@
notifications.success("Invitation accepted successfully") notifications.success("Invitation accepted successfully")
$goto("../auth/login") $goto("../auth/login")
} catch (error) { } catch (error) {
notifications.error("Error accepting invitation") notifications.error(error.message)
} }
} }
</script> </script>

View File

@ -20,6 +20,7 @@
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte" import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte" import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { isEnabled, FEATURE_FLAGS } from "../../../helpers/featureFlags"
let loaded = false let loaded = false
let userInfoModal let userInfoModal
@ -54,10 +55,17 @@
if (!$adminStore.cloud) { if (!$adminStore.cloud) {
menu = menu.concat([ menu = menu.concat([
{ {
title: "Updates", title: "Update",
href: "/builder/portal/settings/update", href: "/builder/portal/settings/update",
}, },
]) ])
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
menu = menu.concat({
title: "Upgrade",
href: "/builder/portal/settings/upgrade",
})
}
} }
} else { } else {
menu = menu.concat([ menu = menu.concat([

View File

@ -15,7 +15,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { templates } from "stores/portal" import { templates } from "stores/portal"
let loaded = false let loaded = $templates?.length
let template let template
let creationModal = false let creationModal = false
let creatingApp = false let creatingApp = false
@ -75,8 +75,8 @@
<div class="title"> <div class="title">
<div class="welcome"> <div class="welcome">
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Heading size="M">{createAppTitle}</Heading> <Heading size="L">{createAppTitle}</Heading>
<Body size="S"> <Body size="M">
{welcomeBody} {welcomeBody}
</Body> </Body>
</Layout> </Layout>
@ -84,7 +84,7 @@
<div class="buttons"> <div class="buttons">
<Button <Button
dataCy="create-app-btn" dataCy="create-app-btn"
size="L" size="M"
icon="Add" icon="Add"
cta cta
on:click={initiateAppCreation} on:click={initiateAppCreation}
@ -94,7 +94,7 @@
<Button <Button
dataCy="import-app-btn" dataCy="import-app-btn"
icon="Import" icon="Import"
size="L" size="M"
quiet quiet
secondary secondary
on:click={initiateAppImport} on:click={initiateAppImport}

View File

@ -40,7 +40,7 @@
let unpublishModal let unpublishModal
let iconModal let iconModal
let creatingApp = false let creatingApp = false
let loaded = false let loaded = $apps?.length || $templates?.length
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let appName = "" let appName = ""
@ -292,8 +292,8 @@
<div class="title"> <div class="title">
<div class="welcome"> <div class="welcome">
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Heading size="M">{welcomeHeader}</Heading> <Heading size="L">{welcomeHeader}</Heading>
<Body size="S"> <Body size="M">
{welcomeBody} {welcomeBody}
</Body> </Body>
</Layout> </Layout>
@ -301,7 +301,7 @@
<div class="buttons"> <div class="buttons">
<Button <Button
dataCy="create-app-btn" dataCy="create-app-btn"
size="L" size="M"
icon="Add" icon="Add"
cta cta
on:click={initiateAppCreation} on:click={initiateAppCreation}
@ -311,7 +311,7 @@
{#if $apps?.length > 0} {#if $apps?.length > 0}
<Button <Button
icon="Experience" icon="Experience"
size="L" size="M"
quiet quiet
secondary secondary
on:click={$goto("/builder/portal/apps/templates")} on:click={$goto("/builder/portal/apps/templates")}
@ -348,7 +348,7 @@
{#if enrichedApps.length} {#if enrichedApps.length}
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div class="title"> <div class="title">
<Detail size="L">My apps</Detail> <Detail size="L">Apps</Detail>
{#if enrichedApps.length > 1} {#if enrichedApps.length > 1}
<div class="app-actions"> <div class="app-actions">
{#if cloud} {#if cloud}

View File

@ -5,7 +5,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { templates } from "stores/portal" import { templates } from "stores/portal"
let loaded = false let loaded = $templates?.length
onMount(async () => { onMount(async () => {
try { try {

View File

@ -26,7 +26,7 @@
}) })
notifications.success("Successfully created user") notifications.success("Successfully created user")
} catch (error) { } catch (error) {
notifications.error("Error creating user") notifications.error(error.message)
} }
} }
</script> </script>

View File

@ -0,0 +1,151 @@
<script>
import {
Layout,
Heading,
Body,
Divider,
Link,
Button,
Input,
Label,
notifications,
} from "@budibase/bbui"
import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify"
import { processStringSync } from "@budibase/string-templates"
import { API } from "api"
import { onMount } from "svelte"
$: license = $auth.user.license
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: activateDisabled = !licenseKey || licenseKeyDisabled
let licenseInfo
let licenseKeyDisabled = false
let licenseKeyType = "text"
let licenseKey = ""
// Make sure page can't be visited directly in cloud
$: {
if ($admin.cloud) {
$redirect("../../portal")
}
}
const activate = async () => {
await API.activateLicenseKey({ licenseKey })
await auth.getSelf()
await setLicenseInfo()
notifications.success("Successfully activated")
}
const refresh = async () => {
try {
await API.refreshLicense()
await auth.getSelf()
notifications.success("Refreshed license")
} catch (err) {
console.error(err)
notifications.error("Error refreshing license")
}
}
// deactivate the license key field if there is a license key set
$: {
if (licenseInfo?.licenseKey) {
licenseKey = "**********************************************"
licenseKeyType = "password"
licenseKeyDisabled = true
activateDisabled = true
}
}
const setLicenseInfo = async () => {
licenseInfo = await API.getLicenseInfo()
}
onMount(async () => {
await setLicenseInfo()
})
</script>
{#if $auth.isAdmin}
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading size="M">Upgrade</Heading>
<Body size="M">
{#if license.plan.type === "free"}
Upgrade your budibase installation to unlock additional features. To
subscribe to a plan visit your <Link size="L" href={upgradeUrl}
>Account</Link
>.
{:else}
To manage your plan visit your <Link size="L" href={upgradeUrl}
>Account</Link
>.
{/if}
</Body>
</Layout>
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">Activate</Heading>
<Body size="S">Enter your license key below to activate your plan</Body>
</Layout>
<Layout noPadding>
<div class="fields">
<div class="field">
<Label size="L">License Key</Label>
<Input
thin
bind:value={licenseKey}
type={licenseKeyType}
disabled={licenseKeyDisabled}
/>
</div>
</div>
<div>
<Button cta on:click={activate} disabled={activateDisabled}
>Activate</Button
>
</div>
</Layout>
<Divider size="S" />
<Layout gap="L" noPadding>
<Layout gap="S" noPadding>
<Heading size="S">Plan</Heading>
<Layout noPadding gap="XXS">
<Body size="S">You are currently on the {license.plan.type} plan</Body
>
<Body size="XS">
{processStringSync(
"Updated {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(license.refreshedAt).getTime(),
}
)}
</Body>
</Layout>
</Layout>
<div>
<Button secondary on:click={refresh}>Refresh</Button>
</div>
</Layout>
</Layout>
{/if}
<style>
.fields {
display: grid;
grid-gap: var(--spacing-m);
}
.field {
display: grid;
grid-template-columns: 100px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,14 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Publish Dev",
"program": "${workspaceFolder}/scripts/publishDev.js"
}
]
}

View File

@ -1834,7 +1834,12 @@
"icon": "Form", "icon": "Form",
"hasChildren": true, "hasChildren": true,
"illegalChildren": ["section", "form"], "illegalChildren": ["section", "form"],
"actions": ["ValidateForm", "ClearForm", "ChangeFormStep"], "actions": [
"ValidateForm",
"ClearForm",
"ChangeFormStep",
"UpdateFieldValue"
],
"styles": ["size"], "styles": ["size"],
"settings": [ "settings": [
{ {
@ -1975,6 +1980,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2049,6 +2065,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2089,6 +2116,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2125,6 +2163,17 @@
"key": "placeholder", "key": "placeholder",
"placeholder": "Choose an option" "placeholder": "Choose an option"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "select", "type": "select",
"label": "Type", "label": "Type",
@ -2274,6 +2323,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
@ -2399,6 +2459,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2439,6 +2510,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "select", "type": "select",
"label": "Formatting", "label": "Formatting",
@ -2512,6 +2594,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2657,6 +2750,17 @@
"label": "Extensions", "label": "Extensions",
"key": "extensions" "key": "extensions"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2697,6 +2801,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
@ -2742,6 +2857,17 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -2750,6 +2876,62 @@
} }
] ]
}, },
"s3upload": {
"name": "S3 File Upload",
"info": "This component can't be used with S3 datasources that use custom endpoints.",
"icon": "UploadToCloud",
"styles": ["size"],
"editable": true,
"settings": [
{
"type": "field/attachment",
"label": "Field",
"key": "field"
},
{
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "dataSource/s3",
"label": "S3 Datasource",
"key": "datasourceId"
},
{
"type": "text",
"label": "Bucket",
"key": "bucket"
},
{
"type": "text",
"label": "File Name",
"key": "key"
},
{
"type": "event",
"label": "On Change",
"key": "onChange",
"context": [
{
"label": "Field Value",
"key": "value"
}
]
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
},
{
"type": "validation/attachment",
"label": "Validation",
"key": "validation"
}
]
},
"dataprovider": { "dataprovider": {
"name": "Data Provider", "name": "Data Provider",
"info": "Pagination is only available for data stored in tables.", "info": "Pagination is only available for data stored in tables.",
@ -3581,51 +3763,6 @@
} }
] ]
}, },
"s3upload": {
"name": "S3 File Upload",
"info": "This component can't be used with S3 datasources that use custom endpoints.",
"icon": "UploadToCloud",
"styles": ["size"],
"editable": true,
"settings": [
{
"type": "field/attachment",
"label": "Field",
"key": "field"
},
{
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "dataSource/s3",
"label": "S3 Datasource",
"key": "datasourceId"
},
{
"type": "text",
"label": "Bucket",
"key": "bucket"
},
{
"type": "text",
"label": "File Name",
"key": "key"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
},
{
"type": "validation/attachment",
"label": "Validation",
"key": "validation"
}
]
},
"markdownviewer": { "markdownviewer": {
"name": "Markdown Viewer", "name": "Markdown Viewer",
"icon": "TaskList", "icon": "TaskList",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.105-alpha.8", "version": "1.0.105-alpha.35",
"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": "^1.0.105-alpha.8", "@budibase/bbui": "^1.0.105-alpha.35",
"@budibase/frontend-core": "^1.0.105-alpha.8", "@budibase/frontend-core": "^1.0.105-alpha.35",
"@budibase/string-templates": "^1.0.105-alpha.8", "@budibase/string-templates": "^1.0.105-alpha.35",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,5 +1,5 @@
import { createAPIClient } from "@budibase/frontend-core" import { createAPIClient } from "@budibase/frontend-core"
import { notificationStore, authStore } from "../stores" import { notificationStore, authStore, devToolsStore } from "../stores"
import { get } from "svelte/store" import { get } from "svelte/store"
export const API = createAPIClient({ export const API = createAPIClient({
@ -21,6 +21,12 @@ export const API = createAPIClient({
if (auth?.csrfToken) { if (auth?.csrfToken) {
headers["x-csrf-token"] = auth.csrfToken headers["x-csrf-token"] = auth.csrfToken
} }
// Add role header
const role = get(devToolsStore).role
if (role) {
headers["x-budibase-role"] = role
}
}, },
// Show an error notification for all API failures. // Show an error notification for all API failures.

View File

@ -14,6 +14,8 @@
routeStore, routeStore,
builderStore, builderStore,
themeStore, themeStore,
appStore,
devToolsStore,
} from "stores" } from "stores"
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte" import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte" import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
@ -28,6 +30,8 @@
import CustomThemeWrapper from "./CustomThemeWrapper.svelte" import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
import DNDHandler from "components/preview/DNDHandler.svelte" import DNDHandler from "components/preview/DNDHandler.svelte"
import KeyboardManager from "components/preview/KeyboardManager.svelte" import KeyboardManager from "components/preview/KeyboardManager.svelte"
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
import DevTools from "components/devtools/DevTools.svelte"
// Provide contexts // Provide contexts
setContext("sdk", SDK) setContext("sdk", SDK)
@ -55,8 +59,22 @@
if ($authStore) { if ($authStore) {
// There is a logged in user, so handle them // There is a logged in user, so handle them
if ($screenStore.screens.length) { if ($screenStore.screens.length) {
let firstRoute
// If using devtools, find the first screen matching our role
if ($devToolsStore.role) {
const roleRoutes = $screenStore.screens.filter(
screen => screen.routing?.roleId === $devToolsStore.role
)
firstRoute = roleRoutes[0]?.routing?.route || "/"
}
// Otherwise just use the first route
else {
firstRoute = $screenStore.screens[0]?.routing?.route ?? "/"
}
// Screens exist so navigate back to the home screen // Screens exist so navigate back to the home screen
const firstRoute = $screenStore.screens[0].routing?.route ?? "/"
routeStore.actions.navigate(firstRoute) routeStore.actions.navigate(firstRoute)
} else { } else {
// No screens likely means the user has no permissions to view this app // No screens likely means the user has no permissions to view this app
@ -70,6 +88,8 @@
} }
} }
} }
$: isDevPreview = $appStore.isDevApp && !$builderStore.inBuilder
</script> </script>
{#if dataLoaded} {#if dataLoaded}
@ -109,6 +129,11 @@
> >
<!-- Actual app --> <!-- Actual app -->
<div id="app-root"> <div id="app-root">
{#if isDevPreview}
<DevToolsHeader />
{/if}
<div id="app-body">
<CustomThemeWrapper> <CustomThemeWrapper>
{#key `${$screenStore.activeLayout._id}-${$builderStore.previewType}`} {#key `${$screenStore.activeLayout._id}-${$builderStore.previewType}`}
<Component <Component
@ -132,16 +157,21 @@
<ConfirmationDisplay /> <ConfirmationDisplay />
<PeekScreenDisplay /> <PeekScreenDisplay />
</CustomThemeWrapper> </CustomThemeWrapper>
{#if $appStore.isDevApp && !$builderStore.inBuilder}
<DevTools />
{/if}
</div>
</div> </div>
<!-- Selection indicators should be bounded by device --> <!-- Preview and dev tools utilities -->
<!-- {#if $appStore.isDevApp}
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SelectionIndicator /> <SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator /> <HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler /> <DNDHandler />
{/if} {/if}
</div> </div>
@ -167,6 +197,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
#clip-root { #clip-root {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
@ -176,10 +207,24 @@
overflow: hidden; overflow: hidden;
background-color: transparent; background-color: transparent;
} }
#app-root { #app-root {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
#app-body {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
overflow: hidden;
} }
.error { .error {
@ -192,19 +237,23 @@
text-align: center; text-align: center;
padding: 20px; padding: 20px;
} }
.error :global(svg) { .error :global(svg) {
fill: var(--spectrum-global-color-gray-500); fill: var(--spectrum-global-color-gray-500);
width: 80px; width: 80px;
height: 80px; height: 80px;
} }
.error :global(h1), .error :global(h1),
.error :global(p) { .error :global(p) {
color: var(--spectrum-global-color-gray-800); color: var(--spectrum-global-color-gray-800);
} }
.error :global(p) { .error :global(p) {
font-style: italic; font-style: italic;
margin-top: -0.5em; margin-top: -0.5em;
} }
.error :global(h1) { .error :global(h1) {
font-weight: 400; font-weight: 400;
} }
@ -214,14 +263,17 @@
#clip-root.preview { #clip-root.preview {
padding: 2px; padding: 2px;
} }
#clip-root.tablet-preview { #clip-root.tablet-preview {
width: calc(1024px + 6px); width: calc(1024px + 6px);
height: calc(768px + 6px); height: calc(768px + 6px);
} }
#clip-root.mobile-preview { #clip-root.mobile-preview {
width: calc(390px + 6px); width: calc(390px + 6px);
height: calc(844px + 6px); height: calc(844px + 6px);
} }
.preview #app-root { .preview #app-root {
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px; border-radius: 4px;

View File

@ -9,12 +9,16 @@
</script> </script>
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext, onMount, onDestroy } from "svelte"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import * as AppComponents from "components/app" import * as AppComponents from "components/app"
import Router from "./Router.svelte" import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "utils/componentProps" import {
import { builderStore } from "stores" enrichProps,
propsAreSame,
getSettingsDefinition,
} from "utils/componentProps"
import { builderStore, devToolsStore, componentStore, appStore } from "stores"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import Manifest from "manifest.json" import Manifest from "manifest.json"
import { getActiveConditions, reduceConditionActions } from "utils/conditions" import { getActiveConditions, reduceConditionActions } from "utils/conditions"
@ -30,8 +34,8 @@
const insideScreenslot = !!getContext("screenslot") const insideScreenslot = !!getContext("screenslot")
// Create component context // Create component context
const componentStore = writable({}) const store = writable({})
setContext("component", componentStore) setContext("component", store)
// Ref to the svelte component // Ref to the svelte component
let ref let ref
@ -90,7 +94,7 @@
// leading to the selected component // leading to the selected component
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.selectedComponentId === id $builderStore.inBuilder && $builderStore.selectedComponentId === id
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id)
$: inDragPath = inSelectedPath && $builderStore.editMode $: inDragPath = inSelectedPath && $builderStore.editMode
// Derive definition properties which can all be optional, so need to be // Derive definition properties which can all be optional, so need to be
@ -101,10 +105,12 @@
// Interactive components can be selected, dragged and highlighted inside // Interactive components can be selected, dragged and highlighted inside
// the builder preview // the builder preview
$: interactive = $: builderInteractive =
$builderStore.inBuilder && $builderStore.inBuilder &&
($builderStore.previewType === "layout" || insideScreenslot) && ($builderStore.previewType === "layout" || insideScreenslot) &&
!isBlock !isBlock
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
$: interactive = builderInteractive || devToolsInteractive
$: editing = editable && selected && $builderStore.editMode $: editing = editable && selected && $builderStore.editMode
$: draggable = $: draggable =
!inDragPath && !inDragPath &&
@ -133,7 +139,7 @@
$: applySettings(staticSettings, enrichedSettings, conditionalSettings) $: applySettings(staticSettings, enrichedSettings, conditionalSettings)
// Update component context // Update component context
$: componentStore.set({ $: store.set({
id, id,
children: children.length, children: children.length,
styles: { styles: {
@ -217,22 +223,6 @@
return type ? Manifest[type] : null return type ? Manifest[type] : null
} }
// Gets the definition of this component's settings from the manifest
const getSettingsDefinition = definition => {
if (!definition) {
return []
}
let settings = []
definition.settings?.forEach(setting => {
if (setting.section) {
settings = settings.concat(setting.settings || [])
} else {
settings.push(setting)
}
})
return settings
}
const getSettingsDefinitionMap = settingsDefinition => { const getSettingsDefinitionMap = settingsDefinition => {
let map = {} let map = {}
settingsDefinition?.forEach(setting => { settingsDefinition?.forEach(setting => {
@ -385,6 +375,28 @@
}) })
} }
} }
onMount(() => {
if (
$appStore.isDevApp &&
!componentStore.actions.isComponentRegistered(id)
) {
componentStore.actions.registerInstance(id, {
getSettings: () => cachedSettings,
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
getDataContext: () => get(context),
})
}
})
onDestroy(() => {
if (
$appStore.isDevApp &&
componentStore.actions.isComponentRegistered(id)
) {
componentStore.actions.unregisterInstance(id)
}
})
</script> </script>
{#if constructor && initialSettings && (visible || inSelectedPath)} {#if constructor && initialSettings && (visible || inSelectedPath)}
@ -419,12 +431,15 @@
.component { .component {
display: contents; display: contents;
} }
.interactive :global(*:hover) { .interactive :global(*:hover) {
cursor: pointer; cursor: pointer;
} }
.draggable :global(*:hover) { .draggable :global(*:hover) {
cursor: grab; cursor: grab;
} }
.editing :global(*:hover) { .editing :global(*:hover) {
cursor: auto; cursor: auto;
} }

View File

@ -179,6 +179,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
flex: 1 1 auto;
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;

View File

@ -102,7 +102,7 @@
white-space: nowrap; white-space: nowrap;
} }
.spectrum-Card-footer { .spectrum-Card-footer {
word-wrap: anywhere; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
.horizontal .spectrum-Card-coverPhoto { .horizontal .spectrum-Card-coverPhoto {

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