Merge remote-tracking branch 'origin/develop' into feature/add-default-screen
This commit is contained in:
commit
407dfc0dcb
95 changed files with 2477 additions and 408 deletions
1
.github/workflows/budibase_ci.yml
vendored
1
.github/workflows/budibase_ci.yml
vendored
|
@ -11,6 +11,7 @@ on:
|
|||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
|
|
2
.github/workflows/deploy-cloud.yaml
vendored
2
.github/workflows/deploy-cloud.yaml
vendored
|
@ -66,7 +66,7 @@ jobs:
|
|||
config-files: values.production.yaml
|
||||
chart-path: charts/budibase
|
||||
namespace: budibase
|
||||
values: globals.appVersion=v${{ env.RELEASE_VERSION }}
|
||||
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
|
||||
name: budibase-prod
|
||||
|
||||
- name: Discord Webhook Action
|
||||
|
|
6
.github/workflows/release-develop.yml
vendored
6
.github/workflows/release-develop.yml
vendored
|
@ -14,6 +14,7 @@ on:
|
|||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
|
@ -26,6 +27,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fail if branch is not develop
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
echo "Ref is not develop, you must run this job from develop."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
|
|
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
@ -14,6 +14,7 @@ on:
|
|||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
|
@ -27,6 +28,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fail if branch is not master
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
|
@ -53,8 +59,8 @@ jobs:
|
|||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||
git config user.name "Budibase Release Bot"
|
||||
git config user.email "<>"
|
||||
git config --global user.name "Budibase Release Bot"
|
||||
git config --global user.email "<>"
|
||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release
|
||||
|
||||
|
|
7
.github/workflows/smoke_test.yaml
vendored
7
.github/workflows/smoke_test.yaml
vendored
|
@ -31,8 +31,11 @@ jobs:
|
|||
continue-on-error: true
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
record: true
|
||||
install: false
|
||||
command: yarn test:e2e:ci
|
||||
command: yarn test:e2e:ci:record
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
# TODO: upload recordings to s3
|
||||
# - name: Configure AWS Credentials
|
||||
|
@ -46,7 +49,7 @@ jobs:
|
|||
uses: tsickert/discord-webhook@v4.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
|
||||
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.dashboardUrl }}"
|
||||
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.outputs.dashboardUrl }}"
|
||||
embed-title: ${{ steps.cypress.outcome }}
|
||||
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ spec:
|
|||
{{ else }}
|
||||
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}
|
||||
{{ end }}
|
||||
{{ if .Values.services.couchdb.enabled }}
|
||||
- name: COUCH_DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -44,6 +45,7 @@ spec:
|
|||
secretKeyRef:
|
||||
name: {{ template "couchdb.fullname" . }}
|
||||
key: adminPassword
|
||||
{{ end }}
|
||||
- name: ENABLE_ANALYTICS
|
||||
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||
- name: INTERNAL_API_KEY
|
||||
|
@ -112,6 +114,8 @@ spec:
|
|||
value: {{ .Values.globals.google.secret | quote }}
|
||||
- name: AUTOMATION_MAX_ITERATIONS
|
||||
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||
- name: TENANT_FEATURE_FLAGS
|
||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
|
||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||
imagePullPolicy: Always
|
||||
|
|
|
@ -12,10 +12,8 @@ spec:
|
|||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.services.objectStore.storage }}
|
||||
{{- if (eq "-" .Values.services.objectStore.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.services.objectStore.storageClass }}"
|
||||
{{ if .Values.services.objectStore.storageClass }}
|
||||
storageClassName: {{ .Values.services.objectStore.storageClass }}
|
||||
{{- end }}
|
||||
status: {}
|
||||
{{- end }}
|
||||
|
|
|
@ -12,10 +12,8 @@ spec:
|
|||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.services.redis.storage }}
|
||||
{{- if (eq "-" .Values.services.redis.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.services.redis.storageClass }}"
|
||||
{{- end }}
|
||||
{{ if .Values.services.redis.storageClass }}
|
||||
storageClassName: {{ .Values.services.redis.storageClass }}
|
||||
{{ end }}
|
||||
status: {}
|
||||
{{- end }}
|
||||
|
|
|
@ -29,6 +29,7 @@ spec:
|
|||
- env:
|
||||
- name: CLUSTER_PORT
|
||||
value: {{ .Values.services.worker.port | quote }}
|
||||
{{ if .Values.services.couchdb.enabled }}
|
||||
- name: COUCH_DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -39,6 +40,7 @@ spec:
|
|||
secretKeyRef:
|
||||
name: {{ template "couchdb.fullname" . }}
|
||||
key: adminPassword
|
||||
{{ end }}
|
||||
- name: COUCH_DB_URL
|
||||
{{ if .Values.services.couchdb.url }}
|
||||
value: {{ .Values.services.couchdb.url }}
|
||||
|
|
|
@ -155,7 +155,7 @@ services:
|
|||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner.
|
||||
storageClass: "-"
|
||||
storageClass: ""
|
||||
|
||||
objectStore:
|
||||
minio: true
|
||||
|
@ -171,7 +171,7 @@ services:
|
|||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner.
|
||||
storageClass: "-"
|
||||
storageClass: ""
|
||||
|
||||
# Override values in couchDB subchart
|
||||
couchdb:
|
||||
|
@ -215,7 +215,7 @@ couchdb:
|
|||
## The CouchDB image
|
||||
image:
|
||||
repository: couchdb
|
||||
tag: 3.1.0
|
||||
tag: 3.2.1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Experimental integration with Lucene-powered fulltext search
|
||||
|
|
|
@ -30,7 +30,7 @@ http {
|
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default "upgrade";
|
||||
}
|
||||
|
@ -42,13 +42,13 @@ http {
|
|||
client_max_body_size 1000m;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
|
||||
|
||||
set $csp_default "default-src 'self'";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
|
||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||
set $csp_object "object-src 'none'";
|
||||
set $csp_base_uri "base-uri 'self'";
|
||||
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com";
|
||||
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com";
|
||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||
set $csp_frame "frame-src 'self' https:";
|
||||
set $csp_img "img-src http: https: data: blob:";
|
||||
|
@ -58,7 +58,7 @@ http {
|
|||
|
||||
error_page 502 503 504 /error.html;
|
||||
location = /error.html {
|
||||
root /usr/share/nginx/html;
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
|
||||
|
@ -154,4 +154,4 @@ http {
|
|||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"test:e2e": "lerna run cy:test --stream",
|
||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||
"test:e2e:ci:record": "lerna run cy:ci:record --stream",
|
||||
"build:specs": "lerna run specs",
|
||||
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -23,24 +23,30 @@ exports.isDevApp = app => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a development app ID to a deployed app ID.
|
||||
* Generates a development app ID from a real app ID.
|
||||
* @returns {string} the dev app ID which can be used for dev database.
|
||||
*/
|
||||
exports.getProdAppID = appId => {
|
||||
// if dev, convert it
|
||||
if (appId.startsWith(APP_DEV_PREFIX)) {
|
||||
const id = appId.split(APP_DEV_PREFIX)[1]
|
||||
return `${APP_PREFIX}${id}`
|
||||
exports.getDevelopmentAppID = appId => {
|
||||
if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
|
||||
return appId
|
||||
}
|
||||
return appId
|
||||
// split to take off the app_ element, then join it together incase any other app_ exist
|
||||
const split = appId.split(APP_PREFIX)
|
||||
split.shift()
|
||||
const rest = split.join(APP_PREFIX)
|
||||
return `${APP_DEV_PREFIX}${rest}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a deployed app ID to a development app ID.
|
||||
* Convert a development app ID to a deployed app ID.
|
||||
*/
|
||||
exports.getDevelopmentAppID = appId => {
|
||||
if (!appId.startsWith(APP_DEV_PREFIX)) {
|
||||
const id = appId.split(APP_PREFIX)[1]
|
||||
return `${APP_DEV_PREFIX}${id}`
|
||||
exports.getProdAppID = appId => {
|
||||
if (!appId || !appId.startsWith(APP_DEV_PREFIX)) {
|
||||
return appId
|
||||
}
|
||||
return appId
|
||||
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||
const split = appId.split(APP_DEV_PREFIX)
|
||||
split.shift()
|
||||
const rest = split.join(APP_DEV_PREFIX)
|
||||
return `${APP_PREFIX}${rest}`
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@ exports.closeDB = async db => {
|
|||
return
|
||||
}
|
||||
try {
|
||||
return db.close()
|
||||
// specifically await so that if there is an error, it can be ignored
|
||||
return await db.close()
|
||||
} catch (err) {
|
||||
// ignore error, already closed
|
||||
}
|
||||
|
|
|
@ -1,29 +1,19 @@
|
|||
const PouchDB = require("pouchdb")
|
||||
const env = require("../environment")
|
||||
|
||||
exports.getCouchUrl = () => {
|
||||
if (!env.COUCH_DB_URL) return
|
||||
|
||||
// username and password already exist in URL
|
||||
if (env.COUCH_DB_URL.includes("@")) {
|
||||
return env.COUCH_DB_URL
|
||||
}
|
||||
|
||||
const [protocol, ...rest] = env.COUCH_DB_URL.split("://")
|
||||
|
||||
if (!env.COUCH_DB_USERNAME || !env.COUCH_DB_PASSWORD) {
|
||||
throw new Error(
|
||||
"CouchDB configuration invalid. You must provide a fully qualified CouchDB url, or the COUCH_DB_USER and COUCH_DB_PASSWORD environment variables."
|
||||
)
|
||||
}
|
||||
|
||||
return `${protocol}://${env.COUCH_DB_USERNAME}:${env.COUCH_DB_PASSWORD}@${rest}`
|
||||
}
|
||||
|
||||
exports.splitCouchUrl = url => {
|
||||
function getUrlInfo() {
|
||||
let url = env.COUCH_DB_URL
|
||||
let username, password, host
|
||||
const [protocol, rest] = url.split("://")
|
||||
const [auth, host] = rest.split("@")
|
||||
const [username, password] = auth.split(":")
|
||||
if (url.includes("@")) {
|
||||
const hostParts = rest.split("@")
|
||||
host = hostParts[1]
|
||||
const authParts = hostParts[0].split(":")
|
||||
username = authParts[0]
|
||||
password = authParts[1]
|
||||
} else {
|
||||
host = rest
|
||||
}
|
||||
return {
|
||||
url: `${protocol}://${host}`,
|
||||
auth: {
|
||||
|
@ -33,32 +23,51 @@ exports.splitCouchUrl = url => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.getCouchInfo = () => {
|
||||
const urlInfo = getUrlInfo()
|
||||
let username
|
||||
let password
|
||||
if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (urlInfo.auth.username) {
|
||||
// set from url
|
||||
username = urlInfo.auth.username
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB username not set")
|
||||
}
|
||||
if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (urlInfo.auth.password) {
|
||||
// set from url
|
||||
password = urlInfo.auth.password
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB password not set")
|
||||
}
|
||||
const authCookie = Buffer.from(`${username}:${password}`).toString("base64")
|
||||
return {
|
||||
url: urlInfo.url,
|
||||
auth: {
|
||||
username: username,
|
||||
password: password,
|
||||
},
|
||||
cookie: `Basic ${authCookie}`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a constructor for PouchDB.
|
||||
* This should be rarely used outside of the main application config.
|
||||
* Exposed for exceptional cases such as in-memory views.
|
||||
*/
|
||||
exports.getPouch = (opts = {}) => {
|
||||
let auth = {
|
||||
username: env.COUCH_DB_USERNAME,
|
||||
password: env.COUCH_DB_PASSWORD,
|
||||
}
|
||||
let url = exports.getCouchUrl() || "http://localhost:4005"
|
||||
// need to update security settings
|
||||
if (!auth.username || !auth.password || url.includes("@")) {
|
||||
const split = exports.splitCouchUrl(url)
|
||||
url = split.url
|
||||
auth = split.auth
|
||||
}
|
||||
|
||||
const authCookie = Buffer.from(`${auth.username}:${auth.password}`).toString(
|
||||
"base64"
|
||||
)
|
||||
let { url, cookie } = exports.getCouchInfo()
|
||||
let POUCH_DB_DEFAULTS = {
|
||||
prefix: url,
|
||||
fetch: (url, opts) => {
|
||||
// use a specific authorization cookie - be very explicit about how we authenticate
|
||||
opts.headers.set("Authorization", `Basic ${authCookie}`)
|
||||
opts.headers.set("Authorization", cookie)
|
||||
return PouchDB.fetch(url, opts)
|
||||
},
|
||||
}
|
||||
|
|
61
packages/backend-core/src/db/tests/utils.spec.js
Normal file
61
packages/backend-core/src/db/tests/utils.spec.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
const {
|
||||
generateAppID,
|
||||
getDevelopmentAppID,
|
||||
getProdAppID,
|
||||
isDevAppID,
|
||||
isProdAppID,
|
||||
} = require("../utils")
|
||||
|
||||
function getID() {
|
||||
const appId = generateAppID()
|
||||
const split = appId.split("_")
|
||||
const uuid = split[split.length - 1]
|
||||
const devAppId = `app_dev_${uuid}`
|
||||
return { appId, devAppId, split, uuid }
|
||||
}
|
||||
|
||||
describe("app ID manipulation", () => {
|
||||
it("should be able to generate a new app ID", () => {
|
||||
expect(generateAppID().startsWith("app_")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should be able to convert a production app ID to development", () => {
|
||||
const { appId, uuid } = getID()
|
||||
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`)
|
||||
})
|
||||
|
||||
it("should be able to convert a development app ID to development", () => {
|
||||
const { devAppId, uuid } = getID()
|
||||
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`)
|
||||
})
|
||||
|
||||
it("should be able to convert a development ID to a production", () => {
|
||||
const { devAppId, uuid } = getID()
|
||||
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`)
|
||||
})
|
||||
|
||||
it("should be able to convert a production ID to production", () => {
|
||||
const { appId, uuid } = getID()
|
||||
expect(getProdAppID(appId)).toEqual(`app_${uuid}`)
|
||||
})
|
||||
|
||||
it("should be able to confirm dev app ID is development", () => {
|
||||
const { devAppId } = getID()
|
||||
expect(isDevAppID(devAppId)).toEqual(true)
|
||||
})
|
||||
|
||||
it("should be able to confirm prod app ID is not development", () => {
|
||||
const { appId } = getID()
|
||||
expect(isDevAppID(appId)).toEqual(false)
|
||||
})
|
||||
|
||||
it("should be able to confirm prod app ID is prod", () => {
|
||||
const { appId } = getID()
|
||||
expect(isProdAppID(appId)).toEqual(true)
|
||||
})
|
||||
|
||||
it("should be able to confirm dev app ID is not prod", () => {
|
||||
const { devAppId } = getID()
|
||||
expect(isProdAppID(devAppId)).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -12,7 +12,7 @@ const {
|
|||
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
||||
const fetch = require("node-fetch")
|
||||
const { doWithDB, allDbs } = require("./index")
|
||||
const { getCouchUrl } = require("./pouch")
|
||||
const { getCouchInfo } = require("./pouch")
|
||||
const { getAppMetadata } = require("../cache/appMetadata")
|
||||
const { checkSlashesInUrl } = require("../helpers")
|
||||
const {
|
||||
|
@ -43,6 +43,18 @@ exports.isDevAppID = isDevAppID
|
|||
exports.getDevelopmentAppID = getDevelopmentAppID
|
||||
exports.getProdAppID = getProdAppID
|
||||
|
||||
/**
|
||||
* Generates a new app ID.
|
||||
* @returns {string} The new app ID which the app doc can be stored under.
|
||||
*/
|
||||
exports.generateAppID = (tenantId = null) => {
|
||||
let id = APP_PREFIX
|
||||
if (tenantId) {
|
||||
id += `${tenantId}${SEPARATOR}`
|
||||
}
|
||||
return `${id}${newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||
|
@ -169,8 +181,14 @@ exports.getAllDbs = async (opts = { efficient: false }) => {
|
|||
return allDbs()
|
||||
}
|
||||
let dbs = []
|
||||
async function addDbs(url) {
|
||||
const response = await fetch(checkSlashesInUrl(encodeURI(url)))
|
||||
let { url, cookie } = getCouchInfo()
|
||||
async function addDbs(couchUrl) {
|
||||
const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: cookie,
|
||||
},
|
||||
})
|
||||
if (response.status === 200) {
|
||||
let json = await response.json()
|
||||
dbs = dbs.concat(json)
|
||||
|
@ -178,7 +196,7 @@ exports.getAllDbs = async (opts = { efficient: false }) => {
|
|||
throw "Cannot connect to CouchDB instance"
|
||||
}
|
||||
}
|
||||
let couchUrl = `${getCouchUrl()}/_all_dbs`
|
||||
let couchUrl = `${url}/_all_dbs`
|
||||
let tenantId = getTenantId()
|
||||
if (!env.MULTI_TENANCY || (!efficient && tenantId === DEFAULT_TENANT_ID)) {
|
||||
// just get all DBs when:
|
||||
|
|
|
@ -6,9 +6,13 @@ function isTest() {
|
|||
)
|
||||
}
|
||||
|
||||
function isDev() {
|
||||
return process.env.NODE_ENV !== "production"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
@ -30,8 +34,15 @@ module.exports = {
|
|||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
PLATFORM_URL: process.env.PLATFORM_URL,
|
||||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||
BACKUPS_BUCKET_NAME: process.env.BACKUPS_BUCKET_NAME || "backups",
|
||||
APPS_BUCKET_NAME: process.env.APPS_BUCKET_NAME || "prod-budi-app-assets",
|
||||
TEMPLATES_BUCKET_NAME: process.env.TEMPLATES_BUCKET_NAME || "templates",
|
||||
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global",
|
||||
GLOBAL_CLOUD_BUCKET_NAME:
|
||||
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
|
||||
USE_COUCH: process.env.USE_COUCH || true,
|
||||
isTest,
|
||||
isDev,
|
||||
_set(key, value) {
|
||||
process.env[key] = value
|
||||
module.exports[key] = value
|
||||
|
|
|
@ -49,4 +49,5 @@ exports.getTenantFeatureFlags = tenantId => {
|
|||
|
||||
exports.FeatureFlag = {
|
||||
LICENSING: "LICENSING",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const { join } = require("path")
|
||||
const { tmpdir } = require("os")
|
||||
const env = require("../environment")
|
||||
|
||||
exports.ObjectStoreBuckets = {
|
||||
BACKUPS: "backups",
|
||||
APPS: "prod-budi-app-assets",
|
||||
TEMPLATES: "templates",
|
||||
GLOBAL: "global",
|
||||
GLOBAL_CLOUD: "prod-budi-tenant-uploads",
|
||||
BACKUPS: env.BACKUPS_BUCKET_NAME,
|
||||
APPS: env.APPS_BUCKET_NAME,
|
||||
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
||||
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
||||
GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME,
|
||||
}
|
||||
|
||||
exports.budibaseTempDir = function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.148-alpha.0",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -2,17 +2,22 @@
|
|||
import dayjs from "dayjs"
|
||||
|
||||
export let value
|
||||
export let schema
|
||||
|
||||
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||
// date, but will make actual ISO dates invalid
|
||||
$: time = new Date(`0-${value}`)
|
||||
$: isTime = !isNaN(time)
|
||||
$: isTimeOnly = !isNaN(time) || schema?.timeOnly
|
||||
$: isDateOnly = schema?.dateOnly
|
||||
$: format = isTimeOnly
|
||||
? "HH:mm:ss"
|
||||
: isDateOnly
|
||||
? "MMMM D YYYY"
|
||||
: "MMMM D YYYY, HH:mm"
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{dayjs(isTime ? time : value).format(
|
||||
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
|
||||
)}
|
||||
{dayjs(isTimeOnly ? time : value).format(format)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -66,13 +66,13 @@ filterTests(["smoke", "all"], () => {
|
|||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
cy.get(".spectrum-Menu").contains("Admin").click({ force: true })
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
else if (i == 1) {
|
||||
cy.get(".spectrum-Menu").contains("Power").click({ force: true })
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
else if (i == 2) {
|
||||
cy.get(".spectrum-Menu").contains("Basic").click({ force: true })
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
|
|
|
@ -207,7 +207,7 @@ filterTests(["all"], () => {
|
|||
.contains(queryName)
|
||||
.siblings(".actions")
|
||||
.within(() => {
|
||||
cy.get(".icon").click({ force: true })
|
||||
cy.get(".spectrum-Icon").click({ force: true })
|
||||
})
|
||||
// Select and confirm duplication
|
||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify HR Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter HR Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="HR"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for HR templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (templateNameText == "Job Application Tracker") {
|
||||
// Template name should include 'applicant-tracking-system'
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', 'applicant-tracking-system')
|
||||
}
|
||||
else if (templateNameText == "Job Portal App") {
|
||||
// Template name should include 'job-portal'
|
||||
const templateNameSplit = templateNameParsed.split('-app')[0]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
}
|
||||
else {
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
}
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,239 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Job Application Functionality", () => {
|
||||
const templateName = "Job Application Tracker"
|
||||
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.deleteApp(templateName)
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, 'open')
|
||||
}
|
||||
})
|
||||
cy.wait(2000)
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should create and publish app with Job Application Tracker template", () => {
|
||||
// Select Job Application Tracker template
|
||||
cy.get(".template-thumbnail-text")
|
||||
.contains(templateName).parentsUntil(".template-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Use template").click({ force: true })
|
||||
})
|
||||
|
||||
// Confirm URL matches template name
|
||||
const appUrl = cy.get(".app-server")
|
||||
appUrl.invoke('text').then(appUrlText => {
|
||||
expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
})
|
||||
|
||||
// Create App
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||
})
|
||||
|
||||
// Publish App
|
||||
cy.wait(2000) // Wait for app to generate
|
||||
cy.get(".toprightnav").contains("Publish").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||
})
|
||||
|
||||
// Verify Published app
|
||||
cy.wait(2000) // Wait for App to publish and modal to appear
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("View App").click({ force: true })
|
||||
cy.window().its('open').should('be.calledOnce')
|
||||
})
|
||||
})
|
||||
|
||||
it("should add active/inactive vacancies", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
|
||||
// loop for active/inactive vacancies
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// Vacancies section
|
||||
cy.get(".links").contains("Vacancies").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Create New").click()
|
||||
|
||||
// Add inactive vacancy
|
||||
// Title
|
||||
cy.get('[data-name="Title"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Tester")
|
||||
})
|
||||
|
||||
// Closing Date
|
||||
cy.get('[data-name="Closing date"]').within(() => {
|
||||
cy.get('[aria-label=Calendar]').click({ force: true })
|
||||
})
|
||||
cy.get("[aria-current=date]").click()
|
||||
|
||||
// Department
|
||||
cy.get('[data-name="Department"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Employment Type
|
||||
cy.get('[data-name="Employment type"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Salary
|
||||
cy.get('[data-name="Salary ($)"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type(40000)
|
||||
})
|
||||
|
||||
// Description
|
||||
cy.get('[data-name="Description"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("description")
|
||||
})
|
||||
|
||||
// Responsibilities
|
||||
cy.get('[data-name="Responsibilities"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Responsibilities")
|
||||
})
|
||||
|
||||
// Requirements
|
||||
cy.get('[data-name="Requirements"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Requirements")
|
||||
})
|
||||
|
||||
// Hiring manager
|
||||
cy.get('[data-name="Hiring manager"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Active
|
||||
if (i == 0) {
|
||||
cy.get('[data-name="Active"]').within(() => {
|
||||
cy.get(".spectrum-Checkbox-box").click({ force: true })
|
||||
})
|
||||
}
|
||||
|
||||
// Location
|
||||
cy.get('[data-name="Location"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Save vacancy
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
||||
// Check table was updated
|
||||
cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester')
|
||||
}
|
||||
})
|
||||
|
||||
xit("should filter applications by stage", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Applications section
|
||||
cy.get(".links").contains("Applications").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
||||
// Filter by stage - Confirm table updates
|
||||
cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true })
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
for (let i = 1; i < len; i++) {
|
||||
cy.get(".spectrum-Menu-item").eq(i).click()
|
||||
const stage = cy.get(".spectrum-Picker-label")
|
||||
stage.invoke('text').then(stageText => {
|
||||
if (stageText == "1st interview") {
|
||||
cy.get(".placeholder").should('contain', 'No rows found')
|
||||
}
|
||||
else {
|
||||
cy.get(".spectrum-Table-row").should('contain', stageText)
|
||||
}
|
||||
cy.get(".spectrum-Picker").contains(stageText).click({ force: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
xit("should edit an application", () => {
|
||||
// Switch application from not hired to hired
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Not Hired section
|
||||
cy.get(".links").contains("Not hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// View application
|
||||
cy.get(".spectrum-Table").within(() => {
|
||||
cy.get(".spectrum-Button").contains("View").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
|
||||
// Update value for 'Staged'
|
||||
cy.get('[data-name="Stage"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item").contains("Hired").click()
|
||||
})
|
||||
|
||||
// Save application
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// Hired section
|
||||
cy.get(".links").contains("Hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// Verify Table size - Total rows = 2
|
||||
cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => {
|
||||
expect(len).to.eq(2)
|
||||
}))
|
||||
})
|
||||
|
||||
xit("should delete an application", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Hired section
|
||||
cy.get(".links").contains("Hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// View first application
|
||||
cy.get(".spectrum-Table-row").eq(0).within(() => {
|
||||
cy.get(".spectrum-Button").contains("View").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
|
||||
// Delete application
|
||||
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Confirm").click()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,66 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify IT Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter IT Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="IT"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for IT templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (templateNameText == "Hashicorp Scorecard Template") {
|
||||
const templateNameSplit = templateNameParsed.split('-template')[0]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
}
|
||||
else if (templateNameText == "IT Ticketing System") {
|
||||
const templateNameSplit = templateNameParsed.split('it-')[1]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
}
|
||||
else if (templateNameText == "IT Incident Report Form") {
|
||||
const templateNameSplit = templateNameParsed.split('-form')[0]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
}
|
||||
else {
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
}
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Admin Panel Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Admin Panels Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Admin Panels"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Admin Panels templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,57 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Aproval Apps Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Approval Apps Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Approval Apps"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Approval Apps templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (templateNameText == "Content Approval System") {
|
||||
// Template name should include 'content-approval'
|
||||
const templateNameSplit = templateNameParsed.split('-system')[0]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
}
|
||||
else {
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
}
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,57 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Business Apps Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Business Apps Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Business Apps"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Business Apps templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (templateNameText == "Employee Check-in/Check-Out Template") {
|
||||
// Remove / from template name
|
||||
const templateNameReplace = templateNameParsed.replace(/\//g, "-")
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameReplace)
|
||||
}
|
||||
else {
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
}
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,50 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Directories Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Directories Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Directories"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Directories templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
const templateNameSplit = templateNameParsed.split('-template')[0]
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameSplit)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Forms Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Forms Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Forms"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Forms templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Healthcare Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Healthcare Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Healthcare"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Healthcare templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Legal Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Legal Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Legal"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Legal templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Logistics Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Logistics Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Logistics"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Logistics templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Manufacturing Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Manufacturing Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Manufacturing"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Manufacturing templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,57 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Marketing Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Marketing Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Marketing"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Marketing templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
if (templateNameText == "Lead Generation Form") {
|
||||
// Multi-step lead form
|
||||
// Template name includes 'multi-step-lead-form'
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', 'multi-step-lead-form')
|
||||
}
|
||||
else {
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
}
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Operations Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Operations Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Operations"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Operations templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,77 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Portals Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Portal templates", () => {
|
||||
// Filter Portal Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Portal"]').click()
|
||||
})
|
||||
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Portals templates", () => {
|
||||
// Filter Portals Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Portals"]').click()
|
||||
})
|
||||
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
import filterTests from "../../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Verify Professional Services Template Details", () => {
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
||||
// Template navigation
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
// Filter Professional Services Templates
|
||||
cy.get(".template-category-filters").within(() => {
|
||||
cy.get('[data-cy="Professional Services"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("should verify the details option for Professional Services templates", () => {
|
||||
cy.get(".template-grid").find(".template-card").its('length')
|
||||
.then(len => {
|
||||
// Verify template name is within details link
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".template-card").eq(i).within(() => {
|
||||
const templateName = cy.get(".template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-')
|
||||
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed)
|
||||
})
|
||||
// Verify correct status from Details link - 200
|
||||
cy.get('a')
|
||||
.then(link => {
|
||||
cy.request(link.prop('href'))
|
||||
.its('status')
|
||||
.should('eq', 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -44,7 +44,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
|||
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.wait(1000)
|
||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||
|
||||
// If apps already exist
|
||||
|
@ -77,40 +77,38 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
if (val.length > 0) {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
} else {
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
}
|
||||
return acc
|
||||
}, "")
|
||||
|
||||
if (appId == "") {
|
||||
return
|
||||
}
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
}
|
||||
return acc
|
||||
}, "")
|
||||
|
||||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
if (appId == "") {
|
||||
return
|
||||
}
|
||||
|
||||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").then($menu => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
} else {
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
})
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
}
|
||||
})
|
||||
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
})
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -14,8 +14,10 @@
|
|||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome",
|
||||
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
|
||||
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
|
||||
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
|
||||
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record",
|
||||
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
|
||||
"cy:debug:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:open"
|
||||
},
|
||||
|
@ -65,10 +67,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.124-alpha.0",
|
||||
"@budibase/client": "^1.0.124-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.124-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||
"@budibase/bbui": "^1.0.148-alpha.0",
|
||||
"@budibase/client": "^1.0.148-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.148-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.148-alpha.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -53,6 +53,18 @@
|
|||
x => x.blockToLoop === block.id
|
||||
)
|
||||
|
||||
async function removeLooping() {
|
||||
loopingSelected = false
|
||||
let loopBlock =
|
||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)
|
||||
automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteStep() {
|
||||
let loopBlock =
|
||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||
|
@ -151,9 +163,7 @@
|
|||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<div class="block-options">
|
||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||
<Icon name="DeleteOutline" />
|
||||
</div>
|
||||
<ActionButton on:click={() => removeLooping()} icon="DeleteOutline" />
|
||||
</div>
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
|
|
|
@ -182,7 +182,11 @@
|
|||
<div class="fields">
|
||||
{#each schemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
<Label>{value.title || (key === "row" ? "Table" : key)}</Label>
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{#if value.type === "string" && value.enum}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
|
@ -265,6 +269,7 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let bindings
|
||||
export let block
|
||||
export let isTestModal
|
||||
|
||||
let table
|
||||
let schemaFields
|
||||
|
@ -103,35 +104,18 @@
|
|||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
{#if schema.type !== "attachment"}
|
||||
{#if $automationStore.selectedAutomation.automation.testData}
|
||||
{#if !rowControl}
|
||||
<RowSelectorTypes
|
||||
{field}
|
||||
{schema}
|
||||
{bindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
placeholder={placeholders[schema.type]}
|
||||
panel={AutomationBindingPanel}
|
||||
value={Array.isArray(value[field])
|
||||
? value[field].join(" ")
|
||||
: value[field]}
|
||||
on:change={e => onChange(e, field, schema.type)}
|
||||
label={field}
|
||||
type="string"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
{:else if !rowControl}
|
||||
<RowSelectorTypes {field} {schema} {bindings} {value} {onChange} />
|
||||
{#if !rowControl}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
{bindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
placeholder={placeholders[schema.type]}
|
||||
panel={AutomationBindingPanel}
|
||||
value={Array.isArray(value[field])
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
||||
export let onChange
|
||||
|
@ -15,6 +16,7 @@
|
|||
export let schema
|
||||
export let value
|
||||
export let bindings
|
||||
export let isTestModal
|
||||
|
||||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
|
@ -51,7 +53,8 @@
|
|||
{:else if schema.type === "link"}
|
||||
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
<DrawerBindableInput
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
panel={AutomationBindingPanel}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
<TableFilterButton
|
||||
{schema}
|
||||
on:change={onFilter}
|
||||
disabled={!hasCols || !hasRows}
|
||||
disabled={!hasCols}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
export let readonly
|
||||
|
||||
const resolveTimeStamp = timestamp => {
|
||||
if (!timestamp) {
|
||||
return null
|
||||
}
|
||||
let maskedDate = new Date(`0-${timestamp}`)
|
||||
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||
return maskedDate
|
||||
|
@ -34,7 +37,7 @@
|
|||
$: label = meta.name ? capitalise(meta.name) : ""
|
||||
|
||||
const timeStamp = resolveTimeStamp(value)
|
||||
const isTimeStamp = !!timeStamp
|
||||
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
||||
</script>
|
||||
|
||||
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||
|
@ -46,7 +49,12 @@
|
|||
sort
|
||||
/>
|
||||
{:else if type === "datetime"}
|
||||
<DatePicker {label} timeOnly={isTimeStamp} bind:value />
|
||||
<DatePicker
|
||||
{label}
|
||||
timeOnly={isTimeStamp}
|
||||
enableTime={!meta?.dateOnly}
|
||||
bind:value
|
||||
/>
|
||||
{:else if type === "attachment"}
|
||||
<Dropzone {label} bind:value />
|
||||
{:else if type === "boolean"}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import GoogleLogo from "assets/google-logo.png"
|
||||
import { store } from "builderStore"
|
||||
import { auth } from "stores/portal"
|
||||
|
@ -10,7 +9,7 @@
|
|||
$: tenantId = $auth.tenantId
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
<button
|
||||
on:click={async () => {
|
||||
let ds = datasource
|
||||
if (!ds) {
|
||||
|
@ -22,26 +21,32 @@
|
|||
)
|
||||
}}
|
||||
>
|
||||
<div class="inner">
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</div>
|
||||
</ActionButton>
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
button {
|
||||
width: 195px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
font-weight: 500;
|
||||
background: #4285f4;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.inner img {
|
||||
|
||||
img {
|
||||
border-radius: 2px;
|
||||
width: 18px;
|
||||
margin: 3px 10px 3px 3px;
|
||||
}
|
||||
.inner p {
|
||||
margin: 0;
|
||||
margin-right: 11px;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
<script>
|
||||
export let width = 100
|
||||
export let height = 100
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 46 46"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
||||
>
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_normal_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="filter-1"
|
||||
>
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter1"
|
||||
result="shadowBlurOuter1"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
|
||||
in="shadowBlurOuter1"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter1"
|
||||
/>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter2"
|
||||
result="shadowBlurOuter2"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
|
||||
in="shadowBlurOuter2"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter2"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1" />
|
||||
<feMergeNode in="shadowMatrixOuter2" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
|
||||
</defs>
|
||||
<g
|
||||
id="Google-Button"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSPage"
|
||||
>
|
||||
<g
|
||||
id="9-PATCH"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-608.000000, -219.000000)"
|
||||
/>
|
||||
<g
|
||||
id="btn_google_dark_normal"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-1.000000, -1.000000)"
|
||||
>
|
||||
<g
|
||||
id="button"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(4.000000, 4.000000)"
|
||||
filter="url(#filter-1)"
|
||||
>
|
||||
<g id="button-bg">
|
||||
<use
|
||||
fill="#4285F4"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-2"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="button-bg-copy">
|
||||
<use
|
||||
fill="#FFFFFF"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-3"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
</g>
|
||||
<g
|
||||
id="logo_googleg_48dp"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(15.000000, 15.000000)"
|
||||
>
|
||||
<path
|
||||
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
|
||||
id="Shape"
|
||||
fill="#4285F4"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
|
||||
id="Shape"
|
||||
fill="#34A853"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
|
||||
id="Shape"
|
||||
fill="#FBBC05"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
|
||||
id="Shape"
|
||||
fill="#EA4335"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"
|
||||
id="Shape"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
|
@ -49,6 +49,10 @@
|
|||
filters = [...filters, duplicate]
|
||||
}
|
||||
|
||||
const getSchema = filter => {
|
||||
return schemaFields.find(field => field.name === filter.field)
|
||||
}
|
||||
|
||||
const onFieldChange = (expression, field) => {
|
||||
// Update the field type
|
||||
expression.type = enrichedSchemaFields.find(x => x.name === field)?.type
|
||||
|
@ -150,7 +154,12 @@
|
|||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter).dateOnly}
|
||||
timeOnly={getSchema(filter).timeOnly}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput disabled />
|
||||
{/if}
|
||||
|
|
|
@ -44,6 +44,20 @@
|
|||
$: readQuery = query.queryVerb === "read" || query.readable
|
||||
$: queryInvalid = !query.name || (readQuery && data.length === 0)
|
||||
|
||||
//Cast field in query preview response to number if specified by schema
|
||||
$: {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let row = data[i]
|
||||
for (let fieldName of Object.keys(fields)) {
|
||||
if (fields[fieldName] === "number" && !isNaN(Number(row[fieldName]))) {
|
||||
row[fieldName] = Number(row[fieldName])
|
||||
} else {
|
||||
row[fieldName] = row[fieldName]?.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seed the transformer
|
||||
if (query && !query.transformer) {
|
||||
query.transformer = "return data"
|
||||
|
|
|
@ -144,7 +144,11 @@ export const RelationshipTypes = {
|
|||
MANY_TO_ONE: "many-to-one",
|
||||
}
|
||||
|
||||
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
||||
export const ALLOWABLE_STRING_OPTIONS = [
|
||||
FIELDS.STRING,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.LONGFORM,
|
||||
]
|
||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
Table,
|
||||
Checkbox,
|
||||
} from "@budibase/bbui"
|
||||
import { email } from "stores/portal"
|
||||
import { email, admin } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import analytics, { Events } from "analytics"
|
||||
|
@ -58,6 +58,7 @@
|
|||
const savedConfig = await API.saveConfig(smtp)
|
||||
smtpConfig._rev = savedConfig._rev
|
||||
smtpConfig._id = savedConfig._id
|
||||
await admin.getChecklist()
|
||||
notifications.success(`Settings saved`)
|
||||
analytics.captureEvent(Events.SMTP.SAVED)
|
||||
} catch (error) {
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</Body>
|
||||
<Input
|
||||
type="email"
|
||||
label="Username"
|
||||
label="Email"
|
||||
bind:value={$email}
|
||||
error={$touched && $error}
|
||||
/>
|
||||
|
|
|
@ -24,14 +24,8 @@ export function createAdminStore() {
|
|||
const admin = writable(DEFAULT_CONFIG)
|
||||
|
||||
async function init() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const checklist = await API.getChecklist(tenantId)
|
||||
const totalSteps = Object.keys(checklist).length
|
||||
const completedSteps = Object.values(checklist).filter(
|
||||
x => x?.checked
|
||||
).length
|
||||
await getChecklist()
|
||||
await getEnvironment()
|
||||
|
||||
// enable system status checks in the cloud
|
||||
if (get(admin).cloud) {
|
||||
await getSystemStatus()
|
||||
|
@ -40,8 +34,6 @@ export function createAdminStore() {
|
|||
|
||||
admin.update(store => {
|
||||
store.loaded = true
|
||||
store.checklist = checklist
|
||||
store.onboardingProgress = (completedSteps / totalSteps) * 100
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
@ -81,6 +73,20 @@ export function createAdminStore() {
|
|||
})
|
||||
}
|
||||
|
||||
async function getChecklist() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const checklist = await API.getChecklist(tenantId)
|
||||
const totalSteps = Object.keys(checklist).length
|
||||
const completedSteps = Object.values(checklist).filter(
|
||||
x => x?.checked
|
||||
).length
|
||||
admin.update(store => {
|
||||
store.checklist = checklist
|
||||
store.onboardingProgress = (completedSteps / totalSteps) * 100
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
function unload() {
|
||||
admin.update(store => {
|
||||
store.loaded = false
|
||||
|
@ -93,6 +99,7 @@ export function createAdminStore() {
|
|||
init,
|
||||
checkImportComplete,
|
||||
unload,
|
||||
getChecklist,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.124-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.124-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||
"@budibase/bbui": "^1.0.148-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.148-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.148-alpha.0",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -88,6 +88,10 @@
|
|||
const schema = schemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
const getSchema = filter => {
|
||||
return schemaFields.find(field => field.name === filter.field)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" class:mobile={$context.device.mobile}>
|
||||
|
@ -134,7 +138,12 @@
|
|||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter).dateOnly}
|
||||
timeOnly={getSchema(filter).timeOnly}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else}
|
||||
<Input disabled />
|
||||
{/if}
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
fieldApi = value?.fieldApi
|
||||
fieldSchema = value?.fieldSchema
|
||||
})
|
||||
onDestroy(() => unsubscribe?.())
|
||||
|
||||
// Determine label class from position
|
||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||
|
@ -52,6 +51,11 @@
|
|||
const updateLabel = e => {
|
||||
builderStore.actions.updateProp("label", e.target.textContent)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
fieldApi?.deregister()
|
||||
unsubscribe?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<FieldGroupFallback>
|
||||
|
|
|
@ -329,6 +329,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// We don't want to actually remove the field state when deregistering, just
|
||||
// remove any errors and validation
|
||||
const deregister = () => {
|
||||
const fieldInfo = getField(field)
|
||||
fieldInfo.update(state => {
|
||||
state.fieldState.validator = null
|
||||
state.fieldState.error = null
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Updates the disabled state of a certain field
|
||||
const setDisabled = fieldDisabled => {
|
||||
const fieldInfo = getField(field)
|
||||
|
@ -348,6 +359,7 @@
|
|||
reset,
|
||||
updateValidation,
|
||||
setDisabled,
|
||||
deregister,
|
||||
validate: () => {
|
||||
// Validate the field by force setting the same value again
|
||||
const { fieldState } = get(getField(field))
|
||||
|
|
|
@ -155,7 +155,7 @@
|
|||
icon="Duplicate"
|
||||
on:click={() => {
|
||||
builderStore.actions.duplicateComponent(
|
||||
$builderStore.selectedComponent._id
|
||||
$builderStore.selectedComponentId
|
||||
)
|
||||
}}
|
||||
title="Duplicate component"
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
builderStore,
|
||||
uploadStore,
|
||||
rowSelectionStore,
|
||||
componentStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -24,6 +25,7 @@ export default {
|
|||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
componentStore,
|
||||
styleable,
|
||||
linkable,
|
||||
getAction,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.124-alpha.0",
|
||||
"@budibase/bbui": "^1.0.148-alpha.0",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
36
packages/server/__mocks__/@google-cloud/firestore.ts
Normal file
36
packages/server/__mocks__/@google-cloud/firestore.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
module FirebaseMock {
|
||||
const firebase: any = {}
|
||||
|
||||
firebase.Firestore = function () {
|
||||
this.get = jest.fn(() => [
|
||||
{
|
||||
data: jest.fn(() => ({ result: "test" })),
|
||||
},
|
||||
])
|
||||
|
||||
this.update = jest.fn()
|
||||
this.set = jest.fn()
|
||||
this.delete = jest.fn()
|
||||
|
||||
this.doc = jest.fn(() => ({
|
||||
update: this.update,
|
||||
set: this.set,
|
||||
delete: this.delete,
|
||||
get: jest.fn(() => ({
|
||||
data: jest.fn(() => ({ result: "test" })),
|
||||
})),
|
||||
id: "test_id",
|
||||
}))
|
||||
|
||||
this.where = jest.fn(() => ({
|
||||
get: this.get,
|
||||
}))
|
||||
|
||||
this.collection = jest.fn(() => ({
|
||||
doc: this.doc,
|
||||
where: this.where,
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = firebase
|
||||
}
|
|
@ -14,21 +14,13 @@ module PgMock {
|
|||
function Client() {}
|
||||
|
||||
Client.prototype.query = query
|
||||
Client.prototype.end = jest.fn()
|
||||
Client.prototype.connect = jest.fn()
|
||||
Client.prototype.release = jest.fn()
|
||||
|
||||
function Pool() {}
|
||||
|
||||
const on = jest.fn()
|
||||
Pool.prototype.query = query
|
||||
Pool.prototype.connect = jest.fn(() => {
|
||||
// @ts-ignore
|
||||
return new Client()
|
||||
})
|
||||
Pool.prototype.on = on
|
||||
|
||||
pg.Client = Client
|
||||
pg.Pool = Pool
|
||||
pg.queryMock = query
|
||||
pg.on = on
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -68,10 +68,10 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/backend-core": "^1.0.124-alpha.0",
|
||||
"@budibase/client": "^1.0.124-alpha.0",
|
||||
"@budibase/pro": "1.0.124-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||
"@budibase/backend-core": "^1.0.148-alpha.0",
|
||||
"@budibase/client": "^1.0.148-alpha.0",
|
||||
"@budibase/pro": "1.0.148-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.148-alpha.0",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
|
|
@ -406,11 +406,14 @@ const destroyApp = async (ctx: any) => {
|
|||
if (!env.isTest() && !isUnpublish) {
|
||||
await deleteApp(appId)
|
||||
}
|
||||
// automations only in production
|
||||
if (isUnpublish) {
|
||||
await cleanupAutomations(appId)
|
||||
}
|
||||
// make sure the app/role doesn't stick around after the app has been deleted
|
||||
await removeAppFromUserRoles(ctx, appId)
|
||||
// remove app role when the dev app is deleted (no trace of app anymore)
|
||||
else {
|
||||
await removeAppFromUserRoles(ctx, appId)
|
||||
}
|
||||
await appCache.invalidateAppMetadata(appId)
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
const { cloneDeep } = require("lodash")
|
||||
const { definitions } = require("../../integrations")
|
||||
const { getTenantId } = require("@budibase/backend-core/tenancy")
|
||||
const { SourceNames } = require("../../definitions/datasource")
|
||||
const googlesheets = require("../../integrations/googlesheets")
|
||||
const env = require("../../environment")
|
||||
const { featureFlags } = require("@budibase/backend-core")
|
||||
|
||||
exports.fetch = async function (ctx) {
|
||||
ctx.status = 200
|
||||
const defs = cloneDeep(definitions)
|
||||
|
||||
// for google sheets integration google verification
|
||||
if (env.EXCLUDE_QUOTAS_TENANTS) {
|
||||
const excludedTenants = env.EXCLUDE_QUOTAS_TENANTS.split(",")
|
||||
const tenantId = getTenantId()
|
||||
if (excludedTenants.includes(tenantId)) {
|
||||
defs[SourceNames.GOOGLE_SHEETS] = googlesheets.schema
|
||||
}
|
||||
if (featureFlags.isEnabled(featureFlags.FeatureFlag.GOOGLE_SHEETS)) {
|
||||
defs[SourceNames.GOOGLE_SHEETS] = googlesheets.schema
|
||||
}
|
||||
|
||||
ctx.body = defs
|
||||
|
|
|
@ -175,9 +175,10 @@ module External {
|
|||
const thisRow: Row = {}
|
||||
// filter the row down to what is actually the row (not joined)
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const value = row[`${table.name}.${fieldName}`] || row[fieldName]
|
||||
const pathValue = row[`${table.name}.${fieldName}`]
|
||||
const value = pathValue != null ? pathValue : row[fieldName]
|
||||
// all responses include "select col as table.col" so that overlaps are handled
|
||||
if (value) {
|
||||
if (value != null) {
|
||||
thisRow[fieldName] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { SearchIndexes } = require("../../../db/utils")
|
||||
const fetch = require("node-fetch")
|
||||
const { getCouchUrl } = require("@budibase/backend-core/db")
|
||||
const { getCouchInfo } = require("@budibase/backend-core/db")
|
||||
const { getAppId } = require("@budibase/backend-core/context")
|
||||
|
||||
/**
|
||||
|
@ -242,11 +242,10 @@ class QueryBuilder {
|
|||
|
||||
async run() {
|
||||
const appId = getAppId()
|
||||
const url = `${getCouchUrl()}/${appId}/_design/database/_search/${
|
||||
SearchIndexes.ROWS
|
||||
}`
|
||||
const { url, cookie } = getCouchInfo()
|
||||
const fullPath = `${url}/${appId}/_design/database/_search/${SearchIndexes.ROWS}`
|
||||
const body = this.buildSearchBody()
|
||||
return await runQuery(url, body)
|
||||
return await runQuery(fullPath, body, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,12 +253,16 @@ class QueryBuilder {
|
|||
* Executes a lucene search query.
|
||||
* @param url The query URL
|
||||
* @param body The request body defining search criteria
|
||||
* @param cookie The auth cookie for CouchDB
|
||||
* @returns {Promise<{rows: []}>}
|
||||
*/
|
||||
const runQuery = async (url, body) => {
|
||||
const runQuery = async (url, body, cookie) => {
|
||||
const response = await fetch(url, {
|
||||
body: JSON.stringify(body),
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: cookie,
|
||||
},
|
||||
})
|
||||
const json = await response.json()
|
||||
|
||||
|
|
|
@ -86,3 +86,15 @@ exports.substituteLoopStep = (hbsString, substitute) => {
|
|||
|
||||
return hbsString
|
||||
}
|
||||
|
||||
exports.stringSplit = value => {
|
||||
if (value == null) {
|
||||
return []
|
||||
}
|
||||
if (value.split("\n").length > 1) {
|
||||
value = value.split("\n")
|
||||
} else {
|
||||
value = value.split(",")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -47,7 +47,11 @@ exports.FieldTypes = {
|
|||
|
||||
exports.CanSwitchTypes = [
|
||||
[exports.FieldTypes.JSON, exports.FieldTypes.ARRAY],
|
||||
[exports.FieldTypes.STRING, exports.FieldTypes.OPTIONS],
|
||||
[
|
||||
exports.FieldTypes.STRING,
|
||||
exports.FieldTypes.OPTIONS,
|
||||
exports.FieldTypes.LONGFORM,
|
||||
],
|
||||
[exports.FieldTypes.BOOLEAN, exports.FieldTypes.NUMBER],
|
||||
]
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ const {
|
|||
StaticDatabases,
|
||||
isDevAppID,
|
||||
isProdAppID,
|
||||
getDevelopmentAppID,
|
||||
generateAppID,
|
||||
} = require("@budibase/backend-core/db")
|
||||
|
||||
const UNICODE_MAX = "\ufff0"
|
||||
|
@ -80,6 +82,8 @@ exports.UNICODE_MAX = UNICODE_MAX
|
|||
exports.SearchIndexes = SearchIndexes
|
||||
exports.AppStatus = AppStatus
|
||||
exports.BudibaseInternalDB = BudibaseInternalDB
|
||||
exports.generateAppID = generateAppID
|
||||
exports.generateDevAppID = getDevelopmentAppID
|
||||
|
||||
exports.generateRoleID = generateRoleID
|
||||
exports.getRoleParams = getRoleParams
|
||||
|
@ -243,28 +247,6 @@ exports.getLinkParams = (otherProps = {}) => {
|
|||
return getDocParams(DocumentTypes.LINK, null, otherProps)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new app ID.
|
||||
* @returns {string} The new app ID which the app doc can be stored under.
|
||||
*/
|
||||
exports.generateAppID = (tenantId = null) => {
|
||||
let id = `${DocumentTypes.APP}${SEPARATOR}`
|
||||
if (tenantId) {
|
||||
id += `${tenantId}${SEPARATOR}`
|
||||
}
|
||||
return `${id}${newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a development app ID from a real app ID.
|
||||
* @returns {string} the dev app ID which can be used for dev database.
|
||||
*/
|
||||
exports.generateDevAppID = appId => {
|
||||
const prefix = `${DocumentTypes.APP}${SEPARATOR}`
|
||||
const rest = appId.split(prefix)[1]
|
||||
return `${DocumentTypes.APP_DEV}${SEPARATOR}${rest}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new layout ID.
|
||||
* @returns {string} The new layout ID which the layout doc can be stored under.
|
||||
|
|
|
@ -92,13 +92,13 @@ module Firebase {
|
|||
|
||||
class FirebaseIntegration implements IntegrationBase {
|
||||
private config: FirebaseConfig
|
||||
private db: Firestore
|
||||
private client: Firestore
|
||||
|
||||
constructor(config: FirebaseConfig) {
|
||||
this.config = config
|
||||
if (config.serviceAccount) {
|
||||
const serviceAccount = JSON.parse(config.serviceAccount)
|
||||
this.db = new Firestore({
|
||||
this.client = new Firestore({
|
||||
projectId: serviceAccount.project_id,
|
||||
credentials: {
|
||||
client_email: serviceAccount.client_email,
|
||||
|
@ -106,7 +106,7 @@ module Firebase {
|
|||
},
|
||||
})
|
||||
} else {
|
||||
this.db = new Firestore({
|
||||
this.client = new Firestore({
|
||||
projectId: config.projectId,
|
||||
credentials: {
|
||||
client_email: config.email,
|
||||
|
@ -118,7 +118,7 @@ module Firebase {
|
|||
|
||||
async create(query: { json: object; extra: { [key: string]: string } }) {
|
||||
try {
|
||||
const documentReference = this.db
|
||||
const documentReference = this.client
|
||||
.collection(query.extra.collection)
|
||||
.doc()
|
||||
await documentReference.set({ ...query.json, id: documentReference.id })
|
||||
|
@ -133,7 +133,7 @@ module Firebase {
|
|||
async read(query: { json: object; extra: { [key: string]: string } }) {
|
||||
try {
|
||||
let snapshot
|
||||
const collectionRef = this.db.collection(query.extra.collection)
|
||||
const collectionRef = this.client.collection(query.extra.collection)
|
||||
if (
|
||||
query.extra.filterField &&
|
||||
query.extra.filter &&
|
||||
|
@ -164,19 +164,19 @@ module Firebase {
|
|||
extra: { [key: string]: string }
|
||||
}) {
|
||||
try {
|
||||
await this.db
|
||||
await this.client
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.update(query.json)
|
||||
|
||||
return (
|
||||
await this.db
|
||||
await this.client
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.get()
|
||||
).data()
|
||||
} catch (err) {
|
||||
console.error("Error writing to firebase", err)
|
||||
console.error("Error writing to Firestore", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -186,13 +186,13 @@ module Firebase {
|
|||
extra: { [key: string]: string }
|
||||
}) {
|
||||
try {
|
||||
await this.db
|
||||
await this.client
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.delete()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error("Error writing to mongodb", err)
|
||||
console.error("Error deleting from Firestore", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ const INTEGRATIONS = {
|
|||
[SourceNames.FIREBASE]: firebase.integration,
|
||||
[SourceNames.GOOGLE_SHEETS]: googlesheets.integration,
|
||||
[SourceNames.REDIS]: redis.integration,
|
||||
[SourceNames.FIREBASE]: firebase.integration,
|
||||
}
|
||||
|
||||
// optionally add oracle integration if the oracle binary can be installed
|
||||
|
|
|
@ -242,12 +242,10 @@ module MSSQLModule {
|
|||
if (typeof name !== "string") {
|
||||
continue
|
||||
}
|
||||
const type: string = convertSqlType(def.DATA_TYPE)
|
||||
|
||||
schema[name] = {
|
||||
autocolumn: !!autoColumns.find((col: string) => col === name),
|
||||
name: name,
|
||||
type,
|
||||
...convertSqlType(def.DATA_TYPE),
|
||||
}
|
||||
}
|
||||
tables[tableName] = {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "./utils"
|
||||
import { DatasourcePlus } from "./base/datasourcePlus"
|
||||
import dayjs from "dayjs"
|
||||
import { FieldTypes } from "../constants"
|
||||
const { NUMBER_REGEX } = require("../utilities")
|
||||
|
||||
module MySQLModule {
|
||||
|
@ -101,7 +102,7 @@ module MySQLModule {
|
|||
}
|
||||
// if not a number, see if it is a date - important to do in this order as any
|
||||
// integer will be considered a valid date
|
||||
else if (dayjs(binding).isValid()) {
|
||||
else if (/^\d/.test(binding) && dayjs(binding).isValid()) {
|
||||
bindings[i] = dayjs(binding).toDate()
|
||||
}
|
||||
}
|
||||
|
@ -151,20 +152,24 @@ module MySQLModule {
|
|||
|
||||
async internalQuery(
|
||||
query: SqlQuery,
|
||||
connect: boolean = true
|
||||
opts: { connect?: boolean; disableCoercion?: boolean } = {
|
||||
connect: true,
|
||||
disableCoercion: false,
|
||||
}
|
||||
): Promise<any[] | any> {
|
||||
try {
|
||||
if (connect) {
|
||||
if (opts?.connect) {
|
||||
await this.connect()
|
||||
}
|
||||
const baseBindings = query.bindings || []
|
||||
const bindings = opts?.disableCoercion
|
||||
? baseBindings
|
||||
: bindingTypeCoerce(baseBindings)
|
||||
// Node MySQL is callback based, so we must wrap our call in a promise
|
||||
const response = await this.client.query(
|
||||
query.sql,
|
||||
bindingTypeCoerce(query.bindings || [])
|
||||
)
|
||||
const response = await this.client.query(query.sql, bindings)
|
||||
return response[0]
|
||||
} finally {
|
||||
if (connect) {
|
||||
if (opts?.connect) {
|
||||
await this.disconnect()
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +184,7 @@ module MySQLModule {
|
|||
// get the tables first
|
||||
const tablesResp = await this.internalQuery(
|
||||
{ sql: "SHOW TABLES;" },
|
||||
false
|
||||
{ connect: false }
|
||||
)
|
||||
const tableNames = tablesResp.map(
|
||||
(obj: any) =>
|
||||
|
@ -191,7 +196,7 @@ module MySQLModule {
|
|||
const schema: TableSchema = {}
|
||||
const descResp = await this.internalQuery(
|
||||
{ sql: `DESCRIBE \`${tableName}\`;` },
|
||||
false
|
||||
{ connect: false }
|
||||
)
|
||||
for (let column of descResp) {
|
||||
const columnName = column.Field
|
||||
|
@ -211,8 +216,8 @@ module MySQLModule {
|
|||
schema[columnName] = {
|
||||
name: columnName,
|
||||
autocolumn: isAuto,
|
||||
type: convertSqlType(column.Type),
|
||||
constraints,
|
||||
...convertSqlType(column.Type),
|
||||
}
|
||||
}
|
||||
if (!tables[tableName]) {
|
||||
|
@ -254,7 +259,8 @@ module MySQLModule {
|
|||
async query(json: QueryJson) {
|
||||
await this.connect()
|
||||
try {
|
||||
const queryFn = (query: any) => this.internalQuery(query, false)
|
||||
const queryFn = (query: any) =>
|
||||
this.internalQuery(query, { connect: false, disableCoercion: true })
|
||||
return await this.queryWithReturning(json, queryFn)
|
||||
} finally {
|
||||
await this.disconnect()
|
||||
|
|
|
@ -279,9 +279,9 @@ module OracleModule {
|
|||
)
|
||||
}
|
||||
|
||||
private internalConvertType(column: OracleColumn): string {
|
||||
private internalConvertType(column: OracleColumn): { type: string } {
|
||||
if (this.isBooleanType(column)) {
|
||||
return FieldTypes.BOOLEAN
|
||||
return { type: FieldTypes.BOOLEAN }
|
||||
}
|
||||
|
||||
return convertSqlType(column.type)
|
||||
|
@ -328,7 +328,7 @@ module OracleModule {
|
|||
fieldSchema = {
|
||||
autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
|
||||
name: columnName,
|
||||
type: this.internalConvertType(oracleColumn),
|
||||
...this.internalConvertType(oracleColumn),
|
||||
}
|
||||
table.schema[columnName] = fieldSchema
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { DatasourcePlus } from "./base/datasourcePlus"
|
||||
|
||||
module PostgresModule {
|
||||
const { Pool } = require("pg")
|
||||
const { Client } = require("pg")
|
||||
const Sql = require("./base/sql")
|
||||
const { escapeDangerousCharacters } = require("../utilities")
|
||||
|
||||
|
@ -104,7 +104,6 @@ module PostgresModule {
|
|||
}
|
||||
|
||||
class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||
static pool: any
|
||||
private readonly client: any
|
||||
private readonly config: PostgresConfig
|
||||
private index: number = 1
|
||||
|
@ -136,11 +135,7 @@ module PostgresModule {
|
|||
}
|
||||
: undefined,
|
||||
}
|
||||
if (!this.pool) {
|
||||
this.pool = new Pool(newConfig)
|
||||
}
|
||||
|
||||
this.client = this.pool
|
||||
this.client = new Client(newConfig)
|
||||
this.setSchema()
|
||||
}
|
||||
|
||||
|
@ -171,16 +166,17 @@ module PostgresModule {
|
|||
} catch (err) {
|
||||
// @ts-ignore
|
||||
throw new Error(err)
|
||||
} finally {
|
||||
await this.client.end()
|
||||
}
|
||||
}
|
||||
|
||||
setSchema() {
|
||||
async setSchema() {
|
||||
await this.client.connect()
|
||||
if (!this.config.schema) {
|
||||
this.config.schema = "public"
|
||||
}
|
||||
this.client.on("connect", (client: any) => {
|
||||
client.query(`SET search_path TO ${this.config.schema}`)
|
||||
})
|
||||
this.client.query(`SET search_path TO ${this.config.schema}`)
|
||||
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
|
||||
}
|
||||
|
||||
|
@ -208,6 +204,8 @@ module PostgresModule {
|
|||
}
|
||||
} catch (err) {
|
||||
tableKeys = {}
|
||||
} finally {
|
||||
await this.client.close()
|
||||
}
|
||||
|
||||
const columnsResponse = await this.client.query(this.COLUMNS_SQL)
|
||||
|
@ -227,7 +225,6 @@ module PostgresModule {
|
|||
}
|
||||
}
|
||||
|
||||
const type: string = convertSqlType(column.data_type)
|
||||
const identity = !!(
|
||||
column.identity_generation ||
|
||||
column.identity_start ||
|
||||
|
@ -242,7 +239,7 @@ module PostgresModule {
|
|||
tables[tableName].schema[columnName] = {
|
||||
autocolumn: isAuto,
|
||||
name: columnName,
|
||||
type,
|
||||
...convertSqlType(column.data_type),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
92
packages/server/src/integrations/tests/firebase.spec.js
Normal file
92
packages/server/src/integrations/tests/firebase.spec.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
const firebase = require("@google-cloud/firestore")
|
||||
const FirebaseIntegration = require("../firebase")
|
||||
jest.mock("@google-cloud/firestore")
|
||||
|
||||
class TestConfiguration {
|
||||
constructor(config = {}) {
|
||||
this.integration = new FirebaseIntegration.integration(config)
|
||||
}
|
||||
}
|
||||
|
||||
describe("Firebase Integration", () => {
|
||||
let config
|
||||
let tableName = "Users"
|
||||
|
||||
beforeEach(() => {
|
||||
config = new TestConfiguration({
|
||||
serviceAccount: "{}"
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the create method with the correct params", async () => {
|
||||
await config.integration.create({
|
||||
table: tableName,
|
||||
json: {
|
||||
Name: "Test Name"
|
||||
},
|
||||
extra: {
|
||||
collection: "test"
|
||||
}
|
||||
})
|
||||
expect(config.integration.client.collection).toHaveBeenCalledWith("test")
|
||||
expect(config.integration.client.set).toHaveBeenCalledWith({
|
||||
Name: "Test Name",
|
||||
id: "test_id"
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the read method with the correct params", async () => {
|
||||
const response = await config.integration.read({
|
||||
table: tableName,
|
||||
json: {
|
||||
Name: "Test"
|
||||
},
|
||||
extra: {
|
||||
collection: "test",
|
||||
filterField: "field",
|
||||
filter: "==",
|
||||
filterValue: "value",
|
||||
}
|
||||
})
|
||||
expect(config.integration.client.collection).toHaveBeenCalledWith("test")
|
||||
expect(config.integration.client.where).toHaveBeenCalledWith("field", "==", "value")
|
||||
expect(response).toEqual([{ result: "test"}])
|
||||
})
|
||||
|
||||
it("calls the update method with the correct params", async () => {
|
||||
const response = await config.integration.update({
|
||||
table: tableName,
|
||||
json: {
|
||||
id: "test",
|
||||
Name: "Test"
|
||||
},
|
||||
extra: {
|
||||
collection: "test"
|
||||
}
|
||||
})
|
||||
expect(config.integration.client.collection).toHaveBeenCalledWith("test")
|
||||
expect(config.integration.client.update).toHaveBeenCalledWith({
|
||||
Name: "Test",
|
||||
id: "test"
|
||||
})
|
||||
expect(response).toEqual({
|
||||
result: "test"
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the delete method with the correct params", async () => {
|
||||
const response = await config.integration.delete({
|
||||
table: tableName,
|
||||
json: {
|
||||
id: "test",
|
||||
Name: "Test"
|
||||
},
|
||||
extra: {
|
||||
collection: "test"
|
||||
}
|
||||
})
|
||||
expect(config.integration.client.collection).toHaveBeenCalledWith("test")
|
||||
expect(config.integration.client.doc).toHaveBeenCalledWith("test")
|
||||
expect(config.integration.client.delete).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -15,10 +15,6 @@ describe("Postgres Integration", () => {
|
|||
config = new TestConfiguration()
|
||||
})
|
||||
|
||||
it("calls the connection callback", async () => {
|
||||
expect(pg.on).toHaveBeenCalledWith('connect', expect.anything())
|
||||
})
|
||||
|
||||
it("calls the create method with the correct params", async () => {
|
||||
const sql = "insert into users (name, age) values ('Joe', 123);"
|
||||
await config.integration.create({
|
||||
|
|
|
@ -35,6 +35,9 @@ const SQL_DATE_TYPE_MAP = {
|
|||
date: FieldTypes.DATETIME,
|
||||
}
|
||||
|
||||
const SQL_DATE_ONLY_TYPES = ["date"]
|
||||
const SQL_TIME_ONLY_TYPES = ["time"]
|
||||
|
||||
const SQL_STRING_TYPE_MAP = {
|
||||
varchar: FieldTypes.STRING,
|
||||
char: FieldTypes.STRING,
|
||||
|
@ -42,9 +45,9 @@ const SQL_STRING_TYPE_MAP = {
|
|||
nvarchar: FieldTypes.STRING,
|
||||
ntext: FieldTypes.STRING,
|
||||
enum: FieldTypes.STRING,
|
||||
blob: FieldTypes.LONGFORM,
|
||||
long: FieldTypes.LONGFORM,
|
||||
text: FieldTypes.LONGFORM,
|
||||
blob: FieldTypes.STRING,
|
||||
long: FieldTypes.STRING,
|
||||
text: FieldTypes.STRING,
|
||||
}
|
||||
|
||||
const SQL_BOOLEAN_TYPE_MAP = {
|
||||
|
@ -85,9 +88,9 @@ export function breakExternalTableId(tableId: string | undefined) {
|
|||
return {}
|
||||
}
|
||||
const parts = tableId.split(DOUBLE_SEPARATOR)
|
||||
let tableName = parts.pop()
|
||||
let datasourceId = parts.shift()
|
||||
// if they need joined
|
||||
let datasourceId = parts.join(DOUBLE_SEPARATOR)
|
||||
let tableName = parts.join(DOUBLE_SEPARATOR)
|
||||
return { datasourceId, tableName }
|
||||
}
|
||||
|
||||
|
@ -137,12 +140,20 @@ export function breakRowIdField(_id: string | { _id: string }): any[] {
|
|||
}
|
||||
|
||||
export function convertSqlType(type: string) {
|
||||
let foundType = FieldTypes.STRING
|
||||
const lcType = type.toLowerCase()
|
||||
for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
|
||||
if (type.toLowerCase().includes(external)) {
|
||||
return internal
|
||||
if (lcType.includes(external)) {
|
||||
foundType = internal
|
||||
break
|
||||
}
|
||||
}
|
||||
return FieldTypes.STRING
|
||||
const schema: any = { type: foundType }
|
||||
if (foundType === FieldTypes.DATETIME) {
|
||||
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lcType)
|
||||
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lcType)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
export function getSqlQuery(query: SqlQuery | string): SqlQuery {
|
||||
|
@ -207,11 +218,20 @@ function shouldCopySpecialColumn(
|
|||
column: { type: string },
|
||||
fetchedColumn: { type: string } | undefined
|
||||
) {
|
||||
const specialTypes = [
|
||||
FieldTypes.OPTIONS,
|
||||
FieldTypes.LONGFORM,
|
||||
FieldTypes.ARRAY,
|
||||
FieldTypes.FORMULA,
|
||||
]
|
||||
if (column && !fetchedColumn) {
|
||||
return true
|
||||
}
|
||||
const fetchedIsNumber =
|
||||
!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER
|
||||
return (
|
||||
column.type === FieldTypes.OPTIONS ||
|
||||
column.type === FieldTypes.ARRAY ||
|
||||
((!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER) &&
|
||||
column.type === FieldTypes.BOOLEAN)
|
||||
specialTypes.indexOf(column.type) !== -1 ||
|
||||
(fetchedIsNumber && column.type === FieldTypes.BOOLEAN)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -100,10 +100,10 @@ class Orchestrator {
|
|||
let automation = this._automation
|
||||
const app = await this.getApp()
|
||||
let stopped = false
|
||||
let loopStep
|
||||
let loopStep = null
|
||||
|
||||
let stepCount = 0
|
||||
let loopStepNumber
|
||||
let loopStepNumber = null
|
||||
let loopSteps = []
|
||||
for (let step of automation.definition.steps) {
|
||||
stepCount++
|
||||
|
@ -117,15 +117,17 @@ class Orchestrator {
|
|||
if (loopStep) {
|
||||
input = await processObject(loopStep.inputs, this._context)
|
||||
}
|
||||
let iterations = loopStep ? input.binding.length : 1
|
||||
let iterations = loopStep
|
||||
? Array.isArray(input.binding)
|
||||
? input.binding.length
|
||||
: automationUtils.stringSplit(input.binding).length
|
||||
: 1
|
||||
let iterationCount = 0
|
||||
for (let index = 0; index < iterations; index++) {
|
||||
let originalStepInput = cloneDeep(step.inputs)
|
||||
|
||||
// Handle if the user has set a max iteration count or if it reaches the max limit set by us
|
||||
if (loopStep) {
|
||||
// lets first of all handle the input
|
||||
// if the input is array then use it, if it is a string then split it on every new line
|
||||
let newInput = await processObject(
|
||||
loopStep.inputs,
|
||||
cloneDeep(this._context)
|
||||
|
@ -134,9 +136,6 @@ class Orchestrator {
|
|||
newInput,
|
||||
loopStep.schema.inputs
|
||||
)
|
||||
this._context.steps[loopStepNumber] = {
|
||||
currentItem: newInput.binding[index],
|
||||
}
|
||||
|
||||
let tempOutput = { items: loopSteps, iterations: iterationCount }
|
||||
if (
|
||||
|
@ -154,6 +153,20 @@ class Orchestrator {
|
|||
break
|
||||
}
|
||||
|
||||
let item
|
||||
if (
|
||||
typeof loopStep.inputs.binding === "string" &&
|
||||
loopStep.inputs.option === "String"
|
||||
) {
|
||||
item = automationUtils.stringSplit(newInput.binding)
|
||||
} else {
|
||||
item = loopStep.inputs.binding
|
||||
}
|
||||
|
||||
this._context.steps[loopStepNumber] = {
|
||||
currentItem: item[index],
|
||||
}
|
||||
|
||||
// The "Loop" binding in the front end is "fake", so replace it here so the context can understand it
|
||||
// Pretty hacky because we need to account for the row object
|
||||
for (let [key, value] of Object.entries(originalStepInput)) {
|
||||
|
@ -178,7 +191,6 @@ class Orchestrator {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
index === parseInt(env.AUTOMATION_MAX_ITERATIONS) ||
|
||||
index === loopStep.inputs.iterations
|
||||
|
@ -192,10 +204,25 @@ class Orchestrator {
|
|||
break
|
||||
}
|
||||
|
||||
let isFailure = false
|
||||
if (
|
||||
this._context.steps[loopStepNumber]?.currentItem ===
|
||||
loopStep.inputs.failure
|
||||
typeof this._context.steps[loopStepNumber]?.currentItem === "object"
|
||||
) {
|
||||
isFailure = Object.keys(
|
||||
this._context.steps[loopStepNumber].currentItem
|
||||
).some(value => {
|
||||
return (
|
||||
this._context.steps[loopStepNumber].currentItem[value] ===
|
||||
loopStep.inputs.failure
|
||||
)
|
||||
})
|
||||
} else {
|
||||
isFailure =
|
||||
this._context.steps[loopStepNumber]?.currentItem ===
|
||||
loopStep.inputs.failure
|
||||
}
|
||||
|
||||
if (isFailure) {
|
||||
this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
|
||||
status: AutomationErrors.FAILURE_CONDITION,
|
||||
success: false,
|
||||
|
@ -286,18 +313,16 @@ class Orchestrator {
|
|||
|
||||
module.exports = (input, callback) => {
|
||||
const appId = input.data.event.appId
|
||||
doInAppContext(appId, () => {
|
||||
doInAppContext(appId, async () => {
|
||||
const automationOrchestrator = new Orchestrator(
|
||||
input.data.automation,
|
||||
input.data.event
|
||||
)
|
||||
automationOrchestrator
|
||||
.execute()
|
||||
.then(response => {
|
||||
callback(null, response)
|
||||
})
|
||||
.catch(err => {
|
||||
callback(err)
|
||||
})
|
||||
try {
|
||||
const response = await automationOrchestrator.execute()
|
||||
callback(null, response)
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -191,14 +191,13 @@ class QueryRunner {
|
|||
}
|
||||
|
||||
module.exports = (input, callback) => {
|
||||
doInAppContext(input.appId, () => {
|
||||
doInAppContext(input.appId, async () => {
|
||||
const Runner = new QueryRunner(input)
|
||||
Runner.execute()
|
||||
.then(response => {
|
||||
callback(null, response)
|
||||
})
|
||||
.catch(err => {
|
||||
callback(err)
|
||||
})
|
||||
try {
|
||||
const response = await Runner.execute()
|
||||
callback(null, response)
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ const { budibaseTempDir } = require("../budibaseDir")
|
|||
const fs = require("fs")
|
||||
const { join } = require("path")
|
||||
const uuid = require("uuid/v4")
|
||||
const { doWithDB } = require("@budibase/backend-core/db")
|
||||
const {
|
||||
doWithDB,
|
||||
dangerousGetDB,
|
||||
closeDB,
|
||||
} = require("@budibase/backend-core/db")
|
||||
const { ObjectStoreBuckets } = require("../../constants")
|
||||
const {
|
||||
upload,
|
||||
|
@ -151,14 +155,18 @@ exports.streamBackup = async appId => {
|
|||
* @return {*} either a readable stream or a string
|
||||
*/
|
||||
exports.exportDB = async (dbName, { stream, filter, exportName } = {}) => {
|
||||
return doWithDB(dbName, async db => {
|
||||
// Stream the dump if required
|
||||
if (stream) {
|
||||
const memStream = new MemoryStream()
|
||||
db.dump(memStream, { filter })
|
||||
return memStream
|
||||
}
|
||||
// streaming a DB dump is a bit more complicated, can't close DB
|
||||
if (stream) {
|
||||
const db = dangerousGetDB(dbName)
|
||||
const memStream = new MemoryStream()
|
||||
memStream.on("end", async () => {
|
||||
await closeDB(db)
|
||||
})
|
||||
db.dump(memStream, { filter })
|
||||
return memStream
|
||||
}
|
||||
|
||||
return doWithDB(dbName, async db => {
|
||||
// Write the dump to file if required
|
||||
if (exportName) {
|
||||
const path = join(budibaseTempDir(), exportName)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -70,7 +70,7 @@ function createTemplate(string, opts) {
|
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @param {object|undefined} opts optional - specify some options for processing.
|
||||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObject = async (object, context, opts) => {
|
||||
|
@ -101,7 +101,7 @@ module.exports.processObject = async (object, context, opts) => {
|
|||
* then nothing will occur.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @param {object|undefined} opts optional - specify some options for processing.
|
||||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processString = async (string, context, opts) => {
|
||||
|
@ -115,7 +115,7 @@ module.exports.processString = async (string, context, opts) => {
|
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||
* if the structure contains any cycles then this will fail.
|
||||
* @param {object} context The context that handlebars should fill data from.
|
||||
* @param {object|undefined} opts optional - specify some options for processing.
|
||||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {object|array} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObjectSync = (object, context, opts) => {
|
||||
|
@ -136,7 +136,7 @@ module.exports.processObjectSync = (object, context, opts) => {
|
|||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||
* @param {string} string The template string which is the filled from the context object.
|
||||
* @param {object} context An object of information which will be used to enrich the string.
|
||||
* @param {object|undefined} opts optional - specify some options for processing.
|
||||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processStringSync = (string, context, opts) => {
|
||||
|
@ -194,7 +194,7 @@ module.exports.makePropSafe = property => {
|
|||
/**
|
||||
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
||||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
||||
* @param opts optional - specify some options for processing.
|
||||
* @param [opts] optional - specify some options for processing.
|
||||
* @returns {boolean} Whether or not the input string is valid.
|
||||
*/
|
||||
module.exports.isValid = (string, opts) => {
|
||||
|
@ -205,6 +205,7 @@ module.exports.isValid = (string, opts) => {
|
|||
"array",
|
||||
"cannot read property",
|
||||
"undefined",
|
||||
"json at position 0",
|
||||
]
|
||||
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
||||
const invalidCases = [`expecting '`]
|
||||
|
|
|
@ -360,6 +360,13 @@ describe("Test the literal helper", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("Test that JSONpase helper", () => {
|
||||
it("should state that the JSONparse helper is valid", async () => {
|
||||
const output = isValid(`{{ JSONparse input }}`)
|
||||
expect(output).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Cover a few complex use cases", () => {
|
||||
it("should allow use of three different collection helpers", async () => {
|
||||
const output = await processString(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.124-alpha.0",
|
||||
"version": "1.0.148-alpha.0",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -31,9 +31,9 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "^1.0.124-alpha.0",
|
||||
"@budibase/pro": "1.0.124-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||
"@budibase/backend-core": "^1.0.148-alpha.0",
|
||||
"@budibase/pro": "1.0.148-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.148-alpha.0",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sentry/node": "6.17.7",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
|
|
|
@ -293,10 +293,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/backend-core@1.0.123-alpha.1":
|
||||
version "1.0.123-alpha.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.123-alpha.1.tgz#3ce95c41837a0a4f071aee08d5d8ad504b5bd14c"
|
||||
integrity sha512-IbalOeH26jOBCEa0ur8zFChVT+NpxYai40rHU2w8ytDxVpVBYidkcPPD0udCGKEoKRz+QJVf47nAH+g+ahsBgg==
|
||||
"@budibase/backend-core@1.0.147":
|
||||
version "1.0.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.147.tgz#fc93a803e97e304170b33bd28b4f5deda32bec18"
|
||||
integrity sha512-0GcF9G/tTAsko9g352MB8K3EtMTVb7v7Av6RdsYyKBdVtOD42HKFzSOD0n/RQUL4v70YByiu99zJAB6z1m1Pcg==
|
||||
dependencies:
|
||||
"@techpass/passport-openidconnect" "^0.3.0"
|
||||
aws-sdk "^2.901.0"
|
||||
|
@ -321,12 +321,12 @@
|
|||
uuid "^8.3.2"
|
||||
zlib "^1.0.5"
|
||||
|
||||
"@budibase/pro@1.0.123-alpha.1":
|
||||
version "1.0.123-alpha.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.123-alpha.1.tgz#d585c8062142b418378fe6583f7495ff3981d2e4"
|
||||
integrity sha512-nwjYv8aeMTWw0bbhiCKnDGm8WMgCWUHXZojyAGJThipK+lhZaLGHRbfkNQ3mJeCmN1EPpKxn0GZ0NGUwqOjb+g==
|
||||
"@budibase/pro@1.0.147":
|
||||
version "1.0.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.147.tgz#c1ec9d92ffaa40285a83c57ddbec1f32802cd9a1"
|
||||
integrity sha512-e4im6Byqeeit/QFHkVhVdRmgIlJJY/W06cQrOuiG/1ngO4PGnXeJqtjQjHVfvchD5Xi3y1MgQ4AtH2Bzb5LEhw==
|
||||
dependencies:
|
||||
"@budibase/backend-core" "1.0.123-alpha.1"
|
||||
"@budibase/backend-core" "1.0.147"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@cspotcode/source-map-consumer@0.8.0":
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
## Description
|
||||
_Describe the problem or feature in addition to a link to the relevant github issues._
|
||||
|
||||
Addresses:
|
||||
- `<Enter the Link to the issue(s) this PR addresses>`
|
||||
- ...more if required
|
||||
|
||||
## Screenshots
|
||||
_If a UI facing feature, some screenshots of the new functionality._
|
||||
_If a UI facing feature, a short video of the happy path, and some screenshots of the new functionality._
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue