commit
f358506ca5
|
@ -6,4 +6,5 @@ packages/server/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||||
|
packages/builder/cypress/reports
|
|
@ -72,3 +72,56 @@ jobs:
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
|
||||||
|
- name: Get the latest budibase release version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Tag and release Proxy service docker image
|
||||||
|
run: |
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
yarn build:docker:proxy:release
|
||||||
|
docker tag proxy-service budibase/proxy:$RELEASE_TAG
|
||||||
|
docker push budibase/proxy:$RELEASE_TAG
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
RELEASE_TAG: k8s-release
|
||||||
|
|
||||||
|
- name: Pull values.yaml from budibase-infra
|
||||||
|
run: |
|
||||||
|
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||||
|
-H 'Accept: application/vnd.github.v3.raw' \
|
||||||
|
-o values.release.yaml \
|
||||||
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml
|
||||||
|
wc -l values.release.yaml
|
||||||
|
|
||||||
|
- name: Deploy to Release Environment
|
||||||
|
uses: glopezep/helm@v1.7.1
|
||||||
|
with:
|
||||||
|
release: budibase-release
|
||||||
|
namespace: budibase
|
||||||
|
chart: charts/budibase
|
||||||
|
token: ${{ github.token }}
|
||||||
|
helm: helm3
|
||||||
|
values: |
|
||||||
|
globals:
|
||||||
|
appVersion: develop
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
nginx: true
|
||||||
|
value-files: >-
|
||||||
|
[
|
||||||
|
"values.release.yaml"
|
||||||
|
]
|
||||||
|
env:
|
||||||
|
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
||||||
|
|
||||||
|
- name: Discord Webhook Action
|
||||||
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||||
|
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
|
||||||
|
embed-title: ${{ env.RELEASE_VERSION }}
|
|
@ -33,23 +33,20 @@ jobs:
|
||||||
with:
|
with:
|
||||||
record: true
|
record: true
|
||||||
install: false
|
install: false
|
||||||
|
tag: nightly
|
||||||
command: yarn test:e2e:ci:record
|
command: yarn test:e2e:ci:record
|
||||||
env:
|
env:
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
|
||||||
# TODO: upload recordings to s3
|
- uses: actions/upload-artifact@v3
|
||||||
# - name: Configure AWS Credentials
|
|
||||||
# uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
# with:
|
|
||||||
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
# aws-region: eu-west-1
|
|
||||||
|
|
||||||
- name: Discord Webhook Action
|
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
|
name: Test Reports
|
||||||
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.outputs.dashboardUrl }}"
|
path: packages/builder/cypress/reports/testReport.html
|
||||||
embed-title: ${{ steps.cypress.outcome }}
|
|
||||||
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
|
|
||||||
|
|
||||||
|
- name: Cypress Discord Notify
|
||||||
|
run: yarn test:e2e:ci:notify
|
||||||
|
env:
|
||||||
|
CYPRESS_WEBHOOK_URL: ${{ secrets.BUDI_QA_WEBHOOK }}
|
||||||
|
CYPRESS_OUTCOME: ${{ steps.cypress.outcome }}
|
||||||
|
CYPRESS_DASHBOARD_URL: ${{ steps.cypress.outputs.dashboardUrl }}
|
||||||
|
GITHUB_RUN_URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID
|
||||||
|
|
|
@ -97,5 +97,7 @@ hosting/proxy/.generated-nginx.prod.conf
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
hosting/.generated*
|
hosting/.generated*
|
||||||
packages/builder/cypress.env.json
|
packages/builder/cypress.env.json
|
||||||
stats.html
|
packages/builder/cypress/reports
|
||||||
|
stats.html
|
||||||
|
|
||||||
|
|
|
@ -215,7 +215,7 @@ couchdb:
|
||||||
## The CouchDB image
|
## The CouchDB image
|
||||||
image:
|
image:
|
||||||
repository: couchdb
|
repository: couchdb
|
||||||
tag: 3.1.0
|
tag: 3.2.1
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
## Experimental integration with Lucene-powered fulltext search
|
## Experimental integration with Lucene-powered fulltext search
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.191",
|
"version": "1.0.192-alpha.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -49,11 +49,13 @@
|
||||||
"test:e2e": "lerna run cy:test --stream",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||||
"test:e2e:ci:record": "lerna run cy:ci:record --stream",
|
"test:e2e:ci:record": "lerna run cy:ci:record --stream",
|
||||||
|
"test:e2e:ci:notify": "lerna run cy:ci:notify",
|
||||||
"build:specs": "lerna run specs",
|
"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": "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",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||||
|
"build:docker:proxy:release": "node scripts/proxy/generateProxyConfig release && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
|
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
|
||||||
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.191",
|
"version": "1.0.192-alpha.1",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
"aws-sdk": "^2.901.0",
|
"aws-sdk": "^2.901.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
"emitter-listener": "^1.1.2",
|
"emitter-listener": "^1.1.2",
|
||||||
"ioredis": "^4.27.1",
|
"ioredis": "^4.27.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
|
|
@ -1,61 +1,194 @@
|
||||||
|
require("../../tests/utilities/dbConfig");
|
||||||
const {
|
const {
|
||||||
generateAppID,
|
generateAppID,
|
||||||
getDevelopmentAppID,
|
getDevelopmentAppID,
|
||||||
getProdAppID,
|
getProdAppID,
|
||||||
isDevAppID,
|
isDevAppID,
|
||||||
isProdAppID,
|
isProdAppID,
|
||||||
|
getPlatformUrl,
|
||||||
|
getScopedConfig
|
||||||
} = require("../utils")
|
} = require("../utils")
|
||||||
|
const tenancy = require("../../tenancy");
|
||||||
|
const { Configs, DEFAULT_TENANT_ID } = require("../../constants");
|
||||||
|
const env = require("../../environment")
|
||||||
|
|
||||||
function getID() {
|
describe("utils", () => {
|
||||||
const appId = generateAppID()
|
describe("app ID manipulation", () => {
|
||||||
const split = appId.split("_")
|
|
||||||
const uuid = split[split.length - 1]
|
function getID() {
|
||||||
const devAppId = `app_dev_${uuid}`
|
const appId = generateAppID()
|
||||||
return { appId, devAppId, split, uuid }
|
const split = appId.split("_")
|
||||||
|
const uuid = split[split.length - 1]
|
||||||
|
const devAppId = `app_dev_${uuid}`
|
||||||
|
return { appId, devAppId, split, uuid }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const DB_URL = "http://dburl.com"
|
||||||
|
const DEFAULT_URL = "http://localhost:10000"
|
||||||
|
const ENV_URL = "http://env.com"
|
||||||
|
|
||||||
|
const setDbPlatformUrl = async () => {
|
||||||
|
const db = tenancy.getGlobalDB()
|
||||||
|
db.put({
|
||||||
|
_id: "config_settings",
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
config: {
|
||||||
|
platformUrl: DB_URL
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("app ID manipulation", () => {
|
const clearSettingsConfig = async () => {
|
||||||
it("should be able to generate a new app ID", () => {
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
expect(generateAppID().startsWith("app_")).toEqual(true)
|
const db = tenancy.getGlobalDB()
|
||||||
|
try {
|
||||||
|
const config = await db.get("config_settings")
|
||||||
|
await db.remove("config_settings", config._rev)
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getPlatformUrl", () => {
|
||||||
|
describe("self host", () => {
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env._set("SELF_HOST", 1)
|
||||||
|
await clearSettingsConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the default url", async () => {
|
||||||
|
await tenancy.doInTenant(null, async () => {
|
||||||
|
const url = await getPlatformUrl()
|
||||||
|
expect(url).toBe(DEFAULT_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the environment", async () => {
|
||||||
|
await tenancy.doInTenant(null, async () => {
|
||||||
|
env._set("PLATFORM_URL", ENV_URL)
|
||||||
|
const url = await getPlatformUrl()
|
||||||
|
expect(url).toBe(ENV_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets the platform url from the database", async () => {
|
||||||
|
await tenancy.doInTenant(null, async () => {
|
||||||
|
await setDbPlatformUrl()
|
||||||
|
const url = await getPlatformUrl()
|
||||||
|
expect(url).toBe(DB_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
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", () => {
|
describe("cloud", () => {
|
||||||
const { devAppId, uuid } = getID()
|
const TENANT_AWARE_URL = "http://default.env.com"
|
||||||
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to convert a development ID to a production", () => {
|
beforeEach(async () => {
|
||||||
const { devAppId, uuid } = getID()
|
env._set("SELF_HOSTED", 0)
|
||||||
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`)
|
env._set("MULTI_TENANCY", 1)
|
||||||
})
|
env._set("PLATFORM_URL", ENV_URL)
|
||||||
|
await clearSettingsConfig()
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to convert a production ID to production", () => {
|
it("gets the platform url from the environment without tenancy", async () => {
|
||||||
const { appId, uuid } = getID()
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
expect(getProdAppID(appId)).toEqual(`app_${uuid}`)
|
const url = await getPlatformUrl({ tenantAware: false })
|
||||||
})
|
expect(url).toBe(ENV_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to confirm dev app ID is development", () => {
|
it("gets the platform url from the environment with tenancy", async () => {
|
||||||
const { devAppId } = getID()
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
expect(isDevAppID(devAppId)).toEqual(true)
|
const url = await getPlatformUrl()
|
||||||
})
|
expect(url).toBe(TENANT_AWARE_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to confirm prod app ID is not development", () => {
|
it("never gets the platform url from the database", async () => {
|
||||||
const { appId } = getID()
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
expect(isDevAppID(appId)).toEqual(false)
|
await setDbPlatformUrl()
|
||||||
|
const url = await getPlatformUrl()
|
||||||
|
expect(url).toBe(TENANT_AWARE_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to confirm prod app ID is prod", () => {
|
describe("getScopedConfig", () => {
|
||||||
const { appId } = getID()
|
describe("settings config", () => {
|
||||||
expect(isProdAppID(appId)).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to confirm dev app ID is not prod", () => {
|
beforeEach(async () => {
|
||||||
const { devAppId } = getID()
|
env._set("SELF_HOSTED", 1)
|
||||||
expect(isProdAppID(devAppId)).toEqual(false)
|
env._set("PLATFORM_URL", "")
|
||||||
|
await clearSettingsConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the platform url with an existing config", async () => {
|
||||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
|
await setDbPlatformUrl()
|
||||||
|
const db = tenancy.getGlobalDB()
|
||||||
|
const config = await getScopedConfig(db, { type: Configs.SETTINGS })
|
||||||
|
expect(config.platformUrl).toBe(DB_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the platform url without an existing config", async () => {
|
||||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||||
|
const db = tenancy.getGlobalDB()
|
||||||
|
const config = await getScopedConfig(db, { type: Configs.SETTINGS })
|
||||||
|
expect(config.platformUrl).toBe(DEFAULT_URL)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,7 @@ const {
|
||||||
APP_PREFIX,
|
APP_PREFIX,
|
||||||
APP_DEV,
|
APP_DEV,
|
||||||
} = require("./constants")
|
} = require("./constants")
|
||||||
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
const { getTenantId, getGlobalDBName, getGlobalDB } = require("../tenancy")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { doWithDB, allDbs } = require("./index")
|
const { doWithDB, allDbs } = require("./index")
|
||||||
const { getCouchInfo } = require("./pouch")
|
const { getCouchInfo } = require("./pouch")
|
||||||
|
@ -392,9 +392,7 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
|
||||||
// always provide the platform URL
|
// always provide the platform URL
|
||||||
if (type === Configs.SETTINGS) {
|
if (type === Configs.SETTINGS) {
|
||||||
if (scopedConfig && scopedConfig.doc) {
|
if (scopedConfig && scopedConfig.doc) {
|
||||||
scopedConfig.doc.config.platformUrl = await getPlatformUrl(
|
scopedConfig.doc.config.platformUrl = await getPlatformUrl()
|
||||||
scopedConfig.doc.config
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
scopedConfig = {
|
scopedConfig = {
|
||||||
doc: {
|
doc: {
|
||||||
|
@ -409,19 +407,30 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
|
||||||
return scopedConfig && scopedConfig.doc
|
return scopedConfig && scopedConfig.doc
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlatformUrl = async settings => {
|
const getPlatformUrl = async (opts = { tenantAware: true }) => {
|
||||||
let platformUrl = env.PLATFORM_URL || "http://localhost:10000"
|
let platformUrl = env.PLATFORM_URL || "http://localhost:10000"
|
||||||
|
|
||||||
if (!env.SELF_HOSTED && env.MULTI_TENANCY) {
|
if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) {
|
||||||
// cloud and multi tenant - add the tenant to the default platform url
|
// cloud and multi tenant - add the tenant to the default platform url
|
||||||
const tenantId = getTenantId()
|
const tenantId = getTenantId()
|
||||||
if (!platformUrl.includes("localhost:")) {
|
if (!platformUrl.includes("localhost:")) {
|
||||||
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
|
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
|
||||||
}
|
}
|
||||||
} else {
|
} else if (env.SELF_HOSTED) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
// get the doc directly instead of with getScopedConfig to prevent loop
|
||||||
|
let settings
|
||||||
|
try {
|
||||||
|
settings = await db.get(generateConfigID({ type: Configs.SETTINGS }))
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// self hosted - check for platform url override
|
// self hosted - check for platform url override
|
||||||
if (settings && settings.platformUrl) {
|
if (settings && settings.config && settings.config.platformUrl) {
|
||||||
platformUrl = settings.platformUrl
|
platformUrl = settings.config.platformUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,15 @@ function isDev() {
|
||||||
return process.env.NODE_ENV !== "production"
|
return process.env.NODE_ENV !== "production"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let LOADED = false
|
||||||
|
if (!LOADED && isDev() && !isTest()) {
|
||||||
|
require("dotenv").config()
|
||||||
|
LOADED = true
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
isTest,
|
||||||
|
isDev,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
|
@ -41,9 +49,8 @@ module.exports = {
|
||||||
GLOBAL_CLOUD_BUCKET_NAME:
|
GLOBAL_CLOUD_BUCKET_NAME:
|
||||||
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
|
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
|
||||||
USE_COUCH: process.env.USE_COUCH || true,
|
USE_COUCH: process.env.USE_COUCH || true,
|
||||||
|
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
isTest,
|
|
||||||
isDev,
|
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
|
|
|
@ -19,5 +19,6 @@ module.exports = {
|
||||||
env: require("./environment"),
|
env: require("./environment"),
|
||||||
accounts: require("./cloud/accounts"),
|
accounts: require("./cloud/accounts"),
|
||||||
tenancy: require("./tenancy"),
|
tenancy: require("./tenancy"),
|
||||||
|
context: require("../context"),
|
||||||
featureFlags: require("./featureFlags"),
|
featureFlags: require("./featureFlags"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
const { Cookies, Configs } = require("../../../constants")
|
const { Cookies, Configs } = require("../../../constants")
|
||||||
const { clearCookie, getCookie } = require("../../../utils")
|
const { clearCookie, getCookie } = require("../../../utils")
|
||||||
const { getScopedConfig } = require("../../../db/utils")
|
const { getScopedConfig, getPlatformUrl } = require("../../../db/utils")
|
||||||
const { doWithDB } = require("../../../db")
|
const { doWithDB } = require("../../../db")
|
||||||
const environment = require("../../../environment")
|
const environment = require("../../../environment")
|
||||||
const { getGlobalDB } = require("../../../tenancy")
|
const { getGlobalDB } = require("../../../tenancy")
|
||||||
|
@ -21,30 +21,10 @@ async function fetchGoogleCreds() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPlatformUrl() {
|
|
||||||
if (environment.PLATFORM_URL) {
|
|
||||||
return environment.PLATFORM_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
let platformUrl = "http://localhost:10000"
|
|
||||||
|
|
||||||
const db = getGlobalDB()
|
|
||||||
const settings = await getScopedConfig(db, {
|
|
||||||
type: Configs.SETTINGS,
|
|
||||||
})
|
|
||||||
|
|
||||||
// self hosted - check for platform url override
|
|
||||||
if (settings && settings.platformUrl) {
|
|
||||||
platformUrl = settings.platformUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
return platformUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
async function preAuth(passport, ctx, next) {
|
async function preAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const googleConfig = await fetchGoogleCreds()
|
const googleConfig = await fetchGoogleCreds()
|
||||||
const platformUrl = await getPlatformUrl()
|
const platformUrl = await getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||||
|
@ -63,7 +43,7 @@ async function preAuth(passport, ctx, next) {
|
||||||
async function postAuth(passport, ctx, next) {
|
async function postAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const config = await fetchGoogleCreds()
|
const config = await fetchGoogleCreds()
|
||||||
const platformUrl = await getPlatformUrl()
|
const platformUrl = await getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
|
|
|
@ -1493,6 +1493,11 @@ domexception@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
webidl-conversions "^5.0.0"
|
webidl-conversions "^5.0.0"
|
||||||
|
|
||||||
|
dotenv@^16.0.1:
|
||||||
|
version "16.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||||
|
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||||
|
|
||||||
double-ended-queue@2.1.0-0:
|
double-ended-queue@2.1.0-0:
|
||||||
version "2.1.0-0"
|
version "2.1.0-0"
|
||||||
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.191",
|
"version": "1.0.192-alpha.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.191",
|
"@budibase/string-templates": "^1.0.192-alpha.1",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/fieldgroup/dist/index-vars.css"
|
||||||
|
import "@spectrum-css/radio/dist/index-vars.css"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let direction = "vertical"
|
||||||
|
export let value = []
|
||||||
|
export let options = []
|
||||||
|
export let error = null
|
||||||
|
export let disabled = false
|
||||||
|
export let getOptionLabel = option => option
|
||||||
|
export let getOptionValue = option => option
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
let tempValue = value
|
||||||
|
let isChecked = e.target.checked
|
||||||
|
if (!tempValue.includes(e.target.value) && isChecked) {
|
||||||
|
tempValue.push(e.target.value)
|
||||||
|
}
|
||||||
|
value = tempValue
|
||||||
|
dispatch(
|
||||||
|
"change",
|
||||||
|
tempValue.filter(val => val !== e.target.value || isChecked)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
||||||
|
{#if options && Array.isArray(options)}
|
||||||
|
{#each options as option}
|
||||||
|
<div
|
||||||
|
title={getOptionLabel(option)}
|
||||||
|
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
on:change={onChange}
|
||||||
|
value={getOptionValue(option)}
|
||||||
|
type="checkbox"
|
||||||
|
class="spectrum-Checkbox-input"
|
||||||
|
{disabled}
|
||||||
|
checked={value.includes(getOptionValue(option))}
|
||||||
|
/>
|
||||||
|
<span class="spectrum-Checkbox-box">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Checkbox-input {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -161,8 +161,8 @@
|
||||||
<input
|
<input
|
||||||
data-input
|
data-input
|
||||||
type="text"
|
type="text"
|
||||||
{disabled}
|
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
class:is-disabled={disabled}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{id}
|
{id}
|
||||||
{value}
|
{value}
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
{disabled}
|
class:is-disabled={disabled}
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
on:click={flatpickr?.open}
|
on:click={flatpickr?.open}
|
||||||
>
|
>
|
||||||
|
@ -217,4 +217,7 @@
|
||||||
:global(.flatpickr-calendar) {
|
:global(.flatpickr-calendar) {
|
||||||
font-family: "Source Sans Pro", sans-serif;
|
font-family: "Source Sans Pro", sans-serif;
|
||||||
}
|
}
|
||||||
|
.is-disabled {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let fileSizeLimit = BYTES_IN_MB * 20
|
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||||
export let processFiles = null
|
export let processFiles = null
|
||||||
export let handleFileTooLarge = null
|
export let handleFileTooLarge = null
|
||||||
|
export let handleTooManyFiles = null
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
export let error = null
|
export let error = null
|
||||||
export let fileTags = []
|
export let fileTags = []
|
||||||
|
@ -71,6 +72,13 @@
|
||||||
handleFileTooLarge(fileSizeLimit, value)
|
handleFileTooLarge(fileSizeLimit, value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileCount = fileList.length + value.length
|
||||||
|
if (handleTooManyFiles && maximum && fileCount > maximum) {
|
||||||
|
handleTooManyFiles(maximum)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (processFiles) {
|
if (processFiles) {
|
||||||
const processedFiles = await processFiles(fileList)
|
const processedFiles = await processFiles(fileList)
|
||||||
const newValue = [...value, ...processedFiles]
|
const newValue = [...value, ...processedFiles]
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
searchTerm = null
|
searchTerm = null
|
||||||
open = true
|
open = !open
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSortedOptions = (options, getLabel, sort) => {
|
const getSortedOptions = (options, getLabel, sort) => {
|
||||||
|
@ -71,105 +71,73 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<div use:clickOutside={() => (open = false)}>
|
||||||
{id}
|
<button
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM"
|
{id}
|
||||||
class:spectrum-Picker--quiet={quiet}
|
class="spectrum-Picker spectrum-Picker--sizeM"
|
||||||
{disabled}
|
class:spectrum-Picker--quiet={quiet}
|
||||||
class:is-invalid={!!error}
|
{disabled}
|
||||||
class:is-open={open}
|
class:is-invalid={!!error}
|
||||||
aria-haspopup="listbox"
|
class:is-open={open}
|
||||||
on:mousedown={onClick}
|
aria-haspopup="listbox"
|
||||||
>
|
on:mousedown={onClick}
|
||||||
{#if fieldIcon}
|
|
||||||
<span class="icon-Placeholder-Padding">
|
|
||||||
<img src={fieldIcon} alt="icon" width="20" height="15" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="spectrum-Picker-label"
|
|
||||||
class:is-placeholder={isPlaceholder}
|
|
||||||
class:auto-width={autoWidth}
|
|
||||||
>
|
>
|
||||||
{fieldText}
|
{#if fieldIcon}
|
||||||
</span>
|
<span class="icon-Placeholder-Padding">
|
||||||
{#if error}
|
<img src={fieldIcon} alt="icon" width="20" height="15" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="spectrum-Picker-label"
|
||||||
|
class:is-placeholder={isPlaceholder}
|
||||||
|
class:auto-width={autoWidth}
|
||||||
|
>
|
||||||
|
{fieldText}
|
||||||
|
</span>
|
||||||
|
{#if error}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Folder"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-label="Folder"
|
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
</button>
|
||||||
<svg
|
{#if open}
|
||||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
<div
|
||||||
focusable="false"
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
aria-hidden="true"
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
>
|
class:auto-width={autoWidth}
|
||||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
>
|
||||||
</svg>
|
{#if autocomplete}
|
||||||
</button>
|
<Search
|
||||||
{#if open}
|
value={searchTerm}
|
||||||
<div
|
on:change={event => (searchTerm = event.detail)}
|
||||||
use:clickOutside={() => (open = false)}
|
{disabled}
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
placeholder="Search"
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
/>
|
||||||
class:auto-width={autoWidth}
|
|
||||||
>
|
|
||||||
{#if autocomplete}
|
|
||||||
<Search
|
|
||||||
value={searchTerm}
|
|
||||||
on:change={event => (searchTerm = event.detail)}
|
|
||||||
{disabled}
|
|
||||||
placeholder="Search"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
|
||||||
{#if placeholderOption}
|
|
||||||
<li
|
|
||||||
class="spectrum-Menu-item placeholder"
|
|
||||||
class:is-selected={isPlaceholder}
|
|
||||||
role="option"
|
|
||||||
aria-selected="true"
|
|
||||||
tabindex="0"
|
|
||||||
on:click={() => onSelectOption(null)}
|
|
||||||
>
|
|
||||||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
|
||||||
</svg>
|
|
||||||
</li>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if filteredOptions.length}
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
{#each filteredOptions as option, idx}
|
{#if placeholderOption}
|
||||||
<li
|
<li
|
||||||
class="spectrum-Menu-item"
|
class="spectrum-Menu-item placeholder"
|
||||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
class:is-selected={isPlaceholder}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
on:click={() => onSelectOption(null)}
|
||||||
>
|
>
|
||||||
{#if getOptionIcon(option, idx)}
|
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||||
<span class="icon-Padding">
|
|
||||||
<img
|
|
||||||
src={getOptionIcon(option, idx)}
|
|
||||||
alt="icon"
|
|
||||||
width="20"
|
|
||||||
height="15"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<span class="spectrum-Menu-itemLabel">
|
|
||||||
{getOptionLabel(option, idx)}
|
|
||||||
</span>
|
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -178,11 +146,44 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
</svg>
|
</svg>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/if}
|
||||||
{/if}
|
{#if filteredOptions.length}
|
||||||
</ul>
|
{#each filteredOptions as option, idx}
|
||||||
</div>
|
<li
|
||||||
{/if}
|
class="spectrum-Menu-item"
|
||||||
|
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||||
|
>
|
||||||
|
{#if getOptionIcon(option, idx)}
|
||||||
|
<span class="icon-Padding">
|
||||||
|
<img
|
||||||
|
src={getOptionIcon(option, idx)}
|
||||||
|
alt="icon"
|
||||||
|
width="20"
|
||||||
|
height="15"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
{getOptionLabel(option, idx)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
|
@ -13,8 +13,11 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let dataCy
|
export let dataCy
|
||||||
export let align
|
export let align
|
||||||
|
export let autofocus = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let field
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
const updateValue = newValue => {
|
const updateValue = newValue => {
|
||||||
|
@ -58,6 +61,11 @@
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
focus = autofocus
|
||||||
|
if (focus) field.focus()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -77,6 +85,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<input
|
<input
|
||||||
|
bind:this={field}
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{id}
|
{id}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export { default as CoreSelect } from "./Select.svelte"
|
||||||
export { default as CoreMultiselect } from "./Multiselect.svelte"
|
export { default as CoreMultiselect } from "./Multiselect.svelte"
|
||||||
export { default as CoreCheckbox } from "./Checkbox.svelte"
|
export { default as CoreCheckbox } from "./Checkbox.svelte"
|
||||||
export { default as CoreRadioGroup } from "./RadioGroup.svelte"
|
export { default as CoreRadioGroup } from "./RadioGroup.svelte"
|
||||||
|
export { default as CoreCheckboxGroup } from "./CheckboxGroup.svelte"
|
||||||
export { default as CoreTextArea } from "./TextArea.svelte"
|
export { default as CoreTextArea } from "./TextArea.svelte"
|
||||||
export { default as CoreCombobox } from "./Combobox.svelte"
|
export { default as CoreCombobox } from "./Combobox.svelte"
|
||||||
export { default as CoreSwitch } from "./Switch.svelte"
|
export { default as CoreSwitch } from "./Switch.svelte"
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let fileSizeLimit = undefined
|
export let fileSizeLimit = undefined
|
||||||
export let processFiles = undefined
|
export let processFiles = undefined
|
||||||
export let handleFileTooLarge = undefined
|
export let handleFileTooLarge = undefined
|
||||||
|
export let handleTooManyFiles = undefined
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
export let fileTags = []
|
export let fileTags = []
|
||||||
export let maximum = undefined
|
export let maximum = undefined
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
{fileSizeLimit}
|
{fileSizeLimit}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
|
{handleTooManyFiles}
|
||||||
{gallery}
|
{gallery}
|
||||||
{fileTags}
|
{fileTags}
|
||||||
{maximum}
|
{maximum}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let dataCy
|
export let dataCy
|
||||||
|
export let autofocus
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{type}
|
{type}
|
||||||
{quiet}
|
{quiet}
|
||||||
|
{autofocus}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
|
|
|
@ -40,6 +40,10 @@
|
||||||
padding-left: var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.paddingX-XXL {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
.paddingY-S {
|
.paddingY-S {
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-s);
|
padding-bottom: var(--spacing-s);
|
||||||
|
@ -56,6 +60,10 @@
|
||||||
padding-top: var(--spacing-xl);
|
padding-top: var(--spacing-xl);
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.paddingY-XXL {
|
||||||
|
padding-top: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-bottom: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
.gap-XXS {
|
.gap-XXS {
|
||||||
grid-gap: var(--spacing-xs);
|
grid-gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
export let wide = false
|
export let wide = false
|
||||||
export let maxWidth = "80ch"
|
export let maxWidth = "80ch"
|
||||||
|
export let noPadding = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="--max-width: {maxWidth}" class:wide>
|
<div style="--max-width: {maxWidth}" class:wide class:noPadding>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -23,4 +24,9 @@
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noPadding {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let dataCy
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
class:is-selected={selected}
|
class:is-selected={selected}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
on:click
|
on:click
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
{#if heading}
|
{#if heading}
|
||||||
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab
|
let tab_internal
|
||||||
let tabInfo
|
let tabInfo
|
||||||
|
|
||||||
const setTabInfo = () => {
|
const setTabInfo = () => {
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
// We just need to get this off the main thread to fix this, by using
|
// We just need to get this off the main thread to fix this, by using
|
||||||
// a 0ms timeout.
|
// a 0ms timeout.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tabInfo = tab?.getBoundingClientRect()
|
tabInfo = tab_internal?.getBoundingClientRect()
|
||||||
if (tabInfo && $selected.title === title) {
|
if (tabInfo && $selected.title === title) {
|
||||||
$selected.info = tabInfo
|
$selected.info = tabInfo
|
||||||
}
|
}
|
||||||
|
@ -27,14 +27,30 @@
|
||||||
setTabInfo()
|
setTabInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Ensure that the underline is in the correct location
|
||||||
|
$: {
|
||||||
|
if ($selected.title === title && tab_internal) {
|
||||||
|
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
||||||
|
$selected = {
|
||||||
|
...$selected,
|
||||||
|
info: tab_internal.getBoundingClientRect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
$selected = { ...$selected, title, info: tab.getBoundingClientRect() }
|
$selected = {
|
||||||
|
...$selected,
|
||||||
|
title,
|
||||||
|
info: tab_internal.getBoundingClientRect(),
|
||||||
|
}
|
||||||
dispatch("click")
|
dispatch("click")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={tab}
|
bind:this={tab_internal}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
class:is-selected={$selected.title === title}
|
||||||
class="spectrum-Tabs-item"
|
class="spectrum-Tabs-item"
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:4100",
|
"baseUrl": "http://localhost:4100",
|
||||||
"video": false,
|
"video": true,
|
||||||
"projectId": "bmbemn",
|
"projectId": "bmbemn",
|
||||||
|
"reporter": "cypress-multi-reporters",
|
||||||
|
"reporterOptions": {
|
||||||
|
"configFile": "reporterConfig.json"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "4100",
|
"PORT": "4100",
|
||||||
"WORKER_PORT": "4200",
|
"WORKER_PORT": "4200",
|
||||||
"JWT_SECRET": "test",
|
"JWT_SECRET": "test",
|
||||||
"HOST_IP": ""
|
"HOST_IP": ""
|
||||||
|
},
|
||||||
|
"retries": {
|
||||||
|
"runMode": 2,
|
||||||
|
"openMode": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,5 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['all'], () => {
|
filterTests(['all'], () => {
|
||||||
context("Add Multi-Option Datatype", () => {
|
context("Add Multi-Option Datatype", () => {
|
||||||
|
@ -17,19 +18,19 @@ filterTests(['all'], () => {
|
||||||
cy.navigateToFrontend()
|
cy.navigateToFrontend()
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
// Add data provider
|
// Add data provider
|
||||||
cy.get(`[data-cy="category-Data"]`).click()
|
cy.get(interact.CATEGORY_DATA).click()
|
||||||
cy.get(`[data-cy="component-Data Provider"]`).click()
|
cy.get(interact.COMPONENT_DATA_PROVIDER).click()
|
||||||
cy.get('[data-cy="dataSource-prop-control"]').click()
|
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
|
||||||
cy.get(".dropdown").contains("Multi Data").click()
|
cy.get(interact.DROPDOWN).contains("Multi Data").click()
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
// Add Form with schema to match table
|
// Add Form with schema to match table
|
||||||
cy.addComponent("Form", "Form")
|
cy.addComponent("Form", "Form")
|
||||||
cy.get('[data-cy="dataSource-prop-control"').click()
|
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
|
||||||
cy.get(".dropdown").contains("Multi Data").click()
|
cy.get(interact.DROPDOWN).contains("Multi Data").click()
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
// Add multi-select picker to form
|
// Add multi-select picker to form
|
||||||
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
|
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
|
||||||
cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}")
|
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}")
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.getComponent(componentId).contains("Choose some options").click()
|
cy.getComponent(componentId).contains("Choose some options").click()
|
||||||
// Check picker has 5 items
|
// Check picker has 5 items
|
||||||
|
@ -40,7 +41,7 @@ filterTests(['all'], () => {
|
||||||
}
|
}
|
||||||
// Check items have been selected
|
// Check items have been selected
|
||||||
cy.getComponent(componentId)
|
cy.getComponent(componentId)
|
||||||
.find(".spectrum-Picker-label")
|
.find(interact.SPECTRUM_PICKER_LABEL)
|
||||||
.contains("(5)")
|
.contains("(5)")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['all'], () => {
|
filterTests(['all'], () => {
|
||||||
context("Add Radio Buttons", () => {
|
context("Add Radio Buttons", () => {
|
||||||
|
@ -12,10 +13,10 @@ filterTests(['all'], () => {
|
||||||
cy.addComponent("Form", "Form")
|
cy.addComponent("Form", "Form")
|
||||||
cy.addComponent("Form", "Options Picker").then((componentId) => {
|
cy.addComponent("Form", "Options Picker").then((componentId) => {
|
||||||
// Provide field setting
|
// Provide field setting
|
||||||
cy.get(`[data-cy="field-prop-control"]`).type("1")
|
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1")
|
||||||
// Open dropdown and select Radio buttons
|
// Open dropdown and select Radio buttons
|
||||||
cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
|
cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => {
|
||||||
cy.get('.spectrum-Popover').contains('Radio buttons')
|
cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons')
|
||||||
.wait(500)
|
.wait(500)
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
@ -28,8 +29,8 @@ filterTests(['all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const addRadioButtonData = (totalRadioButtons) => {
|
const addRadioButtonData = (totalRadioButtons) => {
|
||||||
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
|
cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => {
|
||||||
cy.get('.spectrum-Popover').contains('Custom')
|
cy.get(interact.SPECTRUM_POPOVER).contains('Custom')
|
||||||
.wait(500)
|
.wait(500)
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Application Overview screen", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should be accessible from the applications list", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .title").eq(0)
|
||||||
|
.invoke('attr', 'data-cy')
|
||||||
|
.then(($dataCy) => {
|
||||||
|
const dataCy = $dataCy;
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .title").eq(0)
|
||||||
|
.invoke('attr', 'data-cy')
|
||||||
|
.then(($dataCy) => {
|
||||||
|
const dataCy = $dataCy;
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("View").click({force: true})
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find a more suitable place for this.
|
||||||
|
it("Should allow unlocking in the app list", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click()
|
||||||
|
|
||||||
|
cy.unlockApp({ owned : true })
|
||||||
|
|
||||||
|
cy.get(".appTable").should("exist")
|
||||||
|
cy.get(".lock-status").should('not.be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow unlocking in the app overview screen", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".lock-status").eq(0).contains("Locked by you").click()
|
||||||
|
|
||||||
|
cy.unlockApp({ owned : true })
|
||||||
|
|
||||||
|
cy.get(".lock-status").should("not.be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the deploy state of an app that hasn't been published.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
|
||||||
|
cy.get(".overview-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".status-text").contains("-")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the app deployment state", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Published")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist")
|
||||||
|
cy.get(".status-text").contains("Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect an application that has been unpublished", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
||||||
|
.click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".status-text").contains("Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow the editing of the application icon", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-logo .edit-hover").should("exist").invoke("show").click()
|
||||||
|
|
||||||
|
cy.customiseAppIcon()
|
||||||
|
|
||||||
|
cy.get(".app-logo")
|
||||||
|
.within(() => {
|
||||||
|
cy.get('[aria-label]').eq(0).children()
|
||||||
|
.should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps')
|
||||||
|
cy.get(".app-icon")
|
||||||
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the last time the application was edited", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button").contains("Edit").click({ force: true });
|
||||||
|
|
||||||
|
cy.navigateToFrontend()
|
||||||
|
|
||||||
|
cy.addComponent("Elements", "Headline").then(componentId => {
|
||||||
|
cy.getComponent(componentId).should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='edited-by']").within(() => {
|
||||||
|
cy.get(".editor-name").contains("You")
|
||||||
|
cy.get(".last-edit-text").contains("Last edited a few seconds ago")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should reflect application version is up-to-date", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-version']").within(() => {
|
||||||
|
cy.get(".version-status").contains("You're running the latest!")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should navigate to the settings tab when clicking the App Version card header", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
|
||||||
|
cy.get(".overview-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
cy.get(".settings-tab").should("be.visible")
|
||||||
|
cy.get(".overview-tab").should("not.exist")
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should allow the upgrading of an application, if available.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
cy.location().then(loc => {
|
||||||
|
const params = loc.pathname.split("/")
|
||||||
|
const appId = params[params.length - 1]
|
||||||
|
cy.log(appId)
|
||||||
|
//Downgrade the app for the test
|
||||||
|
cy.alterAppVersion(appId, "0.0.1-alpha.0")
|
||||||
|
.then(()=>{
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.log("Current deployment version: " + clientPackage.version)
|
||||||
|
|
||||||
|
cy.get(".version-status a").contains("Update").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
|
||||||
|
cy.get(".version-section .page-action button").contains("Update").click({ force: true })
|
||||||
|
|
||||||
|
cy.intercept('POST', '**/applications/**/client/update').as('updateVersion')
|
||||||
|
cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true })
|
||||||
|
|
||||||
|
cy.wait("@updateVersion")
|
||||||
|
.its('response.statusCode').should('eq', 200)
|
||||||
|
.then(() => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true })
|
||||||
|
cy.get(".overview-tab [data-cy='app-version']").within(() => {
|
||||||
|
cy.get(".spectrum-Heading").contains(clientPackage.version)
|
||||||
|
cy.get(".version-status").contains("You're running the latest!")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow editing of the app details.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Settings").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
cy.get(".settings-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".details-section .page-action button").contains("Edit").click({ force: true })
|
||||||
|
cy.updateAppName("sample name")
|
||||||
|
|
||||||
|
//publish and check its disabled
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Settings").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
|
||||||
|
cy.get(".details-section .page-action .spectrum-Button").scrollIntoView()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".details-section .page-action .spectrum-Button").should("be.disabled")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow copying of the published application Id", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.publishApp("sample-name")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow unpublishing of the application", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow deleting of the application", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
//The test application was renamed earlier in the spec
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type("sample name")
|
||||||
|
cy.get(".spectrum-Button--warning").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/apps')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable").should("not.exist")
|
||||||
|
|
||||||
|
cy.get(".welcome .container h1").contains("Let's create your first app!")
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,4 +1,5 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['all'], () => {
|
filterTests(['all'], () => {
|
||||||
context("Publish Application Workflow", () => {
|
context("Publish Application Workflow", () => {
|
||||||
|
@ -11,63 +12,63 @@ filterTests(['all'], () => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
|
||||||
cy.get(".appTable .app-status").eq(0)
|
cy.get(interact.APP_TABLE_STATUS).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.contains("Unpublished")
|
cy.contains("Unpublished")
|
||||||
cy.get("svg[aria-label='GlobeStrike']").should("exist")
|
cy.get(interact.GLOBESTRIKE).should("exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(interact.APP_TABLE_ROW_ACTION).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Preview")
|
cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Preview")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist")
|
||||||
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("not.exist")
|
cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should publish an application and correctly reflect that", () => {
|
it("Should publish an application and correctly reflect that", () => {
|
||||||
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
||||||
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true })
|
||||||
|
|
||||||
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
cy.get(interact.DEPLOY_APP_MODAL).should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force : true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
});
|
});
|
||||||
|
|
||||||
//Verify that the app url is presented correctly to the user
|
//Verify that the app url is presented correctly to the user
|
||||||
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
cy.get(interact.DEPLOY_APP_MODAL)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
|
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
|
||||||
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
|
cy.get(interact.DEPLOY_APP_URL_INPUT).should('have.value', appUrl)
|
||||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Done").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
|
||||||
cy.get(".appTable .app-status").eq(0)
|
cy.get(interact.APP_TABLE_STATUS).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.contains("Published")
|
cy.contains("Published")
|
||||||
cy.get("svg[aria-label='Globe']").should("exist")
|
cy.get(interact.GLOBE).should("exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(interact.APP_TABLE_ROW_ACTION).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(interact.SPECTRUM_BUTTON).contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist").click({ force: true })
|
cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
|
||||||
|
|
||||||
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
cy.get(interact.PUBLISH_POPOVER_MENU).should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("[data-cy='publish-popover-action']").should("exist")
|
cy.get(interact.PUBLISH_POPOVER_ACTION).should("exist")
|
||||||
cy.get("button").contains("View app").should("exist")
|
cy.get("button").contains("View app").should("exist")
|
||||||
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
cy.get(interact.PUBLISH_POPOVER_MESSAGE).should("have.text", "Last published a few seconds ago")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -76,36 +77,36 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
cy.get(".appTable .app-status").eq(0)
|
cy.get(interact.APP_TABLE_STATUS).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.contains("Published")
|
cy.contains("Published")
|
||||||
cy.get("svg[aria-label='Globe']").should("exist")
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(interact.APP_TABLE_ROW_ACTION).eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(interact.SPECTRUM_BUTTON).contains("View app")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
//The published status
|
//The published status
|
||||||
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist")
|
cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
|
||||||
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
cy.get(interact.PUBLISH_POPOVER_MENU).should("be.visible")
|
||||||
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
||||||
.click({ force : true })
|
.click({ force : true })
|
||||||
|
|
||||||
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
cy.get(interact.UNPUBLISH_MODAL).should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".confirm-wrap button").click({ force: true }
|
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }
|
||||||
)})
|
)})
|
||||||
|
|
||||||
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist")
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
cy.get(".appTable .app-status").eq(0).contains("Unpublished")
|
cy.get(interact.APP_TABLE_STATUS).eq(0).contains("Unpublished")
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Auto Screens UI", () => {
|
context("Auto Screens UI", () => {
|
||||||
|
@ -12,10 +13,10 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.closeModal();
|
cy.closeModal();
|
||||||
|
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get(interact.LABEL_ADD_CIRCLE).click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||||
cy.get(".item.disabled").contains("Autogenerated screens")
|
cy.get(interact.ITEM_DISABLED).contains("Autogenerated screens")
|
||||||
cy.get(".confirm-wrap .spectrum-Button").should('be.disabled')
|
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.deleteAllApps()
|
cy.deleteAllApps()
|
||||||
|
@ -26,14 +27,14 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
cy.selectExternalDatasource("REST")
|
cy.selectExternalDatasource("REST")
|
||||||
cy.selectExternalDatasource("S3")
|
cy.selectExternalDatasource("S3")
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Save and continue to query").click({ force : true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Save and continue to query").click({ force : true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.navigateToAutogeneratedModal()
|
cy.navigateToAutogeneratedModal()
|
||||||
|
|
||||||
cy.get('.data-source-entry').should('have.length', 1)
|
cy.get(interact.DATA_SOURCE_ENTRY).should('have.length', 1)
|
||||||
cy.get('.data-source-entry')
|
cy.get(interact.DATA_SOURCE_ENTRY)
|
||||||
|
|
||||||
cy.deleteAllApps()
|
cy.deleteAllApps()
|
||||||
});
|
});
|
||||||
|
@ -43,8 +44,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
// Create Autogenerated screens from the internal table
|
// Create Autogenerated screens from the internal table
|
||||||
cy.createDatasourceScreen(["Cypress Tests"])
|
cy.createDatasourceScreen(["Cypress Tests"])
|
||||||
// Confirm screens have been auto generated
|
// Confirm screens have been auto generated
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true })
|
||||||
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests/:id')
|
||||||
.and('contain', 'cypress-tests/new/row')
|
.and('contain', 'cypress-tests/new/row')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,12 +57,12 @@ filterTests(['smoke', 'all'], () => {
|
||||||
// Create Autogenerated screens from the internal tables
|
// Create Autogenerated screens from the internal tables
|
||||||
cy.createDatasourceScreen([initialTable, secondTable])
|
cy.createDatasourceScreen([initialTable, secondTable])
|
||||||
// Confirm screens have been auto generated
|
// Confirm screens have been auto generated
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true })
|
||||||
// Previously generated tables are suffixed with numbers - as expected
|
// Previously generated tables are suffixed with numbers - as expected
|
||||||
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests-2/:id')
|
||||||
.and('contain', 'cypress-tests-2/new/row')
|
.and('contain', 'cypress-tests-2/new/row')
|
||||||
cy.get(".nav-items-container").contains("table-two").click()
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").click()
|
||||||
cy.get(".nav-items-container").should('contain', 'table-two/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-two/:id')
|
||||||
.and('contain', 'table-two/new/row')
|
.and('contain', 'table-two/new/row')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -71,17 +72,17 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.createTable("Table Four")
|
cy.createTable("Table Four")
|
||||||
cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin")
|
cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin")
|
||||||
|
|
||||||
cy.get(".nav-items-container").contains("table-three").click()
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-three").click()
|
||||||
cy.get(".nav-items-container").should('contain', 'table-three/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-three/:id')
|
||||||
.and('contain', 'table-three/new/row')
|
.and('contain', 'table-three/new/row')
|
||||||
|
|
||||||
cy.get(".nav-items-container").contains("table-four").click()
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-four").click()
|
||||||
cy.get(".nav-items-container").should('contain', 'table-four/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-four/:id')
|
||||||
.and('contain', 'table-four/new/row')
|
.and('contain', 'table-four/new/row')
|
||||||
|
|
||||||
//The access level should now be set to admin. Previous screens should be filtered.
|
//The access level should now be set to admin. Previous screens should be filtered.
|
||||||
cy.get(".nav-items-container").contains("table-two").should('not.exist')
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").should('not.exist')
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").should('not.exist')
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
@ -94,8 +95,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
cy.createDatasourceScreen(["books"])
|
cy.createDatasourceScreen(["books"])
|
||||||
|
|
||||||
cy.get(".nav-items-container").contains("books").click()
|
cy.get(interact.NAV_ITEMS_CONTAINER).contains("books").click()
|
||||||
cy.get(".nav-items-container").should('contain', 'books/:id')
|
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'books/:id')
|
||||||
.and('contain', 'books/new/row')
|
.and('contain', 'books/new/row')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import filterTests from '../support/filterTests'
|
import filterTests from '../support/filterTests'
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Create an Application", () => {
|
context("Create an Application", () => {
|
||||||
|
@ -10,14 +11,14 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
if (!(Cypress.env("TEST_ENV"))) {
|
if (!(Cypress.env("TEST_ENV"))) {
|
||||||
it("should show the new user UI/UX", () => {
|
it("should show the new user UI/UX", () => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create
|
||||||
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist")
|
cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist")
|
||||||
cy.get(`[data-cy="import-app-btn"]`).should("exist")
|
cy.get(interact.CREATE_APP_BUTTON).should("exist")
|
||||||
|
|
||||||
cy.get(".template-category-filters").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
|
||||||
cy.get(".template-categories").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
|
||||||
|
|
||||||
cy.get(".appTable").should("not.exist")
|
cy.get(interact.APP_TABLE).should("not.exist")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,21 +30,21 @@ filterTests(['smoke', 'all'], () => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
cy.get(interact.SPECTRUM_BUTTON).contains("Templates").click({force: true})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".template-category-filters").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
|
||||||
cy.get(".template-categories").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
|
||||||
|
|
||||||
cy.get(".template-category").its('length').should('be.gt', 1)
|
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
|
||||||
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2)
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).its('length').should('be.gt', 2)
|
||||||
|
|
||||||
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click()
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(1).click()
|
||||||
cy.get(".template-category").should('have.length', 1)
|
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).should('have.length', 1)
|
||||||
|
|
||||||
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click()
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(0).click()
|
||||||
cy.get(".template-category").its('length').should('be.gt', 1)
|
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should enforce a valid url before submission", () => {
|
it("should enforce a valid url before submission", () => {
|
||||||
|
@ -51,37 +52,40 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
|
|
||||||
// Start create app process. If apps already exist, click second button
|
// Start create app process. If apps already exist, click second button
|
||||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||||
|
|
||||||
|
cy.get(interact.APP_NAME_INPUT).eq(0).should('have.focus')
|
||||||
|
|
||||||
//Auto fill
|
//Auto fill
|
||||||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
cy.get(interact.APP_NAME_INPUT).eq(0).clear()
|
||||||
cy.get("input").eq(1).should("have.value", "/cypress-tests")
|
cy.get(interact.APP_NAME_INPUT).eq(0).type(appName).should("have.value", appName).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/cypress-tests")
|
||||||
|
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
|
||||||
|
|
||||||
//Empty the app url - disabled create
|
//Empty the app url - disabled create
|
||||||
cy.get("input").eq(1).clear().blur()
|
cy.get(interact.APP_NAME_INPUT).eq(1).clear().blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
|
||||||
|
|
||||||
//Invalid url
|
//Invalid url
|
||||||
cy.get("input").eq(1).type("/new app-url").blur()
|
cy.get(interact.APP_NAME_INPUT).eq(1).type("/new app-url").blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
|
||||||
|
|
||||||
//Specifically alter the url
|
//Specifically alter the url
|
||||||
cy.get("input").eq(1).clear()
|
cy.get(interact.APP_NAME_INPUT).eq(1).clear()
|
||||||
cy.get("input").eq(1).type("another-app-name").blur()
|
cy.get(interact.APP_NAME_INPUT).eq(1).type("another-app-name").blur()
|
||||||
cy.get("input").eq(1).should("have.value", "/another-app-name")
|
cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/another-app-name")
|
||||||
cy.get("input").eq(0).should("have.value", appName)
|
cy.get(interact.APP_NAME_INPUT).eq(0).should("have.value", appName)
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -97,6 +101,77 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should create the first application from scratch with a default name", () => {
|
||||||
|
cy.createApp()
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.applicationInAppTable("My app")
|
||||||
|
cy.deleteApp("My app")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create the first application from scratch, using the users first name as the default app name", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.updateUserInformation("Ted", "Userman")
|
||||||
|
|
||||||
|
cy.createApp()
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.applicationInAppTable("Teds app")
|
||||||
|
cy.deleteApp("Teds app")
|
||||||
|
cy.wait(2000)
|
||||||
|
|
||||||
|
//Accomodate names that end in 'S'
|
||||||
|
cy.updateUserInformation("Chris", "Userman")
|
||||||
|
|
||||||
|
cy.createApp()
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.applicationInAppTable("Chris app")
|
||||||
|
cy.deleteApp("Chris app")
|
||||||
|
cy.wait(2000)
|
||||||
|
|
||||||
|
cy.updateUserInformation("", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create an application from an export", () => {
|
||||||
|
const exportedApp = 'cypress/fixtures/exported-app.txt'
|
||||||
|
|
||||||
|
cy.importApp(exportedApp, "")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.applicationInAppTable("My app")
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.deleteApp("My app")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create an application from an export, using the users first name as the default app name", () => {
|
||||||
|
const exportedApp = 'cypress/fixtures/exported-app.txt'
|
||||||
|
|
||||||
|
cy.updateUserInformation("Ted", "Userman")
|
||||||
|
|
||||||
|
cy.importApp(exportedApp, "")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.applicationInAppTable("Teds app")
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.deleteApp("Teds app")
|
||||||
|
|
||||||
|
cy.updateUserInformation("", "")
|
||||||
|
})
|
||||||
|
|
||||||
it("should generate the first application from a template", () => {
|
it("should generate the first application from a template", () => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
|
@ -106,15 +181,15 @@ filterTests(['smoke', 'all'], () => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".template-category-filters").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
|
||||||
cy.get(".template-categories").should("exist")
|
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
|
||||||
|
|
||||||
// Select template
|
// Select template
|
||||||
cy.get('.template-category').eq(0).within(() => {
|
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).eq(0).within(() => {
|
||||||
const card = cy.get('.template-card').eq(0).should("exist");
|
const card = cy.get('.template-card').eq(0).should("exist");
|
||||||
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
|
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
|
||||||
cardOverlay.invoke("show")
|
cardOverlay.invoke("show")
|
||||||
|
@ -128,14 +203,14 @@ filterTests(['smoke', 'all'], () => {
|
||||||
templateName.invoke('text')
|
templateName.invoke('text')
|
||||||
.then(templateNameText => {
|
.then(templateNameText => {
|
||||||
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
|
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
|
||||||
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText)
|
cy.get(interact.SPECTRUM_MODAL_INPUT).eq(0).should("have.value", templateNameText)
|
||||||
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed)
|
cy.get(interact.SPECTRUM_MODAL_INPUT).eq(1).should("have.value", templateNameParsed)
|
||||||
|
|
||||||
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
|
||||||
cy.wait(5000)
|
cy.wait(5000)
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(1000)
|
cy.wait(2000)
|
||||||
|
|
||||||
cy.applicationInAppTable(templateNameText)
|
cy.applicationInAppTable(templateNameText)
|
||||||
cy.deleteApp(templateNameText)
|
cy.deleteApp(templateNameText)
|
||||||
|
|
|
@ -24,7 +24,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add a URL param binding", () => {
|
xit("should add a URL param binding", () => {
|
||||||
const paramName = "foo"
|
const paramName = "foo"
|
||||||
cy.createScreen(`/test/:${paramName}`)
|
cy.createScreen(`/test/:${paramName}`)
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// TODO for now components are skipped, might not be good to keep doing this
|
// TODO for now components are skipped, might not be good to keep doing this
|
||||||
|
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
const interact = require('../support/interact')
|
||||||
|
|
||||||
filterTests(['all'], () => {
|
filterTests(['all'], () => {
|
||||||
xcontext("Create Components", () => {
|
xcontext("Create Components", () => {
|
||||||
|
@ -31,32 +32,32 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
it("should change the text of the headline", () => {
|
it("should change the text of the headline", () => {
|
||||||
const text = "Lorem ipsum dolor sit amet."
|
const text = "Lorem ipsum dolor sit amet."
|
||||||
cy.get("[data-cy=Settings]").click()
|
cy.get(interact.SETTINGS).click()
|
||||||
cy.get("[data-cy=setting-text] input")
|
cy.get(interact.SETTINGS_INPUT)
|
||||||
.type(text)
|
.type(text)
|
||||||
.blur()
|
.blur()
|
||||||
cy.getComponent(headlineId).should("have.text", text)
|
cy.getComponent(headlineId).should("have.text", text)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should change the size of the headline", () => {
|
it("should change the size of the headline", () => {
|
||||||
cy.get("[data-cy=Design]").click()
|
cy.get(interact.DESIGN).click()
|
||||||
cy.contains("Typography").click()
|
cy.contains("Typography").click()
|
||||||
cy.get("[data-cy=font-size-prop-control]").click()
|
cy.get(interact.FONT_SIZE_PROP_CONTROL).click()
|
||||||
cy.contains("60px").click()
|
cy.contains("60px").click()
|
||||||
cy.getComponent(headlineId).should("have.css", "font-size", "60px")
|
cy.getComponent(headlineId).should("have.css", "font-size", "60px")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a form and reset to match schema", () => {
|
it("should create a form and reset to match schema", () => {
|
||||||
cy.addComponent("Form", "Form").then(() => {
|
cy.addComponent("Form", "Form").then(() => {
|
||||||
cy.get("[data-cy=Settings]").click()
|
cy.get(interact.SETTINGS).click()
|
||||||
cy.get("[data-cy=setting-dataSource]")
|
cy.get(interact.DATA_CY_DATASOURCE)
|
||||||
.contains("Choose option")
|
.contains("Choose option")
|
||||||
.click()
|
.click()
|
||||||
cy.get(".dropdown")
|
cy.get(interact.DROPDOWN)
|
||||||
.contains("dog")
|
.contains("dog")
|
||||||
.click()
|
.click()
|
||||||
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
|
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
|
||||||
cy.get("[data-cy=Settings]").click()
|
cy.get(interact.SETTINGS).click()
|
||||||
cy.contains("Update Form Fields").click()
|
cy.contains("Update Form Fields").click()
|
||||||
cy.get(".modal")
|
cy.get(".modal")
|
||||||
.get("button.primary")
|
.get("button.primary")
|
||||||
|
@ -70,7 +71,7 @@ filterTests(['all'], () => {
|
||||||
.find("input")
|
.find("input")
|
||||||
.should("have.length", 2)
|
.should("have.length", 2)
|
||||||
cy.getComponent(fieldGroupId)
|
cy.getComponent(fieldGroupId)
|
||||||
.find(".spectrum-Picker")
|
.find(interact.SPECTRUM_PICKER)
|
||||||
.should("have.length", 1)
|
.should("have.length", 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -84,7 +85,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
|
cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
cy.get(".dropdown-container")
|
cy.get(interact.DROPDOWN_CONTAINER)
|
||||||
.contains("Delete")
|
.contains("Delete")
|
||||||
.click()
|
.click()
|
||||||
cy.get(".modal")
|
cy.get(".modal")
|
||||||
|
|
|
@ -9,7 +9,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should successfully create a screen", () => {
|
it("Should successfully create a screen", () => {
|
||||||
cy.createScreen("/test")
|
cy.createScreen("test")
|
||||||
cy.get(".nav-items-container").within(() => {
|
cy.get(".nav-items-container").within(() => {
|
||||||
cy.contains("/test").should("exist")
|
cy.contains("/test").should("exist")
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,8 @@ filterTests(["smoke", "all"], () => {
|
||||||
context("Create a User and Assign Roles", () => {
|
context("Create a User and Assign Roles", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
cy.deleteApp("Cypress Tests")
|
||||||
|
cy.createApp("Cypress Tests")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a user", () => {
|
it("should create a user", () => {
|
||||||
|
@ -52,7 +54,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(".spectrum-Table").contains("bbuser").click()
|
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table", { timeout: 3000})
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.find(".spectrum-Table-row")
|
.find(".spectrum-Table-row")
|
||||||
.eq(0)
|
.eq(0)
|
||||||
|
@ -66,19 +68,20 @@ filterTests(["smoke", "all"], () => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
cy.get(".spectrum-Menu").contains("Admin").click({ force: true })
|
||||||
}
|
}
|
||||||
if (i == 1) {
|
else if (i == 1) {
|
||||||
cy.get(".spectrum-Popover").contains("Power").click()
|
cy.get(".spectrum-Menu").contains("Power").click({ force: true })
|
||||||
}
|
}
|
||||||
if (i == 2) {
|
else if (i == 2) {
|
||||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
cy.get(".spectrum-Menu").contains("Basic").click({ force: true })
|
||||||
}
|
}
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.contains("Update role")
|
.contains("Update role")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
})
|
})
|
||||||
|
cy.reload()
|
||||||
}
|
}
|
||||||
// Confirm roles exist within Configure roles table
|
// Confirm roles exist within Configure roles table
|
||||||
cy.wait(2000)
|
cy.wait(2000)
|
||||||
|
|
|
@ -11,7 +11,7 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
it("Should add PostgreSQL data source without configuration", () => {
|
xit("Should add PostgreSQL data source without configuration", () => {
|
||||||
// Select PostgreSQL data source
|
// Select PostgreSQL data source
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
|
@ -107,7 +107,7 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a relationship", () => {
|
it("should delete a relationship", () => {
|
||||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||||
cy.reload()
|
cy.reload()
|
||||||
// Delete one relationship
|
// Delete one relationship
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table")
|
||||||
|
@ -155,7 +155,7 @@ filterTests(["all"], () => {
|
||||||
|
|
||||||
it("should switch to schema with no tables", () => {
|
it("should switch to schema with no tables", () => {
|
||||||
// Switch Schema - To one without any tables
|
// Switch Schema - To one without any tables
|
||||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||||
switchSchema("randomText")
|
switchSchema("randomText")
|
||||||
|
|
||||||
// No tables displayed
|
// No tables displayed
|
||||||
|
@ -207,7 +207,7 @@ filterTests(["all"], () => {
|
||||||
.contains(queryName)
|
.contains(queryName)
|
||||||
.siblings(".actions")
|
.siblings(".actions")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".icon").click({ force: true })
|
cy.get(".spectrum-Icon").click({ force: true })
|
||||||
})
|
})
|
||||||
// Select and confirm duplication
|
// Select and confirm duplication
|
||||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
|
|
@ -112,19 +112,9 @@ filterTests(['all'], () => {
|
||||||
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
||||||
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Modal")
|
|
||||||
.within(() => {
|
cy.updateAppName(changedName, noName)
|
||||||
if (noName == true) {
|
|
||||||
cy.get("input").clear()
|
}
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
})
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
|
||||||
return cy
|
|
||||||
}
|
|
||||||
cy.get("input").clear()
|
|
||||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
|
||||||
cy.wait(500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should revert a published app", () => {
|
it("should revert a published app", () => {
|
||||||
|
cy.navigateToFrontend()
|
||||||
|
|
||||||
// Add initial component - Paragraph
|
// Add initial component - Paragraph
|
||||||
cy.addComponent("Elements", "Paragraph")
|
cy.addComponent("Elements", "Paragraph")
|
||||||
// Publish app
|
// Publish app
|
||||||
|
@ -37,6 +39,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-ButtonGroup").within(() => {
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add second component - Button
|
// Add second component - Button
|
||||||
cy.addComponent("Elements", "Button")
|
cy.addComponent("Elements", "Button")
|
||||||
// Click Revert
|
// Click Revert
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify HR Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,222 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Job Application Tracker Template Functionality", () => {
|
||||||
|
const templateName = "Job Application Tracker"
|
||||||
|
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.deleteApp(templateName)
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, {
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
cy.stub(win, 'open')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 & Verify it opened
|
||||||
|
cy.wait(2000) // Wait for app to generate
|
||||||
|
cy.publishApp(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,60 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify IT Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,72 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("IT Ticketing System Template Functionality", () => {
|
||||||
|
const templateName = "IT Ticketing System"
|
||||||
|
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.deleteApp(templateName)
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, {
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
cy.stub(win, 'open')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create and publish app with IT Ticketing System template", () => {
|
||||||
|
// Select IT Ticketing System 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 & Verify it opened
|
||||||
|
cy.wait(2000) // Wait for app to generate
|
||||||
|
cy.publishApp(true)
|
||||||
|
cy.window().its('open').should('be.calledOnce')
|
||||||
|
})
|
||||||
|
|
||||||
|
xit("should filter tickets by status", () => {
|
||||||
|
// Visit published app
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
// Tickets section
|
||||||
|
cy.get(".links").contains("Tickets").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
// Filter by stage - Confirm table updates
|
||||||
|
cy.get(".spectrum-Picker").contains("Filter by status").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 == "In progress" || stageText == "On hold" || stageText == "Triaged") {
|
||||||
|
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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Admin Panel Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,51 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Aproval Apps Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,51 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Business Apps Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,44 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Directories Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Forms Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,43 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Healthcare Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Legal Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Logistics Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Manufacturing Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,44 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Lead Generation Form Template Functionality", () => {
|
||||||
|
const templateName = "Lead Generation Form"
|
||||||
|
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.deleteApp(templateName)
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, {
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
cy.stub(win, 'open')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create and publish app with Lead Generation Form template", () => {
|
||||||
|
// Select Lead Generation Form 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 & Verify it opened
|
||||||
|
cy.wait(2000) // Wait for app to generate
|
||||||
|
cy.publishApp(true)
|
||||||
|
cy.window().its('open').should('be.calledOnce')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,51 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Marketing Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Operations Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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,71 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Portals Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// Filter Portal Templates
|
||||||
|
cy.get(".template-category-filters").within(() => {
|
||||||
|
cy.get('[data-cy="Portal"]').click()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should verify the details option for Portal 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, '-')
|
||||||
|
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,42 @@
|
||||||
|
import filterTests from "../../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Verify Professional Services Template Details", () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
|
||||||
|
// Template navigation
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,7 +7,6 @@ const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
const SERVER_PORT = cypressConfig.env.PORT
|
const SERVER_PORT = cypressConfig.env.PORT
|
||||||
const WORKER_PORT = cypressConfig.env.WORKER_PORT
|
const WORKER_PORT = cypressConfig.env.WORKER_PORT
|
||||||
|
|
||||||
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
||||||
|
|
|
@ -18,24 +18,98 @@ Cypress.Commands.add("login", () => {
|
||||||
cy.get("input").first().type("test@test.com")
|
cy.get("input").first().type("test@test.com")
|
||||||
cy.get('input[type="password"]').first().type("test")
|
cy.get('input[type="password"]').first().type("test")
|
||||||
cy.get('input[type="password"]').eq(1).type("test")
|
cy.get('input[type="password"]').eq(1).type("test")
|
||||||
cy.contains("Create super admin user").click()
|
cy.contains("Create super admin user").click({ force: true })
|
||||||
}
|
}
|
||||||
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
||||||
// login
|
// login
|
||||||
cy.contains("Sign in to Budibase").then(() => {
|
cy.contains("Sign in to Budibase").then(() => {
|
||||||
cy.get("input").first().type("test@test.com")
|
cy.get("input").first().type("test@test.com")
|
||||||
cy.get('input[type="password"]').type("test")
|
cy.get('input[type="password"]').type("test")
|
||||||
cy.get("button").first().click()
|
cy.get("button").first().click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("logOut", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
|
||||||
|
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
|
||||||
|
cy.get("li[data-cy='user-logout']").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("closeModal", () => {
|
Cypress.Commands.add("closeModal", () => {
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".close-icon").click()
|
cy.get(".close-icon").click()
|
||||||
cy.wait(500)
|
cy.wait(1000) // Wait for modal to close
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("importApp", (exportFilePath, name) => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
}
|
||||||
|
cy.get(`[data-cy="import-app-btn"]`).click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get("input").eq(1).should("have.focus")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Dropzone").selectFile(exportFilePath, {
|
||||||
|
action: "drag-drop",
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".gallery .filename").contains("exported-app.txt")
|
||||||
|
|
||||||
|
if (name && name != "") {
|
||||||
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
|
}
|
||||||
|
cy.get(".confirm-wrap button")
|
||||||
|
.should("not.be.disabled")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(5000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||||
|
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
|
||||||
|
cy.get("li[data-cy='user-info']").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal.is-open").within(() => {
|
||||||
|
cy.get("[data-cy='user-first-name']").clear()
|
||||||
|
|
||||||
|
if (!firstName || firstName == "") {
|
||||||
|
cy.get("[data-cy='user-first-name']").invoke("val").should("be.empty")
|
||||||
|
} else {
|
||||||
|
cy.get("[data-cy='user-first-name']")
|
||||||
|
.type(firstName)
|
||||||
|
.should("have.value", firstName)
|
||||||
|
.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get("[data-cy='user-last-name']").clear()
|
||||||
|
|
||||||
|
if (!lastName || lastName == "") {
|
||||||
|
cy.get("[data-cy='user-last-name']").invoke("val").should("be.empty")
|
||||||
|
} else {
|
||||||
|
cy.get("[data-cy='user-last-name']")
|
||||||
|
.type(lastName)
|
||||||
|
.should("have.value", lastName)
|
||||||
|
.blur()
|
||||||
|
}
|
||||||
|
cy.get("button").contains("Update information").click({ force: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -44,7 +118,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
||||||
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(1000)
|
||||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
|
||||||
// If apps already exist
|
// If apps already exist
|
||||||
|
@ -57,8 +131,14 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).should("have.focus")
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
if (name && name != "") {
|
||||||
|
cy.get("input").eq(0).clear()
|
||||||
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
|
}
|
||||||
|
cy.get(".spectrum-ButtonGroup")
|
||||||
|
.contains("Create app")
|
||||||
|
.click({ force: true })
|
||||||
cy.wait(10000)
|
cy.wait(10000)
|
||||||
})
|
})
|
||||||
if (shouldCreateDefaultTable) {
|
if (shouldCreateDefaultTable) {
|
||||||
|
@ -75,42 +155,37 @@ Cypress.Commands.add("deleteApp", name => {
|
||||||
const findAppName = val.some(val => val.name == name)
|
const findAppName = val.some(val => val.name == name)
|
||||||
if (findAppName) {
|
if (findAppName) {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
if (Cypress.env("TEST_ENV")) {
|
const appId = val.reduce((acc, app) => {
|
||||||
cy.searchForApplication(name)
|
if (name === app.name) {
|
||||||
cy.get(".appTable").within(() => {
|
acc = app.appId
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return acc
|
||||||
|
}, "")
|
||||||
|
|
||||||
const appIdParsed = appId.split("_").pop()
|
if (appId == "") {
|
||||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
return
|
||||||
cy.get(actionEleId).within(() => {
|
|
||||||
cy.get(".spectrum-Icon").eq(0).click()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appIdParsed = appId.split("_").pop()
|
||||||
|
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||||
|
cy.get(actionEleId).within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(0).click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Menu").then($menu => {
|
cy.get(".spectrum-Menu").then($menu => {
|
||||||
if ($menu.text().includes("Unpublish")) {
|
if ($menu.text().includes("Unpublish")) {
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").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({ force: true })
|
||||||
|
})
|
||||||
|
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 {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -130,7 +205,7 @@ Cypress.Commands.add("deleteAllApps", () => {
|
||||||
const appIdParsed = val[i].appId.split("_").pop()
|
const appIdParsed = val[i].appId.split("_").pop()
|
||||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||||
cy.get(actionEleId).within(() => {
|
cy.get(actionEleId).within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(0).click()
|
cy.get(".spectrum-Icon").eq(0).click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
@ -143,11 +218,114 @@ Cypress.Commands.add("deleteAllApps", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("customiseAppIcon", () => {
|
||||||
|
// Select random icon
|
||||||
|
cy.get(".grid").within(() => {
|
||||||
|
cy.get(".icon-item")
|
||||||
|
.eq(Math.floor(Math.random() * 23) + 1)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
// Select random colour
|
||||||
|
cy.get(".fill").click()
|
||||||
|
cy.get(".colors").within(() => {
|
||||||
|
cy.get(".color")
|
||||||
|
.eq(Math.floor(Math.random() * 33) + 1)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
cy.intercept("**/applications/**").as("iconChange")
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.wait("@iconChange")
|
||||||
|
cy.get("@iconChange").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("alterAppVersion", (appId, version) => {
|
||||||
|
return cy
|
||||||
|
.request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, {
|
||||||
|
version: version || "0.0.1-alpha.0",
|
||||||
|
})
|
||||||
|
.then(resp => {
|
||||||
|
expect(resp.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("updateAppName", (changedName, noName) => {
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
if (noName == true) {
|
||||||
|
cy.get("input").clear()
|
||||||
|
cy.get(".spectrum-Dialog-grid")
|
||||||
|
.click()
|
||||||
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
|
return cy
|
||||||
|
}
|
||||||
|
cy.get("input").clear()
|
||||||
|
cy.get("input")
|
||||||
|
.eq(0)
|
||||||
|
.type(changedName)
|
||||||
|
.should("have.value", changedName)
|
||||||
|
.blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("unlockApp", unlock_config => {
|
||||||
|
let config = { ...unlock_config }
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal .spectrum-Dialog[data-cy='app-lock-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
if (config.owned) {
|
||||||
|
cy.get(".spectrum-Dialog-heading").contains("Locked by you")
|
||||||
|
cy.get(".lock-expiry-body").contains(
|
||||||
|
"This lock will expire in 10 minutes from now"
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept("**/lock").as("unlockApp")
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Release Lock")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait("@unlockApp")
|
||||||
|
cy.get("@unlockApp").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@unlockApp").its("response.body").should("deep.equal", {
|
||||||
|
message: "Lock released successfully.",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
//Show the name ?
|
||||||
|
cy.get(".lock-expiry-body").should("not.be.visible")
|
||||||
|
cy.get(".spectrum-Button").contains("Done")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("publishApp", resolvedAppPath => {
|
||||||
|
//Assumes you have navigated to an application first
|
||||||
|
cy.get(".toprightnav button.spectrum-Button")
|
||||||
|
.contains("Publish")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
//Verify that the app url is presented correctly to the user
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
let appUrl = Cypress.config().baseUrl + "/app/" + resolvedAppPath
|
||||||
|
cy.get("[data-cy='deployed-app-url'] input").should("have.value", appUrl)
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestApp", () => {
|
Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||||
cy.createScreen("home")
|
//cy.createScreen("home")
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestTableWithData", () => {
|
Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
|
@ -156,6 +334,21 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
cy.addColumn("dog", "age", "Number")
|
cy.addColumn("dog", "age", "Number")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("publishApp", (viewApp = false) => {
|
||||||
|
cy.get(".toprightnav").contains("Publish").click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.wait(2000) // Wait for App to publish and modal to appear
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
if (viewApp) {
|
||||||
|
cy.get(".spectrum-Button").contains("View App").click({ force: true })
|
||||||
|
} else {
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
if (!initialTable) {
|
if (!initialTable) {
|
||||||
cy.navigateToDataSection()
|
cy.navigateToDataSection()
|
||||||
|
@ -248,12 +441,12 @@ Cypress.Commands.add("createUser", email => {
|
||||||
|
|
||||||
Cypress.Commands.add("addComponent", (category, component) => {
|
Cypress.Commands.add("addComponent", (category, component) => {
|
||||||
if (category) {
|
if (category) {
|
||||||
cy.get(`[data-cy="category-${category}"]`).click()
|
cy.get(`[data-cy="category-${category}"]`).click({ force: true })
|
||||||
}
|
}
|
||||||
if (component) {
|
if (component) {
|
||||||
cy.get(`[data-cy="component-${component}"]`).click()
|
cy.get(`[data-cy="component-${component}"]`).click({ force: true })
|
||||||
}
|
}
|
||||||
cy.wait(1000)
|
cy.wait(2000)
|
||||||
cy.location().then(loc => {
|
cy.location().then(loc => {
|
||||||
const params = loc.pathname.split("/")
|
const params = loc.pathname.split("/")
|
||||||
const componentId = params[params.length - 1]
|
const componentId = params[params.length - 1]
|
||||||
|
@ -458,7 +651,12 @@ Cypress.Commands.add("createAppFromScratch", appName => {
|
||||||
.contains("Start from scratch")
|
.contains("Start from scratch")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
cy.get("input")
|
||||||
|
.eq(0)
|
||||||
|
.clear()
|
||||||
|
.type(appName)
|
||||||
|
.should("have.value", appName)
|
||||||
|
.blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
cy.wait(10000)
|
cy.wait(10000)
|
||||||
})
|
})
|
||||||
|
@ -572,12 +770,14 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
cy.intercept("**/tables").as("datasourceTables")
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.contains("Save and fetch tables")
|
.contains("Save and fetch tables")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.wait(1000)
|
|
||||||
})
|
})
|
||||||
|
// Wait for tables to be fetched
|
||||||
|
cy.wait("@datasourceTables", { timeout: 60000 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// createApp test
|
||||||
|
export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]'
|
||||||
|
export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters"
|
||||||
|
export const TEMPLATE_CATEGORY = ".template-categories"
|
||||||
|
export const APP_TABLE = ".appTable"
|
||||||
|
export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button"
|
||||||
|
export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category"
|
||||||
|
export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON =
|
||||||
|
".template-category-filters .spectrum-ActionButton"
|
||||||
|
export const SPECTRUM_MODAL = ".spectrum-Modal"
|
||||||
|
export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data;
|
||||||
|
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup"
|
||||||
|
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input"
|
||||||
|
|
||||||
|
//AddMultiOptionDatatype test
|
||||||
|
export const CATEGORY_DATA = '[data-cy="category-Data"]'
|
||||||
|
export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]'
|
||||||
|
export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]'
|
||||||
|
export const DROPDOWN = ".dropdown"
|
||||||
|
export const SPECTRUM_PICKER_LABEL = ".spectrum-Picker-label"
|
||||||
|
export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]'
|
||||||
|
export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control'
|
||||||
|
|
||||||
|
//AddRadioButtons
|
||||||
|
export const SPECTRUM_POPOVER = ".spectrum-Popover"
|
||||||
|
export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control'
|
||||||
|
export const APP_TABLE_STATUS = ".appTable .app-status"
|
||||||
|
export const APP_TABLE_ROW_ACTION = ".appTable .app-row-actions"
|
||||||
|
export const DEPLOYMENT_TOP_NAV_GLOBESTRIKE =
|
||||||
|
".deployment-top-nav svg[aria-label=GlobeStrike]"
|
||||||
|
export const DEPLOYMENT_TOP_GLOBE = ".deployment-top-nav svg[aria-label=Globe]"
|
||||||
|
export const PUBLISH_POPOVER_MENU = '[data-cy="publish-popover-menu"]'
|
||||||
|
export const PUBLISH_POPOVER_ACTION = '[data-cy="publish-popover-action"]'
|
||||||
|
export const PUBLISH_POPOVER_MESSAGE = ".publish-popover-message"
|
||||||
|
export const SPECTRUM_BUTTON = ".spectrum-Button"
|
||||||
|
export const TOPRIGHTNAV_BUTTON_SPECTRUM = ".toprightnav button.spectrum-Button"
|
||||||
|
|
||||||
|
//createComponents
|
||||||
|
export const SETTINGS = "[data-cy=Settings]"
|
||||||
|
export const SETTINGS_INPUT = "[data-cy=setting-text] input"
|
||||||
|
export const DESIGN = "[data-cy=Design]"
|
||||||
|
export const FONT_SIZE_PROP_CONTRO = "[data-cy=font-size-prop-control]"
|
||||||
|
export const DATA_CY_DATASOURCE = "[data-cy=setting-dataSource]"
|
||||||
|
export const DROPDOWN_CONTAINER = ".dropdown-container"
|
||||||
|
export const SPECTRUM_PICKER = ".spectrum-Picker"
|
||||||
|
|
||||||
|
//autoScreens
|
||||||
|
export const LABEL_ADD_CIRCLE = "[aria-label=AddCircle]"
|
||||||
|
export const ITEM_DISABLED = ".item.disabled"
|
||||||
|
export const CONFIRM_WRAP_SPE_BUTTON = ".confirm-wrap .spectrum-Button"
|
||||||
|
export const DATA_SOURCE_ENTRY = ".data-source-entry"
|
||||||
|
export const NAV_ITEMS_CONTAINER = ".nav-items-container"
|
||||||
|
|
||||||
|
//publishWorkFlow
|
||||||
|
export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]"
|
||||||
|
export const DEPLOY_APP_URL_INPUT = "[data-cy=deployed-app-url] input"
|
||||||
|
export const GLOBESTRIKE = "svg[aria-label=GlobeStrike]"
|
||||||
|
export const GLOBE = "svg[aria-label=Globe]"
|
||||||
|
export const UNPUBLISH_MODAL = "[data-cy=unpublish-modal]"
|
||||||
|
export const CONFIRM_WRAP_BUTTON = ".confirm-wrap button"
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.191",
|
"version": "1.0.192-alpha.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -13,11 +13,13 @@
|
||||||
"cy:setup:ci": "node ./cypress/setup.js",
|
"cy:setup:ci": "node ./cypress/setup.js",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome",
|
"cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js",
|
||||||
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
|
"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: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": "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:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report",
|
||||||
|
"cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline",
|
||||||
|
"cy:ci:notify": "node scripts/cypressResultsWebhook",
|
||||||
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
|
"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"
|
"cy:debug:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:open"
|
||||||
},
|
},
|
||||||
|
@ -67,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.191",
|
"@budibase/bbui": "^1.0.192-alpha.1",
|
||||||
"@budibase/client": "^1.0.191",
|
"@budibase/client": "^1.0.192-alpha.1",
|
||||||
"@budibase/frontend-core": "^1.0.191",
|
"@budibase/frontend-core": "^1.0.192-alpha.1",
|
||||||
"@budibase/string-templates": "^1.0.191",
|
"@budibase/string-templates": "^1.0.192-alpha.1",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
@ -98,9 +100,13 @@
|
||||||
"@testing-library/svelte": "^3.0.0",
|
"@testing-library/svelte": "^3.0.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"cypress": "^9.3.1",
|
"cypress": "^9.3.1",
|
||||||
|
"cypress-multi-reporters": "^1.6.0",
|
||||||
"cypress-terminal-report": "^1.4.1",
|
"cypress-terminal-report": "^1.4.1",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
|
"mochawesome": "^7.1.3",
|
||||||
|
"mochawesome-merge": "^4.2.1",
|
||||||
|
"mochawesome-report-generator": "^6.2.0",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"reporterEnabled": "mochawesome",
|
||||||
|
"mochawesomeReporterOptions": {
|
||||||
|
"reportDir": "cypress/reports",
|
||||||
|
"quiet": true,
|
||||||
|
"overwrite": false,
|
||||||
|
"html": false,
|
||||||
|
"json": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const path = require("path")
|
||||||
|
const fs = require("fs")
|
||||||
|
|
||||||
|
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL
|
||||||
|
const OUTCOME = process.env.CYPRESS_OUTCOME
|
||||||
|
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL
|
||||||
|
const GIT_SHA = process.env.GITHUB_SHA
|
||||||
|
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
|
||||||
|
|
||||||
|
async function generateReport() {
|
||||||
|
// read the report file
|
||||||
|
const REPORT_PATH = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"cypress",
|
||||||
|
"reports",
|
||||||
|
"testReport.json"
|
||||||
|
)
|
||||||
|
const report = fs.readFileSync(REPORT_PATH, "utf-8")
|
||||||
|
return JSON.parse(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discordCypressResultsNotification(report) {
|
||||||
|
const {
|
||||||
|
suites,
|
||||||
|
tests,
|
||||||
|
passes,
|
||||||
|
pending,
|
||||||
|
failures,
|
||||||
|
duration,
|
||||||
|
passPercent,
|
||||||
|
skipped,
|
||||||
|
} = report.stats
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: `**Nightly Tests Status**: ${OUTCOME}`,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: "Budi QA Bot",
|
||||||
|
description: `Nightly Tests`,
|
||||||
|
url: GITHUB_ACTIONS_RUN_URL,
|
||||||
|
color: OUTCOME === "success" ? 3066993 : 15548997,
|
||||||
|
timestamp: new Date(),
|
||||||
|
footer: {
|
||||||
|
icon_url: "http://bbui.budibase.com/budibase-logo.png",
|
||||||
|
text: "Budibase QA Bot",
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
url: "http://bbui.budibase.com/budibase-logo.png",
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
name: "Budibase QA Bot",
|
||||||
|
url: "https://discordapp.com",
|
||||||
|
icon_url: "http://bbui.budibase.com/budibase-logo.png",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Commit",
|
||||||
|
value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cypress Dashboard URL",
|
||||||
|
value: DASHBOARD_URL || "None Supplied",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Github Actions Run URL",
|
||||||
|
value: GITHUB_ACTIONS_RUN_URL || "None Supplied",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Suites",
|
||||||
|
value: suites,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tests",
|
||||||
|
value: tests,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Passed",
|
||||||
|
value: passes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pending",
|
||||||
|
value: pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Skipped",
|
||||||
|
value: skipped,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failures",
|
||||||
|
value: failures,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Duration",
|
||||||
|
value: `${duration / 1000} Seconds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pass Percentage",
|
||||||
|
value: Math.floor(passPercent),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
const response = await fetch(WEBHOOK_URL, options)
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
const text = await response.text()
|
||||||
|
console.error(
|
||||||
|
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const report = await generateReport()
|
||||||
|
await discordCypressResultsNotification(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
|
@ -105,9 +105,7 @@ const automationActions = store => ({
|
||||||
},
|
},
|
||||||
select: automation => {
|
select: automation => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
let testResults = state.selectedAutomation?.testResults
|
|
||||||
state.selectedAutomation = new Automation(cloneDeep(automation))
|
state.selectedAutomation = new Automation(cloneDeep(automation))
|
||||||
state.selectedAutomation.testResults = testResults
|
|
||||||
state.selectedBlock = null
|
state.selectedBlock = null
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
|
|
||||||
let testDataModal
|
let testDataModal
|
||||||
let blocks
|
let blocks
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
@ -41,66 +40,70 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="canvas">
|
<div class="canvas">
|
||||||
<div class="content">
|
<div style="float: left; padding-left: var(--spacing-xl);">
|
||||||
<div class="title">
|
<Heading size="S">{automation.name}</Heading>
|
||||||
<div class="subtitle">
|
</div>
|
||||||
<Heading size="S">{automation.name}</Heading>
|
<div style="float: right; padding-right: var(--spacing-xl);" class="title">
|
||||||
<div style="display:flex; align-items: center;">
|
<div class="subtitle">
|
||||||
<div class="iconPadding">
|
<div style="display:flex; align-items: center;">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<Icon
|
<Icon
|
||||||
on:click={confirmDeleteDialog.show}
|
on:click={confirmDeleteDialog.show}
|
||||||
hoverable
|
hoverable
|
||||||
size="M"
|
size="M"
|
||||||
name="DeleteOutline"
|
name="DeleteOutline"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ActionButton
|
||||||
|
on:click={() => {
|
||||||
|
testDataModal.show()
|
||||||
|
}}
|
||||||
|
icon="MultipleCheck"
|
||||||
|
size="M">Run test</ActionButton
|
||||||
|
>
|
||||||
|
<div style="padding-left: var(--spacing-m);">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
disabled={!$automationStore.selectedAutomation?.testResults}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
testDataModal.show()
|
$automationStore.selectedAutomation.automation.showTestPanel = true
|
||||||
}}
|
}}
|
||||||
icon="MultipleCheck"
|
size="M">Test Details</ActionButton
|
||||||
size="M">Run test</ActionButton
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#each blocks as block, idx (block.id)}
|
|
||||||
<div
|
|
||||||
class="block"
|
|
||||||
animate:flip={{ duration: 500 }}
|
|
||||||
in:fly|local={{ x: 500, duration: 1500 }}
|
|
||||||
>
|
|
||||||
{#if block.stepId !== "LOOP"}
|
|
||||||
<FlowItem {testDataModal} {block} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
okText="Delete Automation"
|
|
||||||
onOk={deleteAutomation}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
>
|
|
||||||
Are you sure you wish to delete the automation
|
|
||||||
<i>{automation.name}?</i>
|
|
||||||
This action cannot be undone.
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<Modal bind:this={testDataModal} width="30%">
|
|
||||||
<TestDataModal />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{#each blocks as block, idx (block.id)}
|
||||||
|
<div
|
||||||
|
class="block"
|
||||||
|
animate:flip={{ duration: 500 }}
|
||||||
|
in:fly|local={{ x: 500, duration: 500 }}
|
||||||
|
out:fly|local={{ x: 500, duration: 500 }}
|
||||||
|
>
|
||||||
|
{#if block.stepId !== "LOOP"}
|
||||||
|
<FlowItem {testDataModal} {block} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
okText="Delete Automation"
|
||||||
|
onOk={deleteAutomation}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you wish to delete the automation
|
||||||
|
<i>{automation.name}?</i>
|
||||||
|
This action cannot be undone.
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal bind:this={testDataModal} width="30%">
|
||||||
|
<TestDataModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.canvas {
|
|
||||||
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
|
|
||||||
overflow-y: auto;
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
/* Fix for firefox not respecting bottom padding in scrolling containers */
|
||||||
.canvas > *:last-child {
|
.canvas > *:last-child {
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
|
@ -128,10 +131,6 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.iconPadding {
|
|
||||||
padding-top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-right: var(--spacing-m);
|
padding-right: var(--spacing-m);
|
||||||
|
|
|
@ -1,39 +1,33 @@
|
||||||
<script>
|
<script>
|
||||||
|
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||||
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Divider,
|
Divider,
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
|
||||||
Detail,
|
Detail,
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
|
||||||
Select,
|
Select,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import ResultsModal from "./ResultsModal.svelte"
|
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
import { externalActions } from "./ExternalActions"
|
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testDataModal
|
export let testDataModal
|
||||||
let selected
|
let selected
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let actionModal
|
let actionModal
|
||||||
let resultsModal
|
|
||||||
let blockComplete
|
let blockComplete
|
||||||
let showLooping = false
|
let showLooping = false
|
||||||
|
|
||||||
$: showBindingPicker =
|
$: showBindingPicker =
|
||||||
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
|
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
|
||||||
|
|
||||||
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
|
|
||||||
step => (block.id ? step.id === block.id : step.stepId === block.stepId)
|
|
||||||
)
|
|
||||||
$: isTrigger = block.type === "TRIGGER"
|
$: isTrigger = block.type === "TRIGGER"
|
||||||
|
|
||||||
$: selected = $automationStore.selectedBlock?.id === block.id
|
$: selected = $automationStore.selectedBlock?.id === block.id
|
||||||
|
@ -181,63 +175,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="blockSection">
|
<FlowItemHeader bind:blockComplete {block} {testDataModal} />
|
||||||
<div
|
|
||||||
on:click={() => {
|
|
||||||
blockComplete = !blockComplete
|
|
||||||
}}
|
|
||||||
class="splitHeader"
|
|
||||||
>
|
|
||||||
<div class="center-items">
|
|
||||||
{#if externalActions[block.stepId]}
|
|
||||||
<img
|
|
||||||
alt={externalActions[block.stepId].name}
|
|
||||||
width="28px"
|
|
||||||
height="28px"
|
|
||||||
src={externalActions[block.stepId].icon}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
width="28px"
|
|
||||||
height="28px"
|
|
||||||
class="spectrum-Icon"
|
|
||||||
style="color:grey;"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
<div class="iconAlign">
|
|
||||||
{#if isTrigger}
|
|
||||||
<Body size="XS">When this happens:</Body>
|
|
||||||
{:else}
|
|
||||||
<Body size="XS">Do this:</Body>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="blockTitle">
|
|
||||||
{#if testResult && testResult[0]}
|
|
||||||
<div style="float: right;" on:click={() => resultsModal.show()}>
|
|
||||||
<StatusLight
|
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
|
||||||
negative={!testResult[0].outputs?.success}
|
|
||||||
><Body size="XS">View response</Body></StatusLight
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div
|
|
||||||
style="margin-left: 10px;"
|
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !blockComplete}
|
{#if !blockComplete}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
|
@ -282,10 +220,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Modal bind:this={resultsModal} width="30%">
|
|
||||||
<ResultsModal {isTrigger} {testResult} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={actionModal} width="30%">
|
<Modal bind:this={actionModal} width="30%">
|
||||||
<ActionModal {blockIdx} bind:blockComplete />
|
<ActionModal {blockIdx} bind:blockComplete />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
<script>
|
||||||
|
import { automationStore } from "builderStore"
|
||||||
|
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
|
||||||
|
import { externalActions } from "./ExternalActions"
|
||||||
|
|
||||||
|
export let block
|
||||||
|
export let blockComplete
|
||||||
|
export let showTestStatus = false
|
||||||
|
export let showParameters = {}
|
||||||
|
|
||||||
|
$: testResult =
|
||||||
|
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
|
||||||
|
block.id ? step.id === block.id : step.stepId === block.stepId
|
||||||
|
)
|
||||||
|
$: isTrigger = block.type === "TRIGGER"
|
||||||
|
|
||||||
|
async function onSelect(block) {
|
||||||
|
await automationStore.update(state => {
|
||||||
|
state.selectedBlock = block
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="blockSection">
|
||||||
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
blockComplete = !blockComplete
|
||||||
|
showParameters[block.id] = blockComplete
|
||||||
|
}}
|
||||||
|
class="splitHeader"
|
||||||
|
>
|
||||||
|
<div class="center-items">
|
||||||
|
{#if externalActions[block.stepId]}
|
||||||
|
<img
|
||||||
|
alt={externalActions[block.stepId].name}
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
src={externalActions[block.stepId].icon}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:grey;"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<div class="iconAlign">
|
||||||
|
{#if isTrigger}
|
||||||
|
<Body size="XS">When this happens:</Body>
|
||||||
|
{:else}
|
||||||
|
<Body size="XS">Do this:</Body>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="blockTitle">
|
||||||
|
{#if showTestStatus && testResult && testResult[0]}
|
||||||
|
<div style="float: right;">
|
||||||
|
<StatusLight
|
||||||
|
positive={isTrigger || testResult[0].outputs?.success}
|
||||||
|
negative={!testResult[0].outputs?.success}
|
||||||
|
><Body size="XS"
|
||||||
|
>{testResult[0].outputs?.success || isTrigger
|
||||||
|
? "Success"
|
||||||
|
: "Error"}</Body
|
||||||
|
></StatusLight
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px; margin-bottom: var(--spacing-xs);"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.center-items {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.splitHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.iconAlign {
|
||||||
|
padding: 0 0 0 var(--spacing-m);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockSection {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,133 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let testResult
|
|
||||||
export let isTrigger
|
|
||||||
let inputToggled
|
|
||||||
let outputToggled
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
showCloseIcon={false}
|
|
||||||
showConfirmButton={false}
|
|
||||||
cancelText="Close"
|
|
||||||
>
|
|
||||||
<div slot="header" class="result-modal-header">
|
|
||||||
<span>Test Results</span>
|
|
||||||
<div>
|
|
||||||
{#if isTrigger || testResult[0].outputs.success}
|
|
||||||
<div class="iconSuccess">
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="iconFailure">
|
|
||||||
<Icon size="S" name="CloseCircle" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{#if testResult[0].outputs.iterations}
|
|
||||||
<div style="display: flex;">
|
|
||||||
<Icon name="Reuse" />
|
|
||||||
<div style="margin-left: 10px;">
|
|
||||||
<Label>
|
|
||||||
This loop ran {testResult[0].outputs.iterations} times.</Label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
on:click={() => {
|
|
||||||
inputToggled = !inputToggled
|
|
||||||
}}
|
|
||||||
class="toggle splitHeader"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div style="display: flex; align-items: center;">
|
|
||||||
<span style="padding-left: var(--spacing-s);">
|
|
||||||
<Detail size="S">Input</Detail>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{#if inputToggled}
|
|
||||||
<Icon size="M" name="ChevronDown" />
|
|
||||||
{:else}
|
|
||||||
<Icon size="M" name="ChevronRight" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if inputToggled}
|
|
||||||
<div class="text-area-container">
|
|
||||||
<TextArea
|
|
||||||
disabled
|
|
||||||
value={JSON.stringify(testResult[0].inputs, null, 2)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div
|
|
||||||
on:click={() => {
|
|
||||||
outputToggled = !outputToggled
|
|
||||||
}}
|
|
||||||
class="toggle splitHeader"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div style="display: flex; align-items: center;">
|
|
||||||
<span style="padding-left: var(--spacing-s);">
|
|
||||||
<Detail size="S">Output</Detail>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{#if outputToggled}
|
|
||||||
<Icon size="M" name="ChevronDown" />
|
|
||||||
{:else}
|
|
||||||
<Icon size="M" name="ChevronRight" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if outputToggled}
|
|
||||||
<div class="text-area-container">
|
|
||||||
<TextArea
|
|
||||||
disabled
|
|
||||||
value={JSON.stringify(testResult[0].outputs, null, 2)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.result-modal-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconSuccess {
|
|
||||||
color: var(--spectrum-global-color-green-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconFailure {
|
|
||||||
color: var(--spectrum-global-color-red-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.splitHeader {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-area-container :global(textarea) {
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -51,6 +51,7 @@
|
||||||
$automationStore.selectedAutomation?.automation,
|
$automationStore.selectedAutomation?.automation,
|
||||||
testData
|
testData
|
||||||
)
|
)
|
||||||
|
$automationStore.selectedAutomation.automation.showTestPanel = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error testing notification")
|
notifications.error("Error testing notification")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
||||||
|
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
||||||
|
import { automationStore } from "builderStore"
|
||||||
|
|
||||||
|
export let automation
|
||||||
|
|
||||||
|
let showParameters
|
||||||
|
let blocks
|
||||||
|
|
||||||
|
$: {
|
||||||
|
blocks = []
|
||||||
|
if (automation) {
|
||||||
|
if (automation.definition.trigger) {
|
||||||
|
blocks.push(automation.definition.trigger)
|
||||||
|
}
|
||||||
|
blocks = blocks
|
||||||
|
.concat(automation.definition.steps || [])
|
||||||
|
.filter(x => x.stepId !== "LOOP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: testResults =
|
||||||
|
$automationStore.selectedAutomation?.testResults?.steps.filter(
|
||||||
|
x => x.stepId !== "LOOP" || []
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<div class="title-text">
|
||||||
|
<Icon name="MultipleCheck" />
|
||||||
|
<div style="padding-left: var(--spacing-l)">Test Details</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding-right: var(--spacing-xl)">
|
||||||
|
<Icon
|
||||||
|
on:click={async () => {
|
||||||
|
$automationStore.selectedAutomation.automation.showTestPanel = false
|
||||||
|
}}
|
||||||
|
hoverable
|
||||||
|
name="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{#each blocks as block, idx}
|
||||||
|
<div class="block">
|
||||||
|
{#if block.stepId !== "LOOP"}
|
||||||
|
<FlowItemHeader showTestStatus={true} bind:showParameters {block} />
|
||||||
|
{#if showParameters && showParameters[block.id]}
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if testResults?.[idx]?.outputs.iterations}
|
||||||
|
<div style="display: flex; padding: 10px 10px 0px 12px;">
|
||||||
|
<Icon name="Reuse" />
|
||||||
|
<div style="margin-left: 10px;">
|
||||||
|
<Label>
|
||||||
|
This loop ran {testResults?.[idx]?.outputs.iterations} times.</Label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<Tabs quiet noPadding selected="Input">
|
||||||
|
<Tab title="Input">
|
||||||
|
<div style="padding: 10px 10px 10px 10px;">
|
||||||
|
<TextArea
|
||||||
|
minHeight="80px"
|
||||||
|
disabled
|
||||||
|
value={JSON.stringify(testResults?.[idx]?.inputs, null, 2)}
|
||||||
|
/>
|
||||||
|
</div></Tab
|
||||||
|
>
|
||||||
|
<Tab title="Output">
|
||||||
|
<div style="padding: 10px 10px 10px 10px;">
|
||||||
|
<TextArea
|
||||||
|
minHeight="100px"
|
||||||
|
disabled
|
||||||
|
value={JSON.stringify(testResults?.[idx]?.outputs, null, 2)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if blocks.length - 1 !== idx}
|
||||||
|
<div class="separator" />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
padding: 0px 30px 0px 30px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
padding-left: var(--spacing-xl);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title :global(h1) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: inline-block;
|
||||||
|
width: 400px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: var(--background);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 40px;
|
||||||
|
border-left: 1px dashed var(--grey-4);
|
||||||
|
color: var(--grey-4);
|
||||||
|
/* center horizontally */
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -53,6 +53,7 @@
|
||||||
: { schema: {} }
|
: { schema: {} }
|
||||||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
|
|
||||||
const onChange = Utils.sequential(async (e, key) => {
|
const onChange = Utils.sequential(async (e, key) => {
|
||||||
try {
|
try {
|
||||||
|
@ -330,6 +331,7 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
|
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -304,7 +304,9 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const newError = {}
|
const newError = {}
|
||||||
if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
if (!external && fieldInfo.name?.startsWith("_")) {
|
||||||
|
newError.name = `Column name cannot start with an underscore.`
|
||||||
|
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
||||||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
||||||
", "
|
", "
|
||||||
)} are not allowed as column names`
|
)} are not allowed as column names`
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
name,
|
name,
|
||||||
schema: addAutoColumns(name, dataImport.schema || {}),
|
schema: addAutoColumns(name, dataImport.schema || {}),
|
||||||
dataImport,
|
dataImport,
|
||||||
|
type: "internal",
|
||||||
|
sourceId: "bb_internal",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set primary display if defined
|
// Only set primary display if defined
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
ModalContent,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
ProgressCircle,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, apps } from "stores/portal"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let buttonSize = "M"
|
||||||
|
|
||||||
|
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
|
||||||
|
let appLockModal
|
||||||
|
let processing = false
|
||||||
|
|
||||||
|
$: lockedBy = app?.lockedBy
|
||||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||||
|
|
||||||
|
$: lockIdentifer = `${
|
||||||
|
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
|
||||||
|
}`
|
||||||
|
|
||||||
|
$: lockedByHeading =
|
||||||
|
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
|
||||||
|
|
||||||
|
const getExpiryDuration = app => {
|
||||||
|
if (!app?.lockedBy?.lockedAt) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
let expiry =
|
||||||
|
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
|
||||||
|
return expiry - new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = async () => {
|
||||||
|
processing = true
|
||||||
|
if (app) {
|
||||||
|
try {
|
||||||
|
await API.releaseAppLock(app.devId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("Lock released successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error releasing lock")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifications.error("No application is selected")
|
||||||
|
}
|
||||||
|
processing = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lock-status">
|
||||||
|
{#if lockedBy}
|
||||||
|
<Button
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon="LockClosed"
|
||||||
|
size={buttonSize}
|
||||||
|
on:click={() => {
|
||||||
|
appLockModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="lock-status-text">
|
||||||
|
{lockedByHeading}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={appLockModal}>
|
||||||
|
<ModalContent
|
||||||
|
title={lockedByHeading}
|
||||||
|
dataCy={"app-lock-modal"}
|
||||||
|
showConfirmButton={false}
|
||||||
|
showCancelButton={false}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Apps are locked to prevent work from being lost from overlapping changes
|
||||||
|
between your team.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||||
|
<span class="lock-expiry-body">
|
||||||
|
{processStringSync(
|
||||||
|
"This lock will expire in {{ duration time 'millisecond' }} from now",
|
||||||
|
{
|
||||||
|
time: getExpiryDuration(app),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<div class="lock-modal-actions">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
quiet={lockedBy && lockedByYou}
|
||||||
|
disabled={processing}
|
||||||
|
on:click={() => {
|
||||||
|
appLockModal.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="cancel"
|
||||||
|
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
{#if lockedByYou}
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
disabled={processing}
|
||||||
|
on:click={() => {
|
||||||
|
releaseLock()
|
||||||
|
appLockModal.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if processing}
|
||||||
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
|
{:else}
|
||||||
|
<span class="unlock">Release Lock</span>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lock-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.lock-status {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Detail } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title = ""
|
||||||
|
export let actionIcon
|
||||||
|
export let action
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
|
$: actionDefined = typeof action === "function"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dash-card" data-cy={dataCy}>
|
||||||
|
<div class="dash-card-header" class:active={actionDefined} on:click={action}>
|
||||||
|
<span class="dash-card-title">
|
||||||
|
<Detail size="M">{title}</Detail>
|
||||||
|
</span>
|
||||||
|
<span class="dash-card-action">
|
||||||
|
{#if actionDefined}
|
||||||
|
<Icon name={actionIcon || "ChevronRight"} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dash-card {
|
||||||
|
background: var(--spectrum-alias-background-color-primary);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
.dash-card-header {
|
||||||
|
padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400);
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.dash-card-body {
|
||||||
|
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
|
||||||
|
}
|
||||||
|
.dash-card-title :global(.spectrum-Detail) {
|
||||||
|
color: var(
|
||||||
|
--spectrum-sidenav-heading-text-color,
|
||||||
|
var(--spectrum-global-color-gray-700)
|
||||||
|
);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dash-card-header.active:hover {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let size
|
||||||
|
export let app
|
||||||
|
|
||||||
|
let iconModal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editable-icon">
|
||||||
|
<div
|
||||||
|
class="edit-hover"
|
||||||
|
on:click={() => {
|
||||||
|
iconModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={"Edit"} size={"L"} />
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<Icon {name} {size} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChooseIconModal {app} bind:this={iconModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editable-icon:hover .app-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.editable-icon {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.editable-icon:hover .edit-hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.edit-hover {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
/* transition: opacity var(--spectrum-global-animation-duration-100) ease; */
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,6 +11,16 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
updateModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
updateModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
export let hideIcon = false
|
||||||
|
|
||||||
let updateModal
|
let updateModal
|
||||||
|
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
@ -57,9 +67,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
{#if !hideIcon}
|
||||||
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
||||||
</div>
|
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="App version"
|
title="App version"
|
||||||
|
|
|
@ -44,6 +44,20 @@
|
||||||
$: readQuery = query.queryVerb === "read" || query.readable
|
$: readQuery = query.queryVerb === "read" || query.readable
|
||||||
$: queryInvalid = !query.name || (readQuery && data.length === 0)
|
$: 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
|
// seed the transformer
|
||||||
if (query && !query.transformer) {
|
if (query && !query.transformer) {
|
||||||
query.transformer = "return data"
|
query.transformer = "return data"
|
||||||
|
|
|
@ -27,6 +27,14 @@
|
||||||
Personalise the platform by adding your first name and last name.
|
Personalise the platform by adding your first name and last name.
|
||||||
</Body>
|
</Body>
|
||||||
<Input disabled bind:value={$auth.user.email} label="Email" />
|
<Input disabled bind:value={$auth.user.email} label="Email" />
|
||||||
<Input bind:value={$values.firstName} label="First name" />
|
<Input
|
||||||
<Input bind:value={$values.lastName} label="Last name" />
|
bind:value={$values.firstName}
|
||||||
|
label="First name"
|
||||||
|
dataCy="user-first-name"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.lastName}
|
||||||
|
label="Last name"
|
||||||
|
dataCy="user-last-name"
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,33 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
Heading,
|
import AppLockModal from "../common/AppLockModal.svelte"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
StatusLight,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
export let viewApp
|
|
||||||
export let editApp
|
export let editApp
|
||||||
export let updateApp
|
export let updateApp
|
||||||
export let deleteApp
|
export let deleteApp
|
||||||
export let previewApp
|
|
||||||
export let unpublishApp
|
export let unpublishApp
|
||||||
|
export let appOverview
|
||||||
export let releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
export let copyAppId
|
export let copyAppId
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title" data-cy={`${app.devId}`}>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||||
</div>
|
</div>
|
||||||
<div class="name" on:click={() => editApp(app)}>
|
<div class="name" on:click={() => appOverview(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -44,19 +37,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight
|
<AppLockModal {app} buttonSize="S" />
|
||||||
positive={!app.lockedYou && !app.lockedOther}
|
|
||||||
notice={app.lockedYou}
|
|
||||||
negative={app.lockedOther}
|
|
||||||
>
|
|
||||||
{#if app.lockedYou}
|
|
||||||
Locked by you
|
|
||||||
{:else if app.lockedOther}
|
|
||||||
Locked by {app.lockedBy.email}
|
|
||||||
{:else}
|
|
||||||
Open
|
|
||||||
{/if}
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
@ -71,23 +52,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
{#if app.deployed}
|
|
||||||
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
|
||||||
>View app
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
|
||||||
>Preview
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
cta
|
secondary
|
||||||
|
quiet
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
on:click={() => editApp(app)}
|
on:click={() => editApp(app)}
|
||||||
>
|
>Edit
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
|
||||||
</div>
|
</div>
|
||||||
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
<span slot="control" class="app-row-actions-icon">
|
<span slot="control" class="app-row-actions-icon">
|
||||||
|
@ -123,6 +96,7 @@
|
||||||
}
|
}
|
||||||
.app-status {
|
.app-status {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
grid-template-columns: 24px 100px;
|
grid-template-columns: 24px 100px;
|
||||||
}
|
}
|
||||||
.app-status span.disabled {
|
.app-status span.disabled {
|
||||||
|
|
|
@ -10,17 +10,32 @@
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
|
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
let creating = false
|
let creating = false
|
||||||
|
let defaultAppName
|
||||||
|
|
||||||
const values = writable({ name: "", url: null })
|
const values = writable({ name: "", url: null })
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
$: validation.check($values)
|
$: validation.check($values)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
$values.name = resolveAppName(template, $values.name)
|
const lastChar = $auth.user?.firstName
|
||||||
|
? $auth.user?.firstName[$auth.user?.firstName.length - 1]
|
||||||
|
: null
|
||||||
|
|
||||||
|
defaultAppName =
|
||||||
|
lastChar && lastChar.toLowerCase() == "s"
|
||||||
|
? `${$auth.user?.firstName} app`
|
||||||
|
: `${$auth.user.firstName}s app`
|
||||||
|
|
||||||
|
$values.name = resolveAppName(
|
||||||
|
template,
|
||||||
|
!$auth.user?.firstName ? "My app" : defaultAppName
|
||||||
|
)
|
||||||
nameToUrl($values.name)
|
nameToUrl($values.name)
|
||||||
await setupValidation()
|
await setupValidation()
|
||||||
})
|
})
|
||||||
|
@ -42,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveAppName = (template, name) => {
|
const resolveAppName = (template, name) => {
|
||||||
if (template && !name) {
|
if (template && !template.fromFile) {
|
||||||
return template.name
|
return template.name
|
||||||
}
|
}
|
||||||
return name ? name.trim() : null
|
return name ? name.trim() : null
|
||||||
|
@ -104,6 +119,22 @@
|
||||||
// Create user
|
// Create user
|
||||||
await API.updateOwnMetadata({ roleId: $values.roleId })
|
await API.updateOwnMetadata({ roleId: $values.roleId })
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
|
|
||||||
|
// Create a default home screen if no template was selected
|
||||||
|
if (template == null) {
|
||||||
|
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||||
|
defaultScreenTemplate.routing.route = "/home"
|
||||||
|
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||||
|
try {
|
||||||
|
await store.actions.screens.save(defaultScreenTemplate)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Could not create a default application screen", err)
|
||||||
|
notifications.warning(
|
||||||
|
"Encountered an issue creating the default screen."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
creating = false
|
creating = false
|
||||||
|
@ -141,15 +172,14 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
|
autofocus={true}
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
on:change={nameToUrl($values.name)}
|
on:change={nameToUrl($values.name)}
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder={$auth.user?.firstName
|
placeholder={defaultAppName}
|
||||||
? `${$auth.user.firstName}s app`
|
|
||||||
: "My app"}
|
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import { Body, ProgressBar, Label } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
export let usage
|
||||||
|
|
||||||
|
let percentage
|
||||||
|
let unlimited = false
|
||||||
|
|
||||||
|
const isUnlimited = () => {
|
||||||
|
if (usage.total === -1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPercentage = () => {
|
||||||
|
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
unlimited = isUnlimited()
|
||||||
|
percentage = getPercentage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="usage">
|
||||||
|
<div class="info">
|
||||||
|
<Label size="XL">{usage.name}</Label>
|
||||||
|
{#if unlimited}
|
||||||
|
<Body size="S">{usage.used}</Body>
|
||||||
|
{:else}
|
||||||
|
<Body size="S">{usage.used} / {usage.total}</Body>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if unlimited}
|
||||||
|
<Body size="S">Unlimited</Body>
|
||||||
|
{:else}
|
||||||
|
<ProgressBar width={"100%"} duration={1} value={percentage} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.usage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,12 @@
|
||||||
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
|
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
|
||||||
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
$: automation = $automationStore.automations[0]
|
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
||||||
|
|
||||||
|
$: automation =
|
||||||
|
$automationStore.selectedAutomation?.automation ||
|
||||||
|
$automationStore.automations[0]
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let webhookModal
|
let webhookModal
|
||||||
</script>
|
</script>
|
||||||
|
@ -39,6 +44,12 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if automation?.showTestPanel}
|
||||||
|
<div class="setup">
|
||||||
|
<TestPanel {automation} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateAutomationModal {webhookModal} />
|
<CreateAutomationModal {webhookModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -52,7 +63,9 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
height: 0;
|
height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 260px minmax(510px, 1fr);
|
grid-auto-flow: column dense;
|
||||||
|
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
@ -64,17 +77,18 @@
|
||||||
border-right: var(--border-light);
|
border-right: var(--border-light);
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--spacing-l) 40px;
|
padding-top: var(--spacing-l);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.centered {
|
.centered {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -92,4 +106,17 @@
|
||||||
.main {
|
.main {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
padding-top: var(--spectrum-global-dimension-size-200);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
background-color: var(--background);
|
||||||
|
grid-column: 3;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right" dataCy="user-menu">
|
||||||
<div slot="control" class="avatar">
|
<div slot="control" class="avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="M"
|
size="M"
|
||||||
|
|
|
@ -31,7 +31,20 @@
|
||||||
$: menu = buildMenu($auth.isAdmin)
|
$: menu = buildMenu($auth.isAdmin)
|
||||||
|
|
||||||
const buildMenu = admin => {
|
const buildMenu = admin => {
|
||||||
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
|
let menu = [
|
||||||
|
{
|
||||||
|
title: "Apps",
|
||||||
|
href: "/builder/portal/apps",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||||
|
menu = menu.concat([
|
||||||
|
{
|
||||||
|
title: "Usage",
|
||||||
|
href: "/builder/portal/settings/usage",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
if (admin) {
|
if (admin) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
|
@ -160,7 +173,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-dropdown">
|
<div class="user-dropdown">
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right" dataCy="user-menu">
|
||||||
<div slot="control" class="avatar">
|
<div slot="control" class="avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="M"
|
size="M"
|
||||||
|
@ -169,7 +182,11 @@
|
||||||
/>
|
/>
|
||||||
<Icon size="XL" name="ChevronDown" />
|
<Icon size="XL" name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
<MenuItem
|
||||||
|
icon="UserEdit"
|
||||||
|
on:click={() => userInfoModal.show()}
|
||||||
|
dataCy={"user-info"}
|
||||||
|
>
|
||||||
Update user information
|
Update user information
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#if $auth.isBuilder}
|
{#if $auth.isBuilder}
|
||||||
|
@ -186,7 +203,9 @@
|
||||||
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
||||||
Close developer mode
|
Close developer mode
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon="LogOut" on:click={logout}>Log out</MenuItem>
|
<MenuItem dataCy="user-logout" icon="LogOut" on:click={logout}
|
||||||
|
>Log out
|
||||||
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -315,7 +334,7 @@
|
||||||
|
|
||||||
.mobile-toggle,
|
.mobile-toggle,
|
||||||
.user-dropdown {
|
.user-dropdown {
|
||||||
flex: 1 1 0;
|
flex: 0 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduce BBUI page padding */
|
/* Reduce BBUI page padding */
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
Body,
|
Body,
|
||||||
Modal,
|
Modal,
|
||||||
Divider,
|
Divider,
|
||||||
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
@ -60,16 +61,15 @@
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="XL">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<ActionButton
|
||||||
quiet
|
|
||||||
secondary
|
secondary
|
||||||
icon={"ChevronLeft"}
|
icon={"ArrowLeft"}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</ActionButton>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Detail,
|
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
@ -11,7 +10,6 @@
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
Divider,
|
|
||||||
Helpers,
|
Helpers,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
@ -67,6 +65,9 @@
|
||||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
|
||||||
|
$: unlocked = lockedApps?.length == 0
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
|
@ -179,8 +180,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewApp = app => {
|
const appOverview = app => {
|
||||||
window.open(`/${app.devId}`)
|
$goto(`../overview/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
|
@ -304,7 +305,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="M">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
|
@ -314,29 +315,17 @@
|
||||||
{welcomeBody}
|
{welcomeBody}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if !$apps?.length}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
|
||||||
dataCy="create-app-btn"
|
|
||||||
size="M"
|
|
||||||
icon="Add"
|
|
||||||
cta
|
|
||||||
on:click={initiateAppCreation}
|
|
||||||
>
|
|
||||||
{createAppButtonText}
|
|
||||||
</Button>
|
|
||||||
{#if $apps?.length > 0}
|
|
||||||
<Button
|
<Button
|
||||||
icon="Experience"
|
dataCy="create-app-btn"
|
||||||
size="M"
|
size="M"
|
||||||
quiet
|
icon="Add"
|
||||||
secondary
|
cta
|
||||||
on:click={$goto("/builder/portal/apps/templates")}
|
on:click={initiateAppCreation}
|
||||||
>
|
>
|
||||||
Templates
|
{createAppButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
|
||||||
{#if !$apps?.length}
|
|
||||||
<Button
|
<Button
|
||||||
dataCy="import-app-btn"
|
dataCy="import-app-btn"
|
||||||
icon="Import"
|
icon="Import"
|
||||||
|
@ -347,15 +336,9 @@
|
||||||
>
|
>
|
||||||
Import app
|
Import app
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Layout gap="S" justifyItems="center">
|
|
||||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
<Divider size="S" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$apps?.length && $templates?.length}
|
{#if !$apps?.length && $templates?.length}
|
||||||
|
@ -363,9 +346,42 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if enrichedApps.length}
|
{#if enrichedApps.length}
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Detail size="L">Apps</Detail>
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
dataCy="create-app-btn"
|
||||||
|
size="M"
|
||||||
|
icon="Add"
|
||||||
|
cta
|
||||||
|
on:click={initiateAppCreation}
|
||||||
|
>
|
||||||
|
{createAppButtonText}
|
||||||
|
</Button>
|
||||||
|
{#if $apps?.length > 0}
|
||||||
|
<Button
|
||||||
|
icon="Experience"
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={$goto("/builder/portal/apps/templates")}
|
||||||
|
>
|
||||||
|
Templates
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if !$apps?.length}
|
||||||
|
<Button
|
||||||
|
dataCy="import-app-btn"
|
||||||
|
icon="Import"
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={initiateAppImport}
|
||||||
|
>
|
||||||
|
Import app
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if enrichedApps.length > 1}
|
{#if enrichedApps.length > 1}
|
||||||
<div class="app-actions">
|
<div class="app-actions">
|
||||||
{#if cloud}
|
{#if cloud}
|
||||||
|
@ -397,7 +413,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="appTable">
|
<div class="appTable" class:unlocked>
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow
|
<AppRow
|
||||||
{copyAppId}
|
{copyAppId}
|
||||||
|
@ -410,7 +426,7 @@
|
||||||
{exportApp}
|
{exportApp}
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
{previewApp}
|
{appOverview}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -471,6 +487,9 @@
|
||||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.appTable {
|
||||||
|
border-top: var(--border-light);
|
||||||
|
}
|
||||||
.app-actions {
|
.app-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -478,7 +497,7 @@
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.title .welcome > .buttons {
|
.title .welcome > .buttons {
|
||||||
padding-top: 30px;
|
padding-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -514,6 +533,11 @@
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appTable.unlocked {
|
||||||
|
grid-template-columns: 1fr 1fr auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
.appTable :global(> div) {
|
.appTable :global(> div) {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { Layout, Page, notifications, Button } from "@budibase/bbui"
|
import { Layout, Page, notifications, ActionButton } from "@budibase/bbui"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { templates } from "stores/portal"
|
import { templates } from "stores/portal"
|
||||||
|
@ -25,16 +25,15 @@
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="XL">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<ActionButton
|
||||||
quiet
|
|
||||||
secondary
|
secondary
|
||||||
icon={"ChevronLeft"}
|
icon={"ArrowLeft"}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</ActionButton>
|
||||||
</span>
|
</span>
|
||||||
{#if loaded && $templates?.length}
|
{#if loaded && $templates?.length}
|
||||||
<TemplateDisplay templates={$templates} />
|
<TemplateDisplay templates={$templates} />
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin}
|
{#if $auth.isAdmin}
|
||||||
<Page wide={$page.path.includes("email/:template")}>
|
<Page maxWidth="90ch" wide={$page.path.includes("email/:template")}>
|
||||||
<slot />
|
<slot />
|
||||||
</Page>
|
</Page>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -297,7 +297,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
{#if providers.google}
|
{#if providers.google}
|
||||||
<Divider />
|
<Divider size="S" />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">
|
<Heading size="S">
|
||||||
<div class="provider-title">
|
<div class="provider-title">
|
||||||
|
@ -336,7 +336,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
{#if providers.oidc}
|
{#if providers.oidc}
|
||||||
<Divider />
|
<Divider size="S" />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">
|
<Heading size="S">
|
||||||
<div class="provider-title">
|
<div class="provider-title">
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
Table,
|
Table,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { email } from "stores/portal"
|
import { email, admin } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
@ -58,6 +58,7 @@
|
||||||
const savedConfig = await API.saveConfig(smtp)
|
const savedConfig = await API.saveConfig(smtp)
|
||||||
smtpConfig._rev = savedConfig._rev
|
smtpConfig._rev = savedConfig._rev
|
||||||
smtpConfig._id = savedConfig._id
|
smtpConfig._id = savedConfig._id
|
||||||
|
await admin.getChecklist()
|
||||||
notifications.success(`Settings saved`)
|
notifications.success(`Settings saved`)
|
||||||
analytics.captureEvent(Events.SMTP.SAVED)
|
analytics.captureEvent(Events.SMTP.SAVED)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -67,6 +68,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteSmtp() {
|
||||||
|
// Delete the SMTP config
|
||||||
|
try {
|
||||||
|
await API.deleteConfig({
|
||||||
|
id: smtpConfig._id,
|
||||||
|
rev: smtpConfig._rev,
|
||||||
|
})
|
||||||
|
smtpConfig = {
|
||||||
|
config: {},
|
||||||
|
}
|
||||||
|
await admin.getChecklist()
|
||||||
|
notifications.success(`Settings cleared`)
|
||||||
|
analytics.captureEvent(Events.SMTP.SAVED)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to clear email settings, reason: ${error?.message || "Unknown"}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchSmtp() {
|
async function fetchSmtp() {
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
try {
|
||||||
|
@ -111,7 +132,7 @@
|
||||||
values below and click activate.
|
values below and click activate.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider size="S" />
|
||||||
{#if smtpConfig}
|
{#if smtpConfig}
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">SMTP</Heading>
|
<Heading size="S">SMTP</Heading>
|
||||||
|
@ -155,10 +176,17 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
<div>
|
<div class="spectrum-ButtonGroup spectrum-Settings-buttonGroup">
|
||||||
<Button cta on:click={saveSmtp}>Save</Button>
|
<Button cta on:click={saveSmtp}>Save</Button>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={deleteSmtp}
|
||||||
|
disabled={!$admin.checklist.smtp.checked}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider size="S" />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Templates</Heading>
|
<Heading size="S">Templates</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
|
@ -185,4 +213,8 @@
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.spectrum-Settings-buttonGroup {
|
||||||
|
gap: var(--spectrum-global-dimension-static-size-200);
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,413 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
ActionButton,
|
||||||
|
ButtonGroup,
|
||||||
|
Heading,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
notifications,
|
||||||
|
ProgressCircle,
|
||||||
|
Input,
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Helpers,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import OverviewTab from "../_components/OverviewTab.svelte"
|
||||||
|
import SettingsTab from "../_components/SettingsTab.svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { apps, auth } from "stores/portal"
|
||||||
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import AppLockModal from "components/common/AppLockModal.svelte"
|
||||||
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let application
|
||||||
|
|
||||||
|
let promise = getPackage()
|
||||||
|
let loaded = false
|
||||||
|
let deletionModal
|
||||||
|
let unpublishModal
|
||||||
|
let appName = ""
|
||||||
|
|
||||||
|
// App
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
|
||||||
|
// Locking
|
||||||
|
$: lockedBy = selectedApp?.lockedBy
|
||||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||||
|
$: lockIdentifer = `${
|
||||||
|
lockedBy && Object.prototype.hasOwnProperty.call(lockedBy, "firstName")
|
||||||
|
? lockedBy?.firstName
|
||||||
|
: lockedBy?.email
|
||||||
|
}`
|
||||||
|
|
||||||
|
// App deployments
|
||||||
|
$: deployments = []
|
||||||
|
$: latestDeployments = deployments
|
||||||
|
.filter(
|
||||||
|
deployment =>
|
||||||
|
deployment.status === "SUCCESS" && application === deployment.appId
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
|
$: isPublished =
|
||||||
|
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
||||||
|
|
||||||
|
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
||||||
|
$: tabs = ["Overview", "Automation History", "Backups", "Settings"]
|
||||||
|
$: selectedTab = "Overview"
|
||||||
|
|
||||||
|
const backToAppList = () => {
|
||||||
|
$goto(`../../../portal/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTabChange = tabKey => {
|
||||||
|
if (tabKey === selectedTab) {
|
||||||
|
return
|
||||||
|
} else if (tabKey && tabs.indexOf(tabKey) > -1) {
|
||||||
|
selectedTab = tabKey
|
||||||
|
} else {
|
||||||
|
notifications.error("Invalid tab key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPackage() {
|
||||||
|
try {
|
||||||
|
const pkg = await API.fetchAppPackage(application)
|
||||||
|
await store.actions.initialise(pkg)
|
||||||
|
loaded = true
|
||||||
|
return pkg
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error initialising app: ${error?.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||||
|
if (deployments.length > 0) {
|
||||||
|
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
||||||
|
if (pending.length) {
|
||||||
|
notifications.warning(
|
||||||
|
"Deployment has been queued and will be processed shortly"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDeployments() {
|
||||||
|
try {
|
||||||
|
const newDeployments = await API.getAppDeployments()
|
||||||
|
reviewPendingDeployments(deployments, newDeployments)
|
||||||
|
return newDeployments
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error fetching deployment history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
if (isPublished) {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: $store.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
window.open(appUrl, "_blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editApp = app => {
|
||||||
|
if (lockedBy && !lockedByYou) {
|
||||||
|
notifications.warning(
|
||||||
|
`App locked by ${lockIdentifer}. Please allow lock to expire or have them unlock this app.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$goto(`../../../app/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyAppId = async app => {
|
||||||
|
await Helpers.copyToClipboard(app.prodId)
|
||||||
|
notifications.success("App ID copied to clipboard.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportApp = app => {
|
||||||
|
const id = isPublished ? app.prodId : app.devId
|
||||||
|
const appName = encodeURIComponent(app.name)
|
||||||
|
window.location = `/api/backups/export?appId=${id}&appname=${appName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpublishApp = app => {
|
||||||
|
selectedApp = app
|
||||||
|
unpublishModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnpublishApp = async () => {
|
||||||
|
if (!selectedApp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
analytics.captureEvent(Events.APP.UNPUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
})
|
||||||
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("App unpublished successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error unpublishing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteApp = app => {
|
||||||
|
selectedApp = app
|
||||||
|
deletionModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteApp = async () => {
|
||||||
|
if (!selectedApp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await API.deleteApp(selectedApp?.devId)
|
||||||
|
backToAppList()
|
||||||
|
notifications.success("App deleted successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error deleting app")
|
||||||
|
}
|
||||||
|
selectedApp = null
|
||||||
|
appName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
store.actions.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
if (!apps.length) {
|
||||||
|
await apps.load()
|
||||||
|
}
|
||||||
|
await API.syncApp(application)
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error initialising app overview")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="overview-wrap">
|
||||||
|
<Page wide noPadding>
|
||||||
|
{#await promise}
|
||||||
|
<span class="page-header">
|
||||||
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
<div class="loading">
|
||||||
|
<ProgressCircle size="XL" />
|
||||||
|
</div>
|
||||||
|
{:then _}
|
||||||
|
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
|
||||||
|
<span class="page-header" class:loaded>
|
||||||
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
<div class="overview-header">
|
||||||
|
<div class="app-title">
|
||||||
|
<div class="app-logo">
|
||||||
|
<div
|
||||||
|
class="app-icon"
|
||||||
|
style="color: {selectedApp?.icon?.color || ''}"
|
||||||
|
>
|
||||||
|
<EditableIcon
|
||||||
|
app={selectedApp}
|
||||||
|
size="XL"
|
||||||
|
name={selectedApp?.icon?.name || "Apps"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-details">
|
||||||
|
<Heading size="M">{selectedApp?.name}</Heading>
|
||||||
|
<div class="app-url">{appUrl}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<AppLockModal app={selectedApp} />
|
||||||
|
<ButtonGroup gap="XS">
|
||||||
|
<Button
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon="Globe"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={viewApp}
|
||||||
|
dataCy="view-app"
|
||||||
|
>
|
||||||
|
View app
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="M"
|
||||||
|
cta
|
||||||
|
icon="Edit"
|
||||||
|
disabled={lockedBy && !lockedByYou}
|
||||||
|
on:click={() => {
|
||||||
|
editApp(selectedApp)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Edit</span>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ActionMenu align="right" dataCy="app-overview-menu-popover">
|
||||||
|
<span slot="control" class="app-overview-actions-icon">
|
||||||
|
<Icon hoverable name="More" />
|
||||||
|
</span>
|
||||||
|
<MenuItem on:click={() => exportApp(selectedApp)} icon="Download">
|
||||||
|
Export
|
||||||
|
</MenuItem>
|
||||||
|
{#if isPublished}
|
||||||
|
<MenuItem
|
||||||
|
on:click={() => unpublishApp(selectedApp)}
|
||||||
|
icon="GlobeRemove"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem on:click={() => copyAppId(selectedApp)} icon="Copy">
|
||||||
|
Copy App ID
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
|
{#if !isPublished}
|
||||||
|
<MenuItem on:click={() => deleteApp(selectedApp)} icon="Delete">
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<div class="tab-wrap">
|
||||||
|
<Tabs
|
||||||
|
selected={selectedTab}
|
||||||
|
noPadding
|
||||||
|
on:select={e => {
|
||||||
|
selectedTab = e.detail
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab title="Overview">
|
||||||
|
<OverviewTab
|
||||||
|
app={selectedApp}
|
||||||
|
deployments={latestDeployments}
|
||||||
|
navigateTab={handleTabChange}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
{#if false}
|
||||||
|
<Tab title="Automation History">
|
||||||
|
<div class="container">Automation History contents</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Backups">
|
||||||
|
<div class="container">Backups contents</div>
|
||||||
|
</Tab>
|
||||||
|
{/if}
|
||||||
|
<Tab title="Settings">
|
||||||
|
<SettingsTab app={selectedApp} />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deletionModal}
|
||||||
|
title="Confirm deletion"
|
||||||
|
okText="Delete app"
|
||||||
|
onOk={confirmDeleteApp}
|
||||||
|
onCancel={() => (appName = null)}
|
||||||
|
disabled={appName !== selectedApp?.name}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
|
||||||
|
|
||||||
|
<p>Please enter the app name below to confirm.</p>
|
||||||
|
<Input
|
||||||
|
bind:value={appName}
|
||||||
|
data-cy="delete-app-confirmation"
|
||||||
|
placeholder={selectedApp?.name}
|
||||||
|
/>
|
||||||
|
</ConfirmDialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
dataCy={"unpublish-modal"}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
{:catch error}
|
||||||
|
<p>Something went wrong: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
</Page>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-url {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.overview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header.loaded {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-wrap :global(> div > .container),
|
||||||
|
.tab-wrap :global(.spectrum-Tabs) {
|
||||||
|
background-color: var(--background);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.overview-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.overview-wrap :global(.content > *) {
|
||||||
|
padding: calc(var(--spacing-xl) * 1.5) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-title {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-details :global(.spectrum-Heading) {
|
||||||
|
line-height: 1em;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.tab-wrap :global(.spectrum-Tabs) {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-top: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
//export let app
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="automation-tab" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.automation-tab {
|
||||||
|
color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,250 @@
|
||||||
|
<script>
|
||||||
|
import DashCard from "components/common/DashCard.svelte"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
Avatar,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { users, auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let deployments
|
||||||
|
export let navigateTab
|
||||||
|
|
||||||
|
const userInit = async () => {
|
||||||
|
try {
|
||||||
|
await users.init()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting user list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let userPromise = userInit()
|
||||||
|
|
||||||
|
$: updateAvailable = clientPackage.version !== $store.version
|
||||||
|
$: isPublished = app && app?.status === AppStatus.DEPLOYED
|
||||||
|
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||||
|
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||||
|
$: filteredUsers = !appEditorId
|
||||||
|
? []
|
||||||
|
: $users.filter(user => user._id === appEditorId)
|
||||||
|
|
||||||
|
$: appEditor = filteredUsers.length ? filteredUsers[0] : null
|
||||||
|
|
||||||
|
const getInitials = user => {
|
||||||
|
let initials = ""
|
||||||
|
initials += user.firstName ? user.firstName[0] : ""
|
||||||
|
initials += user.lastName ? user.lastName[0] : ""
|
||||||
|
|
||||||
|
return initials == "" ? user.email[0] : initials
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overview-tab">
|
||||||
|
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
|
||||||
|
<div class="top">
|
||||||
|
<DashCard title={"App Status"} dataCy={"app-status"}>
|
||||||
|
<div class="status-content">
|
||||||
|
<div class="status-display">
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon name="GlobeCheck" size="XL" disabled={false} />
|
||||||
|
<span>Published</span>
|
||||||
|
{:else}
|
||||||
|
<Icon name="GlobeStrike" size="XL" disabled={true} />
|
||||||
|
<span class="disabled"> Unpublished </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-text">
|
||||||
|
{#if deployments?.length}
|
||||||
|
{processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(deployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
{#if !deployments?.length}
|
||||||
|
-
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||||
|
<div class="last-edited-content">
|
||||||
|
{#await userPromise}
|
||||||
|
<Avatar size="M" initials={"-"} />
|
||||||
|
{:then _}
|
||||||
|
<div class="updated-by">
|
||||||
|
{#if appEditor}
|
||||||
|
<Avatar size="M" initials={getInitials(appEditor)} />
|
||||||
|
<div class="editor-name">
|
||||||
|
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:catch error}
|
||||||
|
<p>Could not fetch user: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
<div class="last-edit-text">
|
||||||
|
{#if app}
|
||||||
|
{processStringSync(
|
||||||
|
"Last edited {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard
|
||||||
|
title={"App Version"}
|
||||||
|
showIcon={true}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Settings")
|
||||||
|
}}
|
||||||
|
dataCy={"app-version"}
|
||||||
|
>
|
||||||
|
<div class="version-content" data-cy={$store.version}>
|
||||||
|
<Heading size="XS">{$store.version}</Heading>
|
||||||
|
{#if updateAvailable}
|
||||||
|
<div class="version-status">
|
||||||
|
New version <strong>{clientPackage.version}</strong> is available
|
||||||
|
-
|
||||||
|
<Link
|
||||||
|
on:click={() => {
|
||||||
|
if (typeof navigateTab === "function") {
|
||||||
|
navigateTab("Settings")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="version-status">You're running the latest!</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
</div>
|
||||||
|
{#if false}
|
||||||
|
<div class="bottom">
|
||||||
|
<DashCard
|
||||||
|
title={"Automation History"}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Automation History")
|
||||||
|
}}
|
||||||
|
dataCy={"automation-history"}
|
||||||
|
>
|
||||||
|
<div class="automation-content">
|
||||||
|
<div class="automation-metrics">
|
||||||
|
<div class="succeeded">
|
||||||
|
<Heading size="XL">0</Heading>
|
||||||
|
<div class="metric-info">
|
||||||
|
<Icon name="CheckmarkCircle" />
|
||||||
|
Success
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="failed">
|
||||||
|
<Heading size="XL">0</Heading>
|
||||||
|
<div class="metric-info">
|
||||||
|
<Icon name="Alert" />
|
||||||
|
Error
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard
|
||||||
|
title={"Backups"}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Backups")
|
||||||
|
}}
|
||||||
|
dataCy={"backups"}
|
||||||
|
>
|
||||||
|
<div class="backups-content">test</div>
|
||||||
|
</DashCard>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overview-tab {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-tab .top {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spectrum-alias-grid-gutter-medium);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-tab .bottom,
|
||||||
|
.automation-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.overview-tab .top {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.overview-tab .bottom {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.overview-tab .top,
|
||||||
|
.overview-tab .bottom {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.status-text,
|
||||||
|
.last-edit-text {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.updated-by {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.succeeded :global(.icon) {
|
||||||
|
color: var(--spectrum-global-color-green-600);
|
||||||
|
}
|
||||||
|
.failed :global(.icon) {
|
||||||
|
color: var(
|
||||||
|
--spectrum-semantic-negative-color-default,
|
||||||
|
var(--spectrum-global-color-red-500)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.metric-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.version-status,
|
||||||
|
.last-edit-text,
|
||||||
|
.status-text {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
|
||||||
|
let versionModal
|
||||||
|
let updatingModal
|
||||||
|
let selfHostPath =
|
||||||
|
"https://docs.budibase.com/docs/hosting-methods#self-host-budibase"
|
||||||
|
|
||||||
|
$: updateAvailable = clientPackage.version !== $store.version
|
||||||
|
$: appUrl = `${window.origin}/app${app?.url}`
|
||||||
|
$: appDeployed = app.status === AppStatus.DEPLOYED
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="settings-tab">
|
||||||
|
<Page wide={false}>
|
||||||
|
<Layout gap="XL" paddingY="XXL" paddingX="">
|
||||||
|
<span class="details-section">
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Name and URL</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
<div class="app-details">
|
||||||
|
<div class="app-name">
|
||||||
|
<div class="name-title detail-title">Name</div>
|
||||||
|
<div class="name">{app?.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-url">
|
||||||
|
<div class="url-title detail-title">Url Path</div>
|
||||||
|
<div class="url">{appUrl}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-action">
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
updatingModal.show()
|
||||||
|
}}
|
||||||
|
disabled={appDeployed}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
<span class="version-section">
|
||||||
|
<Layout gap="XS" paddingY="XXL" paddingX="">
|
||||||
|
<Heading size="S">App version</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
{#if updateAvailable}
|
||||||
|
<p class="version-status">
|
||||||
|
The app is currently using version
|
||||||
|
<strong>{$store.version}</strong>
|
||||||
|
but version <strong>{clientPackage.version}</strong> is available.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="version-status">
|
||||||
|
The app is currently using version
|
||||||
|
<strong>{$store.version}</strong>. You're running the latest!
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
Updates can contain new features, performance improvements and bug
|
||||||
|
fixes.
|
||||||
|
|
||||||
|
<div class="page-action">
|
||||||
|
<Button cta on:click={versionModal.show()}>Update app</Button>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
<span class="selfhost-section">
|
||||||
|
<Layout gap="XS" paddingY="XXL" paddingX="">
|
||||||
|
<Heading size="S">Self-host Budibase</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
Self-host Budibase for free to get unlimited apps and more - and it
|
||||||
|
only takes a few minutes!
|
||||||
|
<div class="page-action">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open(selfHostPath, "_blank")
|
||||||
|
}}>Self-host Budibase</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
</Layout>
|
||||||
|
<VersionModal bind:this={versionModal} hideIcon={true} />
|
||||||
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal {app} />
|
||||||
|
</Modal>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-action {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.detail-title {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: var(
|
||||||
|
--spectrum-alias-font-size-default,
|
||||||
|
var(--spectrum-global-dimension-font-size-100)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,6 +2,6 @@
|
||||||
import { Page } from "@budibase/bbui"
|
import { Page } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page maxWidth="90ch">
|
||||||
<slot />
|
<slot />
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
Link,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
|
import Usage from "components/usage/Usage.svelte"
|
||||||
|
|
||||||
|
let staticUsage = []
|
||||||
|
let monthlyUsage = []
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: quotaUsage = $licensing.quotaUsage
|
||||||
|
$: license = $auth.user?.license
|
||||||
|
|
||||||
|
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
const setMonthlyUsage = () => {
|
||||||
|
monthlyUsage = []
|
||||||
|
if (quotaUsage.monthly) {
|
||||||
|
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
|
||||||
|
const used = quotaUsage.monthly.current[key]
|
||||||
|
if (used !== undefined) {
|
||||||
|
monthlyUsage.push({
|
||||||
|
name: value.name,
|
||||||
|
used: used,
|
||||||
|
total: value.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStaticUsage = () => {
|
||||||
|
staticUsage = []
|
||||||
|
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
|
||||||
|
const used = quotaUsage.usageQuota[key]
|
||||||
|
if (used !== undefined) {
|
||||||
|
staticUsage.push({
|
||||||
|
name: value.name,
|
||||||
|
used: used,
|
||||||
|
total: value.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const capitalise = string => {
|
||||||
|
if (string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
await licensing.getQuotaUsage()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
notifications.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await init()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (license && quotaUsage) {
|
||||||
|
setMonthlyUsage()
|
||||||
|
setStaticUsage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<Layout>
|
||||||
|
<Heading>Usage</Heading>
|
||||||
|
<Body
|
||||||
|
>Get information about your current usage within Budibase.
|
||||||
|
{#if $admin.cloud}
|
||||||
|
{#if $auth.user?.accountPortalAccess}
|
||||||
|
To upgrade your plan and usage limits visit your <Link
|
||||||
|
size="L"
|
||||||
|
href={upgradeUrl}>Account</Link
|
||||||
|
>.
|
||||||
|
{:else}
|
||||||
|
Contact your account holder to upgrade your usage limits.
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Divider size="S" />
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout gap="XS">
|
||||||
|
<Body size="S">YOUR PLAN</Body>
|
||||||
|
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Body size="S">USAGE</Body>
|
||||||
|
<div class="usages">
|
||||||
|
{#each staticUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage {usage} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{#if monthlyUsage.length}
|
||||||
|
<Layout gap="S">
|
||||||
|
<Body size="S">MONTHLY</Body>
|
||||||
|
<div class="usages">
|
||||||
|
{#each monthlyUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage {usage} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<div />
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.usages {
|
||||||
|
display: grid;
|
||||||
|
column-gap: 60px;
|
||||||
|
row-gap: 50px;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue