Merge branch 'feature/new-app-publish-workflow' of github.com:Budibase/budibase into new-design-ui-dirty
This commit is contained in:
commit
d269354d6f
|
@ -12,6 +12,11 @@ on:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||||
|
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -27,6 +32,10 @@ jobs:
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
|
@ -19,6 +19,7 @@ env:
|
||||||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -29,6 +30,10 @@ jobs:
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro develop
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
@ -46,9 +51,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
# setup the username and email.
|
||||||
git config user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config user.email "<>"
|
git config --global user.email "<>"
|
||||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||||
yarn release:develop
|
yarn release:develop
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ env:
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -30,6 +31,10 @@ jobs:
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro master
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
|
@ -11,7 +11,7 @@ sources:
|
||||||
- https://github.com/Budibase/budibase
|
- https://github.com/Budibase/budibase
|
||||||
- https://budibase.com
|
- https://budibase.com
|
||||||
type: application
|
type: application
|
||||||
version: 0.2.8
|
version: 0.2.9
|
||||||
appVersion: 1.0.48
|
appVersion: 1.0.48
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
|
|
|
@ -98,10 +98,6 @@ spec:
|
||||||
value: http://worker-service:{{ .Values.services.worker.port }}
|
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||||
- name: PLATFORM_URL
|
- name: PLATFORM_URL
|
||||||
value: {{ .Values.globals.platformUrl | quote }}
|
value: {{ .Values.globals.platformUrl | quote }}
|
||||||
- name: USE_QUOTAS
|
|
||||||
value: {{ .Values.globals.useQuotas | quote }}
|
|
||||||
- name: EXCLUDE_QUOTAS_TENANTS
|
|
||||||
value: {{ .Values.globals.excludeQuotasTenants | quote }}
|
|
||||||
- name: ACCOUNT_PORTAL_URL
|
- name: ACCOUNT_PORTAL_URL
|
||||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
- name: ACCOUNT_PORTAL_API_KEY
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
|
@ -114,12 +110,23 @@ spec:
|
||||||
value: {{ .Values.globals.google.clientId | quote }}
|
value: {{ .Values.globals.google.clientId | quote }}
|
||||||
- name: GOOGLE_CLIENT_SECRET
|
- name: GOOGLE_CLIENT_SECRET
|
||||||
value: {{ .Values.globals.google.secret | quote }}
|
value: {{ .Values.globals.google.secret | quote }}
|
||||||
|
- name: AUTOMATION_MAX_ITERATIONS
|
||||||
|
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.apps.port }}
|
- containerPort: {{ .Values.services.apps.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -39,5 +39,13 @@ spec:
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: couchdb-backup
|
name: couchdb-backup
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -60,6 +60,14 @@ spec:
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /data
|
- mountPath: /data
|
||||||
name: minio-data
|
name: minio-data
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -32,6 +32,14 @@ spec:
|
||||||
- containerPort: {{ .Values.services.proxy.port }}
|
- containerPort: {{ .Values.services.proxy.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -39,6 +39,14 @@ spec:
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /data
|
- mountPath: /data
|
||||||
name: redis-data
|
name: redis-data
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -121,6 +121,14 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.worker.port }}
|
- containerPort: {{ .Values.services.worker.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -93,16 +93,15 @@ globals:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||||
useQuotas: "0"
|
|
||||||
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
|
|
||||||
accountPortalUrl: ""
|
accountPortalUrl: ""
|
||||||
accountPortalApiKey: ""
|
accountPortalApiKey: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
platformUrl: ""
|
platformUrl: ""
|
||||||
httpMigrations: "0"
|
httpMigrations: "0"
|
||||||
google:
|
google:
|
||||||
clientId: ""
|
clientId: ""
|
||||||
secret: ""
|
secret: ""
|
||||||
|
automationMaxIterations: "500"
|
||||||
|
|
||||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||||
|
|
||||||
|
@ -230,6 +229,8 @@ couchdb:
|
||||||
## Optional tolerations
|
## Optional tolerations
|
||||||
tolerations: []
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
service:
|
service:
|
||||||
# annotations:
|
# annotations:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
14
package.json
14
package.json
|
@ -21,18 +21,17 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
"bootstrap": "lerna link && lerna bootstrap",
|
"bootstrap": "lerna link && lerna bootstrap && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
"publishdev": "lerna run publishdev",
|
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
|
||||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||||
"release": "lerna publish patch --yes --force-publish",
|
"release:pro": "bash scripts/pro/release.sh",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
|
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
|
@ -74,6 +73,7 @@
|
||||||
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
|
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
|
||||||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||||
"security:audit": "node scripts/audit.js",
|
"security:audit": "node scripts/audit.js",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install",
|
||||||
|
"install:pro": "bash scripts/pro/install.sh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -13,6 +13,7 @@ exports.Cookies = {
|
||||||
|
|
||||||
exports.Headers = {
|
exports.Headers = {
|
||||||
API_KEY: "x-budibase-api-key",
|
API_KEY: "x-budibase-api-key",
|
||||||
|
LICENSE_KEY: "x-budibase-license-key",
|
||||||
API_VER: "x-budibase-api-version",
|
API_VER: "x-budibase-api-version",
|
||||||
APP_ID: "x-budibase-app-id",
|
APP_ID: "x-budibase-app-id",
|
||||||
TYPE: "x-budibase-type",
|
TYPE: "x-budibase-type",
|
||||||
|
|
|
@ -174,9 +174,11 @@ function getDB(key, opts) {
|
||||||
if (db && isEqual(opts, storedOpts)) {
|
if (db && isEqual(opts, storedOpts)) {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
const appId = exports.getAppId()
|
const appId = exports.getAppId()
|
||||||
const CouchDB = getCouch()
|
const CouchDB = getCouch()
|
||||||
let toUseAppId
|
let toUseAppId
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case ContextKeys.CURRENT_DB:
|
case ContextKeys.CURRENT_DB:
|
||||||
toUseAppId = appId
|
toUseAppId = appId
|
||||||
|
|
|
@ -23,6 +23,7 @@ exports.StaticDatabases = {
|
||||||
docs: {
|
docs: {
|
||||||
apiKeys: "apikeys",
|
apiKeys: "apikeys",
|
||||||
usageQuota: "usage_quota",
|
usageQuota: "usage_quota",
|
||||||
|
licenseInfo: "license_info",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// contains information about tenancy and so on
|
// contains information about tenancy and so on
|
||||||
|
|
|
@ -27,6 +27,7 @@ const UNICODE_MAX = "\ufff0"
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
BY_API_KEY: "by_api_key",
|
BY_API_KEY: "by_api_key",
|
||||||
|
USER_BY_BUILDERS: "by_builders",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = StaticDatabases
|
exports.StaticDatabases = StaticDatabases
|
||||||
|
@ -429,34 +430,9 @@ async function getScopedConfig(db, params) {
|
||||||
return configDoc && configDoc.config ? configDoc.config : configDoc
|
return configDoc && configDoc.config ? configDoc.config : configDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNewUsageQuotaDoc() {
|
|
||||||
return {
|
|
||||||
_id: StaticDatabases.GLOBAL.docs.usageQuota,
|
|
||||||
quotaReset: Date.now() + 2592000000,
|
|
||||||
usageQuota: {
|
|
||||||
automationRuns: 0,
|
|
||||||
rows: 0,
|
|
||||||
storage: 0,
|
|
||||||
apps: 0,
|
|
||||||
users: 0,
|
|
||||||
views: 0,
|
|
||||||
emails: 0,
|
|
||||||
},
|
|
||||||
usageLimits: {
|
|
||||||
automationRuns: 1000,
|
|
||||||
rows: 4000,
|
|
||||||
apps: 4,
|
|
||||||
storage: 1000,
|
|
||||||
users: 10,
|
|
||||||
emails: 50,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.Replication = Replication
|
exports.Replication = Replication
|
||||||
exports.getScopedConfig = getScopedConfig
|
exports.getScopedConfig = getScopedConfig
|
||||||
exports.generateConfigID = generateConfigID
|
exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
|
||||||
exports.generateDevInfoID = generateDevInfoID
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
|
|
|
@ -56,10 +56,34 @@ exports.createApiKeyView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createUserBuildersView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get("_design/database")
|
||||||
|
} catch (err) {
|
||||||
|
// no design doc, make one
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc.builder && doc.builder.global === true) {
|
||||||
|
emit(doc._id, doc._id)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.USER_BY_BUILDERS]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
const CreateFuncByName = {
|
const CreateFuncByName = {
|
||||||
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
|
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
}
|
}
|
||||||
// can pass DB in if working with something specific
|
// can pass DB in if working with something specific
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
PLATFORM_URL: process.env.PLATFORM_URL,
|
PLATFORM_URL: process.env.PLATFORM_URL,
|
||||||
|
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||||
isTest,
|
isTest,
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
class BudibaseError extends Error {
|
||||||
|
constructor(message, type, code) {
|
||||||
|
super(message)
|
||||||
|
this.type = type
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BudibaseError,
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
const licensing = require("./licensing")
|
||||||
|
|
||||||
|
const codes = {
|
||||||
|
...licensing.codes,
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
...licensing.types,
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
...licensing.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPublicError = err => {
|
||||||
|
let error
|
||||||
|
if (err.code || err.type) {
|
||||||
|
// add generic error information
|
||||||
|
error = {
|
||||||
|
code: err.code,
|
||||||
|
type: err.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.code && context[err.code]) {
|
||||||
|
error = {
|
||||||
|
...error,
|
||||||
|
// get any additional context from this error
|
||||||
|
...context[err.code](err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
codes,
|
||||||
|
types,
|
||||||
|
UsageLimitError: licensing.UsageLimitError,
|
||||||
|
getPublicError,
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
const { BudibaseError } = require("./base")
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
LICENSE_ERROR: "license_error",
|
||||||
|
}
|
||||||
|
|
||||||
|
const codes = {
|
||||||
|
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
||||||
|
return {
|
||||||
|
limitName: err.limitName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class UsageLimitError extends BudibaseError {
|
||||||
|
constructor(message, limitName) {
|
||||||
|
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
|
||||||
|
this.limitName = limitName
|
||||||
|
this.status = 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
types,
|
||||||
|
codes,
|
||||||
|
context,
|
||||||
|
UsageLimitError,
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const tenancy = require("../tenancy")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
||||||
|
* The env var is formatted as:
|
||||||
|
* tenant1:feature1:feature2,tenant2:feature1
|
||||||
|
*/
|
||||||
|
const getFeatureFlags = () => {
|
||||||
|
if (!env.TENANT_FEATURE_FLAGS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantFeatureFlags = {}
|
||||||
|
|
||||||
|
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => {
|
||||||
|
const [tenantId, ...features] = tenantToFeatures.split(":")
|
||||||
|
|
||||||
|
features.forEach(feature => {
|
||||||
|
if (!tenantFeatureFlags[tenantId]) {
|
||||||
|
tenantFeatureFlags[tenantId] = []
|
||||||
|
}
|
||||||
|
tenantFeatureFlags[tenantId].push(feature)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return tenantFeatureFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
||||||
|
|
||||||
|
exports.isEnabled = featureFlag => {
|
||||||
|
const tenantId = tenancy.getTenantId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
TENANT_FEATURE_FLAGS &&
|
||||||
|
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||||
|
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTenantFeatureFlags = tenantId => {
|
||||||
|
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||||
|
return TENANT_FEATURE_FLAGS[tenantId]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.FeatureFlag = {
|
||||||
|
LICENSING: "LICENSING",
|
||||||
|
}
|
|
@ -15,4 +15,9 @@ module.exports = {
|
||||||
auth: require("../auth"),
|
auth: require("../auth"),
|
||||||
constants: require("../constants"),
|
constants: require("../constants"),
|
||||||
migrations: require("../migrations"),
|
migrations: require("../migrations"),
|
||||||
|
errors: require("./errors"),
|
||||||
|
env: require("./environment"),
|
||||||
|
accounts: require("./cloud/accounts"),
|
||||||
|
tenancy: require("./tenancy"),
|
||||||
|
featureFlags: require("./featureFlags"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,27 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
|
||||||
async function authenticate(accessToken, refreshToken, profile, done) {
|
const buildVerifyFn = async saveUserFn => {
|
||||||
const thirdPartyUser = {
|
return (accessToken, refreshToken, profile, done) => {
|
||||||
provider: profile.provider, // should always be 'google'
|
const thirdPartyUser = {
|
||||||
providerType: "google",
|
provider: profile.provider, // should always be 'google'
|
||||||
userId: profile.id,
|
providerType: "google",
|
||||||
profile: profile,
|
userId: profile.id,
|
||||||
email: profile._json.email,
|
profile: profile,
|
||||||
oauth2: {
|
email: profile._json.email,
|
||||||
accessToken: accessToken,
|
oauth2: {
|
||||||
refreshToken: refreshToken,
|
accessToken: accessToken,
|
||||||
},
|
refreshToken: refreshToken,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return authenticateThirdParty(
|
return authenticateThirdParty(
|
||||||
thirdPartyUser,
|
thirdPartyUser,
|
||||||
true, // require local accounts to exist
|
true, // require local accounts to exist
|
||||||
done
|
done,
|
||||||
)
|
saveUserFn
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,11 +30,7 @@ async function authenticate(accessToken, refreshToken, profile, done) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport Google Strategy
|
* @returns Dynamically configured Passport Google Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (
|
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
config,
|
|
||||||
callbackUrl,
|
|
||||||
verify = authenticate
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret } = config
|
const { clientID, clientSecret } = config
|
||||||
|
|
||||||
|
@ -41,6 +40,7 @@ exports.strategyFactory = async function (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verify = buildVerifyFn(saveUserFn)
|
||||||
return new GoogleStrategy(
|
return new GoogleStrategy(
|
||||||
{
|
{
|
||||||
clientID: config.clientID,
|
clientID: config.clientID,
|
||||||
|
@ -58,4 +58,4 @@ exports.strategyFactory = async function (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.authenticate = authenticate
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -2,46 +2,49 @@ const fetch = require("node-fetch")
|
||||||
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
|
||||||
/**
|
const buildVerifyFn = saveUserFn => {
|
||||||
* @param {*} issuer The identity provider base URL
|
/**
|
||||||
* @param {*} sub The user ID
|
* @param {*} issuer The identity provider base URL
|
||||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
* @param {*} sub The user ID
|
||||||
* @param {*} jwtClaims The parsed id_token claims
|
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
* @param {*} jwtClaims The parsed id_token claims
|
||||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||||
* @param {*} idToken The id_token - always a JWT
|
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||||
* @param {*} params The response body from requesting an access_token
|
* @param {*} idToken The id_token - always a JWT
|
||||||
* @param {*} done The passport callback: err, user, info
|
* @param {*} params The response body from requesting an access_token
|
||||||
*/
|
* @param {*} done The passport callback: err, user, info
|
||||||
async function authenticate(
|
*/
|
||||||
issuer,
|
return async (
|
||||||
sub,
|
issuer,
|
||||||
profile,
|
sub,
|
||||||
jwtClaims,
|
profile,
|
||||||
accessToken,
|
jwtClaims,
|
||||||
refreshToken,
|
accessToken,
|
||||||
idToken,
|
refreshToken,
|
||||||
params,
|
idToken,
|
||||||
done
|
params,
|
||||||
) {
|
|
||||||
const thirdPartyUser = {
|
|
||||||
// store the issuer info to enable sync in future
|
|
||||||
provider: issuer,
|
|
||||||
providerType: "oidc",
|
|
||||||
userId: profile.id,
|
|
||||||
profile: profile,
|
|
||||||
email: getEmail(profile, jwtClaims),
|
|
||||||
oauth2: {
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticateThirdParty(
|
|
||||||
thirdPartyUser,
|
|
||||||
false, // don't require local accounts to exist
|
|
||||||
done
|
done
|
||||||
)
|
) => {
|
||||||
|
const thirdPartyUser = {
|
||||||
|
// store the issuer info to enable sync in future
|
||||||
|
provider: issuer,
|
||||||
|
providerType: "oidc",
|
||||||
|
userId: profile.id,
|
||||||
|
profile: profile,
|
||||||
|
email: getEmail(profile, jwtClaims),
|
||||||
|
oauth2: {
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticateThirdParty(
|
||||||
|
thirdPartyUser,
|
||||||
|
false, // don't require local accounts to exist
|
||||||
|
done,
|
||||||
|
saveUserFn
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,7 +89,7 @@ function validEmail(value) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport OIDC Strategy
|
* @returns Dynamically configured Passport OIDC Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (config, callbackUrl) {
|
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret, configUrl } = config
|
const { clientID, clientSecret, configUrl } = config
|
||||||
|
|
||||||
|
@ -106,6 +109,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
|
|
||||||
const body = await response.json()
|
const body = await response.json()
|
||||||
|
|
||||||
|
const verify = buildVerifyFn(saveUserFn)
|
||||||
return new OIDCStrategy(
|
return new OIDCStrategy(
|
||||||
{
|
{
|
||||||
issuer: body.issuer,
|
issuer: body.issuer,
|
||||||
|
@ -116,7 +120,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
callbackURL: callbackUrl,
|
callbackURL: callbackUrl,
|
||||||
},
|
},
|
||||||
authenticate
|
verify
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@ -125,4 +129,4 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.authenticate = authenticate
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -58,8 +58,10 @@ describe("google", () => {
|
||||||
|
|
||||||
it("delegates authentication to third party common", async () => {
|
it("delegates authentication to third party common", async () => {
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
|
const mockSaveUserFn = jest.fn()
|
||||||
|
const authenticate = await google.buildVerifyFn(mockSaveUserFn)
|
||||||
|
|
||||||
await google.authenticate(
|
await authenticate(
|
||||||
data.accessToken,
|
data.accessToken,
|
||||||
data.refreshToken,
|
data.refreshToken,
|
||||||
profile,
|
profile,
|
||||||
|
@ -69,7 +71,8 @@ describe("google", () => {
|
||||||
expect(authenticateThirdParty).toHaveBeenCalledWith(
|
expect(authenticateThirdParty).toHaveBeenCalledWith(
|
||||||
user,
|
user,
|
||||||
true,
|
true,
|
||||||
mockDone)
|
mockDone,
|
||||||
|
mockSaveUserFn)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -83,8 +83,10 @@ describe("oidc", () => {
|
||||||
|
|
||||||
async function doAuthenticate() {
|
async function doAuthenticate() {
|
||||||
const oidc = require("../oidc")
|
const oidc = require("../oidc")
|
||||||
|
const mockSaveUserFn = jest.fn()
|
||||||
|
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
|
||||||
|
|
||||||
await oidc.authenticate(
|
await authenticate(
|
||||||
issuer,
|
issuer,
|
||||||
sub,
|
sub,
|
||||||
profile,
|
profile,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { generateGlobalUserID } = require("../../db/utils")
|
const { generateGlobalUserID } = require("../../db/utils")
|
||||||
const { saveUser } = require("../../utils")
|
|
||||||
const { authError } = require("./utils")
|
const { authError } = require("./utils")
|
||||||
const { newid } = require("../../hashing")
|
const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
|
@ -16,8 +15,11 @@ exports.authenticateThirdParty = async function (
|
||||||
thirdPartyUser,
|
thirdPartyUser,
|
||||||
requireLocalAccount = true,
|
requireLocalAccount = true,
|
||||||
done,
|
done,
|
||||||
saveUserFn = saveUser
|
saveUserFn
|
||||||
) {
|
) {
|
||||||
|
if (!saveUserFn) {
|
||||||
|
throw new Error("Save user function must be provided")
|
||||||
|
}
|
||||||
if (!thirdPartyUser.provider) {
|
if (!thirdPartyUser.provider) {
|
||||||
return authError(done, "third party user provider required")
|
return authError(done, "third party user provider required")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ exports.Databases = {
|
||||||
FLAGS: "flags",
|
FLAGS: "flags",
|
||||||
APP_METADATA: "appMetadata",
|
APP_METADATA: "appMetadata",
|
||||||
QUERY_VARS: "queryVars",
|
QUERY_VARS: "queryVars",
|
||||||
|
LICENSES: "license",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
|
@ -176,6 +176,13 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getBuildersCount = async () => {
|
||||||
|
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||||
|
include_docs: false,
|
||||||
|
})
|
||||||
|
return builders ? builders.length : 0
|
||||||
|
}
|
||||||
|
|
||||||
exports.saveUser = async (
|
exports.saveUser = async (
|
||||||
user,
|
user,
|
||||||
tenantId,
|
tenantId,
|
||||||
|
@ -289,4 +296,5 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
userId,
|
userId,
|
||||||
sessions.map(({ sessionId }) => sessionId)
|
sessions.map(({ sessionId }) => sessionId)
|
||||||
)
|
)
|
||||||
|
await userCache.invalidateUser(userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.105-alpha.20",
|
"@budibase/string-templates": "^1.0.105-alpha.35",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -80,8 +80,4 @@
|
||||||
.active svg {
|
.active svg {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-ActionButton-label {
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
<div use:getAnchor on:click={openMenu}>
|
<div use:getAnchor on:click={openMenu}>
|
||||||
<slot name="control" />
|
<slot name="control" />
|
||||||
</div>
|
</div>
|
||||||
<Popover bind:this={dropdown} {anchor} {align} {portalTarget}>
|
<Popover bind:this={dropdown} {anchor} {align} {portalTarget} {dataCy}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<slot />
|
<slot />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -25,9 +25,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
class="icon"
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:focus={() => (showTooltip = true)}
|
on:focus={() => (showTooltip = true)}
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:click={() => (showTooltip = false)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
on:click
|
on:click
|
||||||
|
@ -45,13 +47,13 @@
|
||||||
</svg>
|
</svg>
|
||||||
{#if tooltip && showTooltip}
|
{#if tooltip && showTooltip}
|
||||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
.icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
@ -75,8 +77,10 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 100%;
|
top: calc(100% + 4px);
|
||||||
white-space: nowrap;
|
width: 100vw;
|
||||||
|
max-width: 150px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
export let maxWidth = "80ch"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class:wide>
|
<div style="--max-width: {maxWidth}" class:wide>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
max-width: 80ch;
|
max-width: var(--max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: calc(var(--spacing-xl) * 2);
|
padding: calc(var(--spacing-xl) * 2);
|
||||||
min-height: calc(100% - var(--spacing-xl) * 4);
|
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
export let secondaryButtonText = undefined
|
export let secondaryButtonText = undefined
|
||||||
export let secondaryAction = undefined
|
export let secondaryAction = undefined
|
||||||
export let secondaryButtonWarning = false
|
export let secondaryButtonWarning = false
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -63,21 +64,24 @@
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
<div class="spectrum-Dialog-grid">
|
<div class="spectrum-Dialog-grid">
|
||||||
{#if title}
|
<h1
|
||||||
<h1
|
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||||
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
class:noDivider={!showDivider}
|
||||||
class:noDivider={!showDivider}
|
class:header-spacing={$$slots.header}
|
||||||
class:header-spacing={$$slots.header}
|
>
|
||||||
>
|
{#if title}
|
||||||
{title}
|
{title}
|
||||||
|
{:else if $$slots.header}
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</h1>
|
|
||||||
{#if showDivider}
|
|
||||||
<Divider size="M" />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</h1>
|
||||||
|
{#if showDivider && (title || $$slots.header)}
|
||||||
|
<Divider size="M" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
||||||
<section class="spectrum-Dialog-content content-grid">
|
<section class="spectrum-Dialog-content content-grid">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
export let anchor
|
export let anchor
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
|
let clazz
|
||||||
|
export { clazz as class }
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
dispatch("open")
|
dispatch("open")
|
||||||
|
@ -37,8 +41,9 @@
|
||||||
use:positionDropdown={{ anchor, align }}
|
use:positionDropdown={{ anchor, align }}
|
||||||
use:clickOutside={hide}
|
use:clickOutside={hide}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class={"spectrum-Popover is-open " + (clazz || "")}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import Popover from "../Popover/Popover.svelte"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
export let align = "left"
|
||||||
|
|
||||||
|
export let anchor
|
||||||
|
export let showTip = true
|
||||||
|
export let direction = "bottom"
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
|
let dropdown
|
||||||
|
let tipSvg =
|
||||||
|
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
||||||
|
|
||||||
|
// This is needed because display: contents is considered "invisible".
|
||||||
|
// It should only ever be an action button, so should be fine.
|
||||||
|
function getAnchor(node) {
|
||||||
|
if (!anchor) {
|
||||||
|
anchor = node.firstChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//need this for the publish/view behaviours
|
||||||
|
export const hide = () => {
|
||||||
|
dropdown.hide()
|
||||||
|
}
|
||||||
|
export const show = () => {
|
||||||
|
dropdown.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMenu = event => {
|
||||||
|
if (!disabled) {
|
||||||
|
event.stopPropagation()
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("popoverMenu", { show, hide })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popover-menu">
|
||||||
|
<div use:getAnchor on:click={openMenu}>
|
||||||
|
<slot name="control" />
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={dropdown}
|
||||||
|
{anchor}
|
||||||
|
{align}
|
||||||
|
class={showTip
|
||||||
|
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
{#if showTip}
|
||||||
|
{@html tipSvg}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="popover-container" data-cy={dataCy}>
|
||||||
|
<div class="popover-menu-wrap">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.spectrum-Popover.is-open.spectrum-Popover--withTip) {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.popover-menu-wrap {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.popover-menu :global(.icon) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
:global(.spectrum-Popover--bottom .spectrum-Popover-tip) {
|
||||||
|
left: 90%;
|
||||||
|
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,11 +16,11 @@
|
||||||
easing: easing,
|
easing: easing,
|
||||||
})
|
})
|
||||||
|
|
||||||
$: if (value) $progress = value
|
$: if (value || value === 0) $progress = value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class:spectrum-ProgressBar--indeterminate={!value}
|
class:spectrum-ProgressBar--indeterminate={!value && value !== 0}
|
||||||
class:spectrum-ProgressBar--sideLabel={sideLabel}
|
class:spectrum-ProgressBar--sideLabel={sideLabel}
|
||||||
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
|
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
|
||||||
value={$progress}
|
value={$progress}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
aria-valuenow={$progress}
|
aria-valuenow={$progress}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
style={width ? `width: ${width}px;` : ""}
|
style={width ? `width: ${width};` : ""}
|
||||||
>
|
>
|
||||||
{#if $$slots}
|
{#if $$slots}
|
||||||
<div
|
<div
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value}
|
{#if value || value === 0}
|
||||||
<div
|
<div
|
||||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||||
>
|
>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<div class="spectrum-ProgressBar-track">
|
<div class="spectrum-ProgressBar-track">
|
||||||
<div
|
<div
|
||||||
class="spectrum-ProgressBar-fill"
|
class="spectrum-ProgressBar-fill"
|
||||||
style={value ? `width: ${$progress}%` : ""}
|
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let autoSortColumns = true
|
export let autoSortColumns = true
|
||||||
export let compact = false
|
export let compact = false
|
||||||
|
export let customPlaceholder = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -387,13 +388,24 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
|
<div
|
||||||
<div class="placeholder-content">
|
class="placeholder"
|
||||||
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
|
class:placeholder--custom={customPlaceholder}
|
||||||
<use xlink:href="#spectrum-icon-18-Table" />
|
class:placeholder--no-fields={!fields?.length}
|
||||||
</svg>
|
>
|
||||||
<div>No rows found</div>
|
{#if customPlaceholder}
|
||||||
</div>
|
<slot name="placeholder" />
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder-content">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Table" />
|
||||||
|
</svg>
|
||||||
|
<div>No rows found</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -458,6 +470,13 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
border-top: var(--table-border);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell:first-of-type {
|
||||||
|
border-left: var(--table-border);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell:last-of-type {
|
||||||
|
border-right: var(--table-border);
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell--alignCenter {
|
.spectrum-Table-headCell--alignCenter {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -576,16 +595,19 @@
|
||||||
border-top: none;
|
border-top: none;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
background-color: var(--table-bg);
|
background-color: var(--table-bg);
|
||||||
|
padding: 40px;
|
||||||
}
|
}
|
||||||
.placeholder--no-fields {
|
.placeholder--no-fields {
|
||||||
border-top: var(--table-border);
|
border-top: var(--table-border);
|
||||||
}
|
}
|
||||||
|
.placeholder--custom {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
.wrapper--quiet .placeholder {
|
.wrapper--quiet .placeholder {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
.placeholder-content {
|
.placeholder-content {
|
||||||
padding: 40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
export let serif = false
|
export let serif = false
|
||||||
export let weight = null
|
export let weight = null
|
||||||
export let textAlign = null
|
export let textAlign = null
|
||||||
|
export let color = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
style={`
|
style={`
|
||||||
${weight ? `font-weight:${weight};` : ""}
|
${weight ? `font-weight:${weight};` : ""}
|
||||||
${textAlign ? `text-align:${textAlign};` : ""}
|
${textAlign ? `text-align:${textAlign};` : ""}
|
||||||
|
${color ? `color:${color};` : ""}
|
||||||
`}
|
`}
|
||||||
class="spectrum-Body spectrum-Body--size{size}"
|
class="spectrum-Body spectrum-Body--size{size}"
|
||||||
class:spectrum-Body--serif={serif}
|
class:spectrum-Body--serif={serif}
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let textAlign
|
export let textAlign
|
||||||
export let noPadding = false
|
export let noPadding = false
|
||||||
|
export let weight = "default" // light, heavy, default
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
style={textAlign ? `text-align:${textAlign}` : ``}
|
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||||
class:noPadding
|
class:noPadding
|
||||||
class="spectrum-Heading spectrum-Heading--size{size}"
|
class="spectrum-Heading spectrum-Heading--size{size} spectrum-Heading--{weight}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -25,6 +25,7 @@ export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
|
export { default as PopoverMenu } from "./Popover/PopoverMenu.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
||||||
export { default as Label } from "./Label/Label.svelte"
|
export { default as Label } from "./Label/Label.svelte"
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Publish Application Workflow", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the unpublished status correctly", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Unpublished")
|
||||||
|
cy.get("svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Preview")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".app-status-icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".app-status-icon svg[aria-label='Globe']").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should publish an application and correctly reflect that", () => {
|
||||||
|
//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(".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/cypress-tests'
|
||||||
|
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".app-status-icon svg[aria-label='Globe']").should("exist").click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get("[data-cy='publish-popover-action']").should("exist")
|
||||||
|
cy.get("button").contains("View App").should("exist")
|
||||||
|
cy.get(".publish-popover-message").should("have.text", "Last Published: a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should unpublish an application from the top navigation and reflect the status change", () => {
|
||||||
|
//Assuming the previous test app exists and is published
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
//The published status
|
||||||
|
cy.get(".app-status-icon svg[aria-label='Globe']").should("exist")
|
||||||
|
.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.get(".app-status-icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0).contains("Unpublished")
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -11,7 +11,7 @@ filterTests(['all'], () => {
|
||||||
cy.applicationInAppTable("Cypress Tests")
|
cy.applicationInAppTable("Cypress Tests")
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[data-cy='app-row-actions-menu']").eq(0).click()
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
||||||
// Select random icon
|
// Select random icon
|
||||||
|
@ -38,6 +38,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".title").children().children()
|
cy.get(".title").children().children()
|
||||||
.should('have.attr', 'style').and('contains', 'color')
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
})
|
})
|
||||||
|
cy.deleteAllApps()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,7 +20,6 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup trigger
|
// Setup trigger
|
||||||
cy.contains("Setup").click()
|
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").click()
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
|
@ -32,12 +31,11 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.contains("Create Row").trigger('mouseover').click().click()
|
cy.contains("Create Row").trigger('mouseover').click().click()
|
||||||
cy.get(".spectrum-Button--cta").click()
|
cy.get(".spectrum-Button--cta").click()
|
||||||
})
|
})
|
||||||
cy.contains("Setup").click()
|
|
||||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.first()
|
.first()
|
||||||
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.type("11")
|
.type("11")
|
||||||
|
|
|
@ -99,30 +99,32 @@ filterTests(['all'], () => {
|
||||||
cy.searchForApplication(originalName)
|
cy.searchForApplication(originalName)
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[data-cy='app-row-actions-menu']").eq(0).click()
|
||||||
})
|
})
|
||||||
// Check for when an app is published
|
// Check for when an app is published
|
||||||
if (published == true) {
|
if (published == true) {
|
||||||
// Should not have Edit as option, will unpublish app
|
// Should not have Edit as option, will unpublish app
|
||||||
cy.should("not.have.value", "Edit")
|
cy.should("not.have.value", "Edit")
|
||||||
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()
|
||||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||||
}
|
}
|
||||||
cy.contains("Edit").click()
|
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
.within(() => {
|
})
|
||||||
if (noName == true) {
|
cy.get(".spectrum-Modal")
|
||||||
cy.get("input").clear()
|
.within(() => {
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
if (noName == true) {
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
|
||||||
return cy
|
|
||||||
}
|
|
||||||
cy.get("input").clear()
|
cy.get("input").clear()
|
||||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
cy.wait(500)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should try to revert an unpublished app", () => {
|
it("should try to revert an unpublished app", () => {
|
||||||
// Click revert icon
|
// Click revert icon
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[data-cy='revert-application-topnav']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
// Enter app name before revert
|
// Enter app name before revert
|
||||||
|
@ -33,11 +33,15 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-ButtonGroup").within(() => {
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
})
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
|
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
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[data-cy='revert-application-topnav']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
|
@ -54,7 +58,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should enter incorrect app name when reverting", () => {
|
it("should enter incorrect app name when reverting", () => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
cy.get("[data-cy='revert-application-topnav']").click({ force: true })
|
||||||
})
|
})
|
||||||
// Enter incorrect app name
|
// Enter incorrect app name
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.105-alpha.20",
|
"@budibase/bbui": "^1.0.105-alpha.35",
|
||||||
"@budibase/client": "^1.0.105-alpha.20",
|
"@budibase/client": "^1.0.105-alpha.35",
|
||||||
"@budibase/frontend-core": "^1.0.105-alpha.20",
|
"@budibase/frontend-core": "^1.0.105-alpha.35",
|
||||||
"@budibase/string-templates": "^1.0.105-alpha.20",
|
"@budibase/string-templates": "^1.0.105-alpha.35",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const Events = {
|
||||||
CREATED: "budibase:app_created",
|
CREATED: "budibase:app_created",
|
||||||
PUBLISHED: "budibase:app_published",
|
PUBLISHED: "budibase:app_published",
|
||||||
UNPUBLISHED: "budibase:app_unpublished",
|
UNPUBLISHED: "budibase:app_unpublished",
|
||||||
|
VIEW_PUBLISHED: "budibase:view_published_app",
|
||||||
},
|
},
|
||||||
ANALYTICS: {
|
ANALYTICS: {
|
||||||
OPT_IN: "budibase:analytics_opt_in",
|
OPT_IN: "budibase:analytics_opt_in",
|
||||||
|
@ -50,3 +51,9 @@ export const Events = {
|
||||||
SAVED: "budibase:sso_saved",
|
SAVED: "budibase:sso_saved",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EventSource = {
|
||||||
|
PORTAL: "portal",
|
||||||
|
URL: "url",
|
||||||
|
NOTIFICATION: "notification",
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { API } from "api"
|
||||||
import PosthogClient from "./PosthogClient"
|
import PosthogClient from "./PosthogClient"
|
||||||
import IntercomClient from "./IntercomClient"
|
import IntercomClient from "./IntercomClient"
|
||||||
import SentryClient from "./SentryClient"
|
import SentryClient from "./SentryClient"
|
||||||
import { Events } from "./constants"
|
import { Events, EventSource } from "./constants"
|
||||||
|
|
||||||
const posthog = new PosthogClient(
|
const posthog = new PosthogClient(
|
||||||
process.env.POSTHOG_TOKEN,
|
process.env.POSTHOG_TOKEN,
|
||||||
|
@ -57,5 +57,5 @@ class AnalyticsHub {
|
||||||
|
|
||||||
const analytics = new AnalyticsHub()
|
const analytics = new AnalyticsHub()
|
||||||
|
|
||||||
export { Events }
|
export { Events, EventSource }
|
||||||
export default analytics
|
export default analytics
|
||||||
|
|
|
@ -654,7 +654,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
* Builds a form schema given a form component.
|
* Builds a form schema given a form component.
|
||||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||||
*/
|
*/
|
||||||
const buildFormSchema = component => {
|
export const buildFormSchema = component => {
|
||||||
let schema = {}
|
let schema = {}
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return schema
|
return schema
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
if (v.internal) {
|
if (v.internal) {
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
}
|
}
|
||||||
|
delete acc.LOOP
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,9 @@
|
||||||
animate:flip={{ duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fly|local={{ x: 500, duration: 1500 }}
|
in:fly|local={{ x: 500, duration: 1500 }}
|
||||||
>
|
>
|
||||||
<FlowItem {testDataModal} {block} />
|
{#if block.stepId !== "LOOP"}
|
||||||
|
<FlowItem {testDataModal} {block} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
ActionButton,
|
|
||||||
Select,
|
Select,
|
||||||
|
ActionButton,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
|
@ -25,8 +25,8 @@
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let actionModal
|
let actionModal
|
||||||
let resultsModal
|
let resultsModal
|
||||||
let setupToggled
|
|
||||||
let blockComplete
|
let blockComplete
|
||||||
|
let showLooping = false
|
||||||
|
|
||||||
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
||||||
$: showBindingPicker =
|
$: showBindingPicker =
|
||||||
|
@ -52,8 +52,21 @@
|
||||||
block.schema?.inputs?.properties || {}
|
block.schema?.inputs?.properties || {}
|
||||||
).every(x => block?.inputs[x])
|
).every(x => block?.inputs[x])
|
||||||
|
|
||||||
|
$: loopingSelected =
|
||||||
|
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)
|
||||||
|
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
|
let loopBlock =
|
||||||
|
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (loopBlock) {
|
||||||
|
automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
|
}
|
||||||
automationStore.actions.deleteAutomationBlock(block)
|
automationStore.actions.deleteAutomationBlock(block)
|
||||||
await automationStore.actions.save(
|
await automationStore.actions.save(
|
||||||
$automationStore.selectedAutomation?.automation
|
$automationStore.selectedAutomation?.automation
|
||||||
|
@ -76,6 +89,23 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addLooping() {
|
||||||
|
loopingSelected = true
|
||||||
|
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||||
|
|
||||||
|
const loopBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
|
"ACTION",
|
||||||
|
"LOOP",
|
||||||
|
loopDefinition
|
||||||
|
)
|
||||||
|
loopBlock.blockToLoop = block.id
|
||||||
|
block.loopBlock = loopBlock.id
|
||||||
|
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function onSelect(block) {
|
async function onSelect(block) {
|
||||||
await automationStore.update(state => {
|
await automationStore.update(state => {
|
||||||
state.selectedBlock = block
|
state.selectedBlock = block
|
||||||
|
@ -84,13 +114,68 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||||
class={`block ${block.type} hoverable`}
|
{#if loopingSelected}
|
||||||
class:selected
|
<div class="blockSection">
|
||||||
on:click={() => {
|
<div
|
||||||
onSelect(block)
|
on:click={() => {
|
||||||
}}
|
showLooping = !showLooping
|
||||||
>
|
}}
|
||||||
|
class="splitHeader"
|
||||||
|
>
|
||||||
|
<div class="center-items">
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:grey;"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
</svg>
|
||||||
|
<div class="iconAlign">
|
||||||
|
<Detail size="S">Looping</Detail>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blockTitle">
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if !showLooping}
|
||||||
|
<div class="blockSection">
|
||||||
|
<div class="block-options">
|
||||||
|
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||||
|
<Icon name="DeleteOutline" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<AutomationBlockSetup
|
||||||
|
schemaProperties={Object.entries(
|
||||||
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
|
.properties
|
||||||
|
)}
|
||||||
|
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)}
|
||||||
|
{webhookModal}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -127,65 +212,66 @@
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if testResult && testResult[0]}
|
<div class="blockTitle">
|
||||||
<span on:click={() => resultsModal.show()}>
|
{#if testResult && testResult[0]}
|
||||||
<StatusLight
|
<div style="float: right;" on:click={() => resultsModal.show()}>
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
<StatusLight
|
||||||
negative={!testResult[0].outputs?.success}
|
positive={isTrigger || testResult[0].outputs?.success}
|
||||||
><Body size="XS">View response</Body></StatusLight
|
negative={!testResult[0].outputs?.success}
|
||||||
>
|
><Body size="XS">View response</Body></StatusLight
|
||||||
</span>
|
>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if !blockComplete}
|
{#if !blockComplete}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="splitHeader">
|
{#if !isTrigger}
|
||||||
<ActionButton
|
<div>
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
setupToggled = !setupToggled
|
|
||||||
}}
|
|
||||||
quiet
|
|
||||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Setup</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if !isTrigger}
|
|
||||||
<div class="block-options">
|
<div class="block-options">
|
||||||
{#if showBindingPicker}
|
{#if !loopingSelected}
|
||||||
<div>
|
<ActionButton on:click={() => addLooping()} icon="Reuse"
|
||||||
<Select
|
>Add Looping</ActionButton
|
||||||
on:change={toggleFieldControl}
|
>
|
||||||
quiet
|
|
||||||
defaultValue="Use values"
|
|
||||||
autoWidth
|
|
||||||
value={rowControl ? "Use bindings" : "Use values"}
|
|
||||||
options={["Use values", "Use bindings"]}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
{#if showBindingPicker}
|
||||||
<Icon name="DeleteOutline" />
|
<Select
|
||||||
</div>
|
on:change={toggleFieldControl}
|
||||||
|
defaultValue="Use values"
|
||||||
|
autoWidth
|
||||||
|
value={rowControl ? "Use bindings" : "Use values"}
|
||||||
|
options={["Use values", "Use bindings"]}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<ActionButton
|
||||||
|
on:click={() => deleteStep()}
|
||||||
|
icon="DeleteOutline"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
{#if setupToggled}
|
<AutomationBlockSetup
|
||||||
<AutomationBlockSetup
|
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
{block}
|
||||||
{block}
|
{webhookModal}
|
||||||
{webhookModal}
|
/>
|
||||||
/>
|
{#if lastStep}
|
||||||
{#if lastStep}
|
<Button on:click={() => testDataModal.show()} cta
|
||||||
<Button on:click={() => testDataModal.show()} cta
|
>Finish and test automation</Button
|
||||||
>Finish and test automation</Button
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -220,8 +306,10 @@
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
.block-options {
|
.block-options {
|
||||||
display: flex;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.center-items {
|
.center-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -256,4 +344,9 @@
|
||||||
/* center horizontally */
|
/* center horizontally */
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blockTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui"
|
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
|
||||||
|
|
||||||
export let testResult
|
export let testResult
|
||||||
export let isTrigger
|
export let isTrigger
|
||||||
|
@ -10,11 +10,11 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
title="Test Automation"
|
|
||||||
cancelText="Close"
|
cancelText="Close"
|
||||||
>
|
>
|
||||||
<div slot="header">
|
<div slot="header" class="result-modal-header">
|
||||||
<div style="float: right;">
|
<span>Test Results</span>
|
||||||
|
<div>
|
||||||
{#if isTrigger || testResult[0].outputs.success}
|
{#if isTrigger || testResult[0].outputs.success}
|
||||||
<div class="iconSuccess">
|
<div class="iconSuccess">
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
<Icon size="S" name="CheckmarkCircle" />
|
||||||
|
@ -26,7 +26,18 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span>
|
||||||
|
{#if testResult[0].outputs.iterations}
|
||||||
|
<div style="display: flex;">
|
||||||
|
<Icon name="Reuse" />
|
||||||
|
<div style="margin-left: 10px;">
|
||||||
|
<Label>
|
||||||
|
This loop ran {testResult[0].outputs.iterations} times.</Label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
inputToggled = !inputToggled
|
inputToggled = !inputToggled
|
||||||
|
@ -89,6 +100,14 @@
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.result-modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.iconSuccess {
|
.iconSuccess {
|
||||||
color: var(--spectrum-global-color-green-600);
|
color: var(--spectrum-global-color-green-600);
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,36 +88,65 @@
|
||||||
if (!block || !automation) {
|
if (!block || !automation) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find previous steps to the selected one
|
// Find previous steps to the selected one
|
||||||
let allSteps = [...automation.steps]
|
let allSteps = [...automation.steps]
|
||||||
|
|
||||||
if (automation.trigger) {
|
if (automation.trigger) {
|
||||||
allSteps = [automation.trigger, ...allSteps]
|
allSteps = [automation.trigger, ...allSteps]
|
||||||
}
|
}
|
||||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||||
|
|
||||||
// Extract all outputs from all previous steps as available bindings
|
// Extract all outputs from all previous steps as available bindins
|
||||||
let bindings = []
|
let bindings = []
|
||||||
for (let idx = 0; idx < blockIdx; idx++) {
|
for (let idx = 0; idx < blockIdx; idx++) {
|
||||||
const outputs = Object.entries(
|
let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
|
||||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
let isLoopBlock =
|
||||||
)
|
allSteps[idx]?.stepId === "LOOP" &&
|
||||||
|
allSteps.find(x => x.blockToLoop === block.id)
|
||||||
|
|
||||||
|
// If the previous block was a loop block, decerement the index so the following
|
||||||
|
// steps are in the correct order
|
||||||
|
if (wasLoopBlock) {
|
||||||
|
blockIdx--
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
|
||||||
|
|
||||||
|
// If its a Loop Block, we need to add this custom schema
|
||||||
|
if (isLoopBlock) {
|
||||||
|
schema = {
|
||||||
|
currentItem: {
|
||||||
|
type: "string",
|
||||||
|
description: "the item currently being executed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputs = Object.entries(schema)
|
||||||
|
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => {
|
outputs.map(([name, value]) => {
|
||||||
const stepsLabel = block.name.startsWith("JS")
|
let runtimeName = isLoopBlock
|
||||||
|
? `loop.${name}`
|
||||||
|
: block.name.startsWith("JS")
|
||||||
? `steps[${idx}].${name}`
|
? `steps[${idx}].${name}`
|
||||||
: `steps.${idx}.${name}`
|
: `steps.${idx}.${name}`
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : stepsLabel
|
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
return {
|
return {
|
||||||
label: runtime,
|
label: runtime,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
category:
|
||||||
|
idx === 0
|
||||||
|
? "Trigger outputs"
|
||||||
|
: isLoopBlock
|
||||||
|
? "Loop Outputs"
|
||||||
|
: `Step ${idx} outputs`,
|
||||||
path: runtime,
|
path: runtime,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +293,14 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
|
{:else if value.customType === "loopOption"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
autoWidth
|
||||||
|
value={inputData[key]}
|
||||||
|
options={["Array", "String"]}
|
||||||
|
defaultValue={"Array"}
|
||||||
|
/>
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import { Pagination } from "@budibase/bbui"
|
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@
|
||||||
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||||
$: id = $tables.selected?._id
|
$: id = $tables.selected?._id
|
||||||
$: fetch = createFetch(id)
|
$: fetch = createFetch(id)
|
||||||
|
$: hasCols = checkHasCols(schema)
|
||||||
|
$: hasRows = !!$fetch.rows?.length
|
||||||
|
|
||||||
const enrichSchema = schema => {
|
const enrichSchema = schema => {
|
||||||
let tempSchema = { ...schema }
|
let tempSchema = { ...schema }
|
||||||
|
@ -47,6 +49,20 @@
|
||||||
|
|
||||||
return tempSchema
|
return tempSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkHasCols = schema => {
|
||||||
|
if (!schema || Object.keys(schema).length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fields = Object.values(schema)
|
||||||
|
for (let field of fields) {
|
||||||
|
if (!field.autocolumn) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches new data whenever the table changes
|
// Fetches new data whenever the table changes
|
||||||
const createFetch = tableId => {
|
const createFetch = tableId => {
|
||||||
return fetchData({
|
return fetchData({
|
||||||
|
@ -104,40 +120,73 @@
|
||||||
disableSorting
|
disableSorting
|
||||||
on:updatecolumns={onUpdateColumns}
|
on:updatecolumns={onUpdateColumns}
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
|
customPlaceholder
|
||||||
>
|
>
|
||||||
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
<div class="buttons">
|
||||||
{#if schema && Object.keys(schema).length > 0}
|
<div class="left-buttons">
|
||||||
{#if !isUsersTable}
|
<CreateColumnButton
|
||||||
<CreateRowButton
|
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||||
on:updaterows={onUpdateRows}
|
|
||||||
title={"Create row"}
|
|
||||||
modalContentComponent={CreateEditRow}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if isInternal}
|
|
||||||
<CreateViewButton />
|
|
||||||
{/if}
|
|
||||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
|
||||||
{#if isUsersTable}
|
|
||||||
<EditRolesButton />
|
|
||||||
{/if}
|
|
||||||
{#if !isInternal}
|
|
||||||
<ExistingRelationshipButton
|
|
||||||
table={$tables.selected}
|
|
||||||
on:updatecolumns={onUpdateColumns}
|
on:updatecolumns={onUpdateColumns}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{#if !isUsersTable}
|
||||||
<HideAutocolumnButton bind:hideAutocolumns />
|
<CreateRowButton
|
||||||
<!-- always have the export last -->
|
on:updaterows={onUpdateRows}
|
||||||
<ExportButton view={$tables.selected?._id} />
|
title={"Create row"}
|
||||||
<ImportButton
|
modalContentComponent={CreateEditRow}
|
||||||
tableId={$tables.selected?._id}
|
disabled={!hasCols}
|
||||||
on:updaterows={onUpdateRows}
|
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||||
/>
|
/>
|
||||||
{#key id}
|
{/if}
|
||||||
<TableFilterButton {schema} on:change={onFilter} />
|
{#if isInternal}
|
||||||
{/key}
|
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="right-buttons">
|
||||||
|
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||||
|
{#if isUsersTable}
|
||||||
|
<EditRolesButton />
|
||||||
|
{/if}
|
||||||
|
{#if !isInternal}
|
||||||
|
<ExistingRelationshipButton
|
||||||
|
table={$tables.selected}
|
||||||
|
on:updatecolumns={onUpdateColumns}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
|
<ImportButton
|
||||||
|
tableId={$tables.selected?._id}
|
||||||
|
on:updaterows={onUpdateRows}
|
||||||
|
/>
|
||||||
|
<ExportButton
|
||||||
|
disabled={!hasRows || !hasCols}
|
||||||
|
view={$tables.selected?._id}
|
||||||
|
/>
|
||||||
|
{#key id}
|
||||||
|
<TableFilterButton
|
||||||
|
{schema}
|
||||||
|
on:change={onFilter}
|
||||||
|
disabled={!hasCols || !hasRows}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="placeholder">
|
||||||
|
<Layout gap="S">
|
||||||
|
{#if !hasCols}
|
||||||
|
<Heading>Let's create some columns</Heading>
|
||||||
|
<Body>
|
||||||
|
Start building out your table structure<br />
|
||||||
|
by adding some columns
|
||||||
|
</Body>
|
||||||
|
{:else}
|
||||||
|
<Heading>Now let's add a row</Heading>
|
||||||
|
<Body>
|
||||||
|
Add some data to your table<br />
|
||||||
|
by adding some rows
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
</Table>
|
</Table>
|
||||||
{#key id}
|
{#key id}
|
||||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
|
@ -162,4 +211,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: var(--spacing-xl);
|
margin-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.buttons {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.left-buttons,
|
||||||
|
.right-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
$: label = meta.name ? capitalise(meta.name) : ""
|
$: label = meta.name ? capitalise(meta.name) : ""
|
||||||
|
|
||||||
const timeStamp = resolveTimeStamp(value)
|
const timeStamp = resolveTimeStamp(value)
|
||||||
const isTimeStamp = timeStamp ? true : false
|
const isTimeStamp = !!timeStamp
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === "options"}
|
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||||
<Select
|
<Select
|
||||||
{label}
|
{label}
|
||||||
data-cy="{meta.name}-select"
|
data-cy="{meta.name}-select"
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<Dropzone {label} bind:value />
|
<Dropzone {label} bind:value />
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
||||||
{:else if type === "array"}
|
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||||
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let type
|
export let type
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
|
export let customPlaceholder = false
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let editableColumn
|
let editableColumn
|
||||||
|
@ -117,10 +118,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div>
|
<Layout noPadding gap="XS">
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="M">{title}</Heading>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div transition:fade|local>
|
<div transition:fade|local>
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
{#key tableId}
|
{#key tableId}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<Table
|
<Table
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
|
{customPlaceholder}
|
||||||
bind:selectedRows
|
bind:selectedRows
|
||||||
allowSelectRows={allowEditing && !isUsersTable}
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
allowEditRows={allowEditing}
|
allowEditRows={allowEditing}
|
||||||
|
@ -153,7 +155,9 @@
|
||||||
on:editrow={e => editRow(e.detail)}
|
on:editrow={e => editRow(e.detail)}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
on:sort
|
on:sort
|
||||||
/>
|
>
|
||||||
|
<slot slot="placeholder" name="placeholder" />
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -176,6 +180,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.table-title > div {
|
.table-title > div {
|
||||||
margin-left: var(--spacing-xs);
|
margin-left: var(--spacing-xs);
|
||||||
|
|
|
@ -2,10 +2,21 @@
|
||||||
import { ActionButton, Modal } from "@budibase/bbui"
|
import { ActionButton, Modal } from "@budibase/bbui"
|
||||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
|
export let highlighted = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="TableColumnAddRight" quiet size="S" on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
selected={highlighted}
|
||||||
|
emphasized={highlighted}
|
||||||
|
icon="TableColumnAddRight"
|
||||||
|
quiet
|
||||||
|
size="S"
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Create column
|
Create column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -4,11 +4,21 @@
|
||||||
|
|
||||||
export let modalContentComponent = CreateEditRow
|
export let modalContentComponent = CreateEditRow
|
||||||
export let title = "Create row"
|
export let title = "Create row"
|
||||||
|
export let disabled = false
|
||||||
|
export let highlighted = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="TableRowAddBottom" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
emphasized={highlighted}
|
||||||
|
selected={highlighted}
|
||||||
|
icon="TableRowAddBottom"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -2,10 +2,18 @@
|
||||||
import { Modal, ActionButton } from "@budibase/bbui"
|
import { Modal, ActionButton } from "@budibase/bbui"
|
||||||
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="CollectionAdd" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="CollectionAdd"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Create view
|
Create view
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -3,11 +3,18 @@
|
||||||
import ExportModal from "../modals/ExportModal.svelte"
|
import ExportModal from "../modals/ExportModal.svelte"
|
||||||
|
|
||||||
export let view
|
export let view
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="DataDownload"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="MagicWand" primary size="S" quiet on:click={hideOrUnhide}>
|
<ActionButton
|
||||||
{#if hideAutocolumns}Show auto columns{:else}Hide auto columns{/if}
|
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
||||||
|
primary
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={hideOrUnhide}
|
||||||
|
>
|
||||||
|
Auto columns
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
icon="Filter"
|
icon="Filter"
|
||||||
size="S"
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
|
{disabled}
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
active={tempValue?.length > 0}
|
active={tempValue?.length > 0}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let onCancel = undefined
|
export let onCancel = undefined
|
||||||
export let warning = true
|
export let warning = true
|
||||||
export let disabled
|
export let disabled
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
{cancelText}
|
{cancelText}
|
||||||
{warning}
|
{warning}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{dataCy}
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{body}
|
{body}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
export let label = null
|
export let label = null
|
||||||
export let value
|
export let value
|
||||||
export let copyValue
|
export let copyValue
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
const copyToClipboard = val => {
|
const copyToClipboard = val => {
|
||||||
const dummy = document.createElement("textarea")
|
const dummy = document.createElement("textarea")
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div data-cy={dataCy}>
|
||||||
<Input readonly {value} {label} />
|
<Input readonly {value} {label} />
|
||||||
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
|
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
|
||||||
<Icon size="S" name="Copy" />
|
<Icon size="S" name="Copy" />
|
||||||
|
|
|
@ -1,24 +1,61 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
ModalContent,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { ProgressCircle } from "@budibase/bbui"
|
||||||
|
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||||
|
|
||||||
let feedbackModal
|
let feedbackModal
|
||||||
let publishModal
|
let publishModal
|
||||||
|
let asyncModal
|
||||||
|
let publishCompleteModal
|
||||||
|
|
||||||
|
let published
|
||||||
|
|
||||||
|
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
|
||||||
|
|
||||||
|
export let onOk
|
||||||
|
|
||||||
async function deployApp() {
|
async function deployApp() {
|
||||||
try {
|
try {
|
||||||
await API.deployAppChanges()
|
//In Progress
|
||||||
|
asyncModal.show()
|
||||||
|
publishModal.hide()
|
||||||
|
|
||||||
|
published = await API.deployAppChanges()
|
||||||
|
|
||||||
analytics.captureEvent(Events.APP.PUBLISHED, {
|
analytics.captureEvent(Events.APP.PUBLISHED, {
|
||||||
appId: $store.appId,
|
appId: $store.appId,
|
||||||
})
|
})
|
||||||
notifications.success("Application published successfully")
|
if (typeof onOk === "function") {
|
||||||
|
await onOk()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Request completed
|
||||||
|
asyncModal.hide()
|
||||||
|
publishCompleteModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
analytics.captureException(error)
|
analytics.captureException(error)
|
||||||
notifications.error("Error publishing app")
|
notifications.error("Error publishing app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
if (published) {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: $store.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
window.open(publishedUrl, "_blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button secondary on:click={publishModal.show}>Publish</Button>
|
<Button secondary on:click={publishModal.show}>Publish</Button>
|
||||||
|
@ -30,11 +67,13 @@
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={publishModal}>
|
<Modal bind:this={publishModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Publish to Production"
|
title="Publish to Production"
|
||||||
confirmText="Publish"
|
confirmText="Publish"
|
||||||
onConfirm={deployApp}
|
onConfirm={deployApp}
|
||||||
|
dataCy={"deploy-app-modal"}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
>The changes you have made will be published to the production version of
|
>The changes you have made will be published to the production version of
|
||||||
|
@ -42,3 +81,59 @@
|
||||||
>
|
>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Publish in progress -->
|
||||||
|
<Modal bind:this={asyncModal}>
|
||||||
|
<ModalContent
|
||||||
|
showCancelButton={false}
|
||||||
|
showConfirmButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<Layout justifyItems="center">
|
||||||
|
<ProgressCircle size="XL" />
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Publish complete -->
|
||||||
|
<span class="publish-modal-wrap">
|
||||||
|
<Modal bind:this={publishCompleteModal}>
|
||||||
|
<ModalContent
|
||||||
|
confirmText="Done"
|
||||||
|
cancelText="View App"
|
||||||
|
onCancel={viewApp}
|
||||||
|
dataCy={"deploy-app-success-modal"}
|
||||||
|
>
|
||||||
|
<div slot="header" class="app-published-header">
|
||||||
|
<svg
|
||||||
|
width="26px"
|
||||||
|
height="26px"
|
||||||
|
class="spectrum-Icon success-icon"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-GlobeCheck" />
|
||||||
|
</svg>
|
||||||
|
<span class="app-published-header-text">App Published!</span>
|
||||||
|
</div>
|
||||||
|
<CopyInput
|
||||||
|
value={publishedUrl}
|
||||||
|
label="You can view your app at:"
|
||||||
|
dataCy="deployed-app-url"
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-published-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.success-icon {
|
||||||
|
color: var(--spectrum-global-color-green-600);
|
||||||
|
}
|
||||||
|
.app-published-header .app-published-header-text {
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import {
|
||||||
const DeploymentStatus = {
|
checkIncomingDeploymentStatus,
|
||||||
SUCCESS: "SUCCESS",
|
DeploymentStatus,
|
||||||
PENDING: "PENDING",
|
} from "components/deploy/utils"
|
||||||
FAILURE: "FAILURE",
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATE_OPTIONS = {
|
const DATE_OPTIONS = {
|
||||||
fullDate: {
|
fullDate: {
|
||||||
|
@ -42,30 +40,17 @@
|
||||||
const formatDate = (date, format) =>
|
const formatDate = (date, format) =>
|
||||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
||||||
|
|
||||||
// Required to check any updated deployment statuses between polls
|
|
||||||
function checkIncomingDeploymentStatus(current, incoming) {
|
|
||||||
for (let incomingDeployment of incoming) {
|
|
||||||
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
|
|
||||||
const currentDeployment = current.find(
|
|
||||||
deployment => deployment._id === incomingDeployment._id
|
|
||||||
)
|
|
||||||
|
|
||||||
// We have just been notified of an ongoing deployments failure
|
|
||||||
if (
|
|
||||||
!currentDeployment ||
|
|
||||||
currentDeployment.status === DeploymentStatus.PENDING
|
|
||||||
) {
|
|
||||||
showErrorReasonModal(incomingDeployment.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDeployments() {
|
async function fetchDeployments() {
|
||||||
try {
|
try {
|
||||||
const newDeployments = await API.getAppDeployments()
|
const newDeployments = await API.getAppDeployments()
|
||||||
if (deployments.length > 0) {
|
if (deployments.length > 0) {
|
||||||
checkIncomingDeploymentStatus(deployments, newDeployments)
|
const pendingDeployments = checkIncomingDeploymentStatus(
|
||||||
|
deployments,
|
||||||
|
newDeployments
|
||||||
|
)
|
||||||
|
if (pendingDeployments.length) {
|
||||||
|
showErrorReasonModal(pendingDeployments[0].err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deployments = newDeployments
|
deployments = newDeployments
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
hoverable
|
hoverable
|
||||||
on:click={revertModal.show}
|
on:click={revertModal.show}
|
||||||
tooltip="Revert changes"
|
tooltip="Revert changes"
|
||||||
|
dataCy="revert-application-topnav"
|
||||||
/>
|
/>
|
||||||
<Modal bind:this={revertModal}>
|
<Modal bind:this={revertModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
export const DeploymentStatus = {
|
||||||
|
SUCCESS: "SUCCESS",
|
||||||
|
PENDING: "PENDING",
|
||||||
|
FAILURE: "FAILURE",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required to check any updated deployment statuses between polls
|
||||||
|
export function checkIncomingDeploymentStatus(current, incoming) {
|
||||||
|
return incoming.reduce((acc, incomingDeployment) => {
|
||||||
|
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
|
||||||
|
const currentDeployment = current.find(
|
||||||
|
deployment => deployment._id === incomingDeployment._id
|
||||||
|
)
|
||||||
|
|
||||||
|
//We have just been notified of an ongoing deployments failure
|
||||||
|
if (
|
||||||
|
!currentDeployment ||
|
||||||
|
currentDeployment.status === DeploymentStatus.PENDING
|
||||||
|
) {
|
||||||
|
acc.push(incomingDeployment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -26,14 +26,6 @@
|
||||||
on:change={value => (parameters.rowId = value.detail)}
|
on:change={value => (parameters.rowId = value.detail)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Label small>Row Rev</Label>
|
|
||||||
<DrawerBindableInput
|
|
||||||
{bindings}
|
|
||||||
title="Row rev to delete"
|
|
||||||
value={parameters.revId}
|
|
||||||
on:change={value => (parameters.revId = value.detail)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Label small />
|
<Label small />
|
||||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label, Combobox } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import {
|
||||||
|
getActionProviderComponents,
|
||||||
|
buildFormSchema,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
export let bindings = []
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{
|
||||||
|
label: "Set value",
|
||||||
|
value: "set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reset to default value",
|
||||||
|
value: "reset",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
||||||
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
|
$: actionProviders = getActionProviderComponents(
|
||||||
|
$currentAsset,
|
||||||
|
$store.selectedComponentId,
|
||||||
|
"ValidateForm"
|
||||||
|
)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "set"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>Form</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.componentId}
|
||||||
|
options={actionProviders}
|
||||||
|
getOptionLabel={x => x._instanceName}
|
||||||
|
getOptionValue={x => x._id}
|
||||||
|
/>
|
||||||
|
<Label small>Type</Label>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={parameters.type}
|
||||||
|
options={typeOptions}
|
||||||
|
/>
|
||||||
|
<Label small>Field</Label>
|
||||||
|
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||||
|
{#if parameters.type === "set"}
|
||||||
|
<Label small>Value</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
value={parameters.value}
|
||||||
|
on:change={e => (parameters.value = e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,3 +14,4 @@ export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||||
export { default as S3Upload } from "./S3Upload.svelte"
|
export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
export { default as ExportData } from "./ExportData.svelte"
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||||
|
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||||
|
|
|
@ -42,25 +42,29 @@
|
||||||
"name": "Trigger Automation",
|
"name": "Trigger Automation",
|
||||||
"component": "TriggerAutomation"
|
"component": "TriggerAutomation"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Field Value",
|
||||||
|
"component": "UpdateFieldValue"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Validate Form",
|
"name": "Validate Form",
|
||||||
"component": "ValidateForm"
|
"component": "ValidateForm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Log Out",
|
"name": "Change Form Step",
|
||||||
"component": "LogOut"
|
"component": "ChangeFormStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Clear Form",
|
"name": "Clear Form",
|
||||||
"component": "ClearForm"
|
"component": "ClearForm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Close Screen Modal",
|
"name": "Log Out",
|
||||||
"component": "CloseScreenModal"
|
"component": "LogOut"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Change Form Step",
|
"name": "Close Screen Modal",
|
||||||
"component": "ChangeFormStep"
|
"component": "CloseScreenModal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Refresh Data Provider",
|
"name": "Refresh Data Provider",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
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 releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div 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={() => editApp(app)}>
|
||||||
|
@ -57,26 +58,38 @@
|
||||||
</StatusLight>
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight active={app.deployed} neutral={!app.deployed}>
|
<div class="app-status">
|
||||||
{#if app.deployed}Published{:else}Unpublished{/if}
|
{#if app.deployed}
|
||||||
</StatusLight>
|
<Icon name="Globe" disabled={false} />
|
||||||
|
Published
|
||||||
|
{:else}
|
||||||
|
<Icon name="GlobeStrike" disabled={true} />
|
||||||
|
<span class="disabled"> Unpublished </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<Button
|
<div class="app-row-actions">
|
||||||
size="S"
|
|
||||||
disabled={app.lockedOther}
|
|
||||||
on:click={() => editApp(app)}
|
|
||||||
secondary
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<ActionMenu align="right">
|
|
||||||
<Icon hoverable slot="control" name="More" />
|
|
||||||
{#if app.deployed}
|
{#if app.deployed}
|
||||||
<MenuItem on:click={() => viewApp(app)} icon="GlobeOutline">
|
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
||||||
View published app
|
>View app
|
||||||
</MenuItem>
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
||||||
|
>Preview
|
||||||
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Button
|
||||||
|
size="S"
|
||||||
|
cta
|
||||||
|
disabled={app.lockedOther}
|
||||||
|
on:click={() => editApp(app)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
|
<Icon hoverable slot="control" name="More" dataCy="app-row-actions-menu" />
|
||||||
{#if app.lockedYou}
|
{#if app.lockedYou}
|
||||||
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
||||||
Release lock
|
Release lock
|
||||||
|
@ -97,6 +110,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-row-actions {
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 75px 75px;
|
||||||
|
}
|
||||||
|
.app-status {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px 100px;
|
||||||
|
}
|
||||||
|
.app-status span.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
.name {
|
.name {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { auth } from "../stores/portal"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
export const FEATURE_FLAGS = {
|
||||||
|
LICENSING: "LICENSING",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEnabled = featureFlag => {
|
||||||
|
const user = get(auth).user
|
||||||
|
if (user?.featureFlags?.includes(featureFlag)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -6,14 +6,65 @@
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
ActionGroup,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
notifications,
|
||||||
|
PopoverMenu,
|
||||||
|
Layout,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
import { auth, apps } from "stores/portal"
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
// Get Package and set store
|
// Get Package and set store
|
||||||
let promise = getPackage()
|
let promise = getPackage()
|
||||||
|
let unpublishModal
|
||||||
|
let publishPopover
|
||||||
|
|
||||||
|
$: enrichedApps = enrichApps($apps, $auth.user)
|
||||||
|
const enrichApps = (apps, user) => {
|
||||||
|
const enrichedApps = apps
|
||||||
|
.map(app => ({
|
||||||
|
...app,
|
||||||
|
deployed: app.status === "published",
|
||||||
|
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
||||||
|
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
||||||
|
}))
|
||||||
|
.filter(app => {
|
||||||
|
return app.devId === application
|
||||||
|
})
|
||||||
|
|
||||||
|
return enrichedApps
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedApp = enrichedApps.length > 0 ? enrichedApps[0] : {}
|
||||||
|
|
||||||
|
$: deployments = []
|
||||||
|
$: latestDeployments = deployments
|
||||||
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
|
$: isPublished =
|
||||||
|
selectedApp.deployed && latestDeployments && latestDeployments?.length
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
|
||||||
// Sync once when you load the app
|
// Sync once when you load the app
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
|
@ -24,12 +75,20 @@
|
||||||
$: appInfo = $apps?.find(app => app.devId === application)
|
$: appInfo = $apps?.find(app => app.devId === application)
|
||||||
$: published = appInfo?.status === "published"
|
$: published = appInfo?.status === "published"
|
||||||
|
|
||||||
const viewPreviewApp = () => {
|
const previewApp = () => {
|
||||||
window.open(`/${application}`)
|
window.open(`/${application}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewPublishedApp = () => {
|
const viewApp = () => {
|
||||||
window.open(`/app${appInfo?.url}`)
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
if (selectedApp.url) {
|
||||||
|
window.open(`/app${selectedApp.url}`)
|
||||||
|
} else {
|
||||||
|
window.open(`/${selectedApp.prodId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPackage() {
|
async function getPackage() {
|
||||||
|
@ -62,23 +121,74 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!hasSynced && application) {
|
if (!hasSynced && application) {
|
||||||
try {
|
try {
|
||||||
await API.syncApp(application)
|
await API.syncApp(application)
|
||||||
|
await apps.load()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Failed to sync with production database")
|
notifications.error("Failed to sync with production database")
|
||||||
}
|
}
|
||||||
hasSynced = true
|
hasSynced = true
|
||||||
}
|
}
|
||||||
if (!$apps?.length) {
|
deployments = await fetchDeployments()
|
||||||
apps.load()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
store.actions.reset()
|
store.actions.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const unpublishApp = () => {
|
||||||
|
publishPopover.hide()
|
||||||
|
unpublishModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const completePublish = async () => {
|
||||||
|
try {
|
||||||
|
await apps.load()
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error refreshing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnpublishApp = async () => {
|
||||||
|
if (!application || !isPublished) {
|
||||||
|
//confirm the app has loaded.
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await promise}
|
{#await promise}
|
||||||
|
@ -113,35 +223,92 @@
|
||||||
<Icon
|
<Icon
|
||||||
name="Visibility"
|
name="Visibility"
|
||||||
hoverable
|
hoverable
|
||||||
on:click={viewPreviewApp}
|
on:click={previewApp}
|
||||||
tooltip="View app preview"
|
tooltip="View app preview"
|
||||||
/>
|
/>
|
||||||
<Icon
|
{#if isPublished}
|
||||||
name={published ? "Globe" : "GlobeStrike"}
|
<PopoverMenu
|
||||||
hoverable
|
bind:this={publishPopover}
|
||||||
disabled={!published}
|
align="right"
|
||||||
on:click={viewPublishedApp}
|
disabled={!isPublished}
|
||||||
tooltip={published
|
dataCy="publish-popover-menu"
|
||||||
? "View published app"
|
>
|
||||||
: "Your app is not published"}
|
<div slot="control" class="icon app-status-icon">
|
||||||
/>
|
<Icon
|
||||||
<DeployModal />
|
size="M"
|
||||||
|
hoverable
|
||||||
|
name="Globe"
|
||||||
|
disabled={!isPublished}
|
||||||
|
tooltip="Your published app"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Layout gap="M">
|
||||||
|
<Heading size="XS">Your published app</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
{#if isPublished}
|
||||||
|
{processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
<div class="publish-popover-actions">
|
||||||
|
<Button
|
||||||
|
warning={true}
|
||||||
|
icon="GlobeStrike"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={unpublishApp}
|
||||||
|
dataCy="publish-popover-action"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</Button>
|
||||||
|
<Button cta on:click={viewApp}>View app</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</PopoverMenu>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !isPublished}
|
||||||
|
<Icon
|
||||||
|
size="M"
|
||||||
|
name="GlobeStrike"
|
||||||
|
disabled
|
||||||
|
tooltip="Your app has not been published yet"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<DeployModal onOk={completePublish} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>Something went wrong: {error.message}</p>
|
<p>Something went wrong: {error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
.loading {
|
.loading {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
notifications.success("Invitation accepted successfully")
|
notifications.success("Invitation accepted successfully")
|
||||||
$goto("../auth/login")
|
$goto("../auth/login")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error accepting invitation")
|
notifications.error(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||||
import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte"
|
import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { isEnabled, FEATURE_FLAGS } from "../../../helpers/featureFlags"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let userInfoModal
|
let userInfoModal
|
||||||
|
@ -54,10 +55,17 @@
|
||||||
if (!$adminStore.cloud) {
|
if (!$adminStore.cloud) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
title: "Updates",
|
title: "Update",
|
||||||
href: "/builder/portal/settings/update",
|
href: "/builder/portal/settings/update",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||||
|
menu = menu.concat({
|
||||||
|
title: "Upgrade",
|
||||||
|
href: "/builder/portal/settings/upgrade",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
|
|
|
@ -75,8 +75,8 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Heading size="M">{createAppTitle}</Heading>
|
<Heading size="L">{createAppTitle}</Heading>
|
||||||
<Body size="S">
|
<Body size="M">
|
||||||
{welcomeBody}
|
{welcomeBody}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
dataCy="create-app-btn"
|
dataCy="create-app-btn"
|
||||||
size="L"
|
size="M"
|
||||||
icon="Add"
|
icon="Add"
|
||||||
cta
|
cta
|
||||||
on:click={initiateAppCreation}
|
on:click={initiateAppCreation}
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
<Button
|
<Button
|
||||||
dataCy="import-app-btn"
|
dataCy="import-app-btn"
|
||||||
icon="Import"
|
icon="Import"
|
||||||
size="L"
|
size="M"
|
||||||
quiet
|
quiet
|
||||||
secondary
|
secondary
|
||||||
on:click={initiateAppImport}
|
on:click={initiateAppImport}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import AppRow from "components/start/AppRow.svelte"
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import Logo from "assets/bb-space-man.svg"
|
import Logo from "assets/bb-space-man.svg"
|
||||||
|
|
||||||
let sortBy = "name"
|
let sortBy = "name"
|
||||||
|
@ -167,6 +167,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewApp = app => {
|
const viewApp = app => {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: app.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
if (app.url) {
|
if (app.url) {
|
||||||
window.open(`/app${app.url}`)
|
window.open(`/app${app.url}`)
|
||||||
} else {
|
} else {
|
||||||
|
@ -174,6 +178,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewApp = app => {
|
||||||
|
window.open(`/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
if (app.lockedOther) {
|
if (app.lockedOther) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -205,6 +213,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
analytics.captureEvent(Events.APP.UNPUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
})
|
||||||
await API.unpublishApp(selectedApp.prodId)
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
await apps.load()
|
await apps.load()
|
||||||
notifications.success("App unpublished successfully")
|
notifications.success("App unpublished successfully")
|
||||||
|
@ -392,6 +403,7 @@
|
||||||
{exportApp}
|
{exportApp}
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
|
{previewApp}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -444,6 +456,7 @@
|
||||||
title="Confirm unpublish"
|
title="Confirm unpublish"
|
||||||
okText="Unpublish app"
|
okText="Unpublish app"
|
||||||
onOk={confirmUnpublishApp}
|
onOk={confirmUnpublishApp}
|
||||||
|
dataCy={"unpublish-modal"}
|
||||||
>
|
>
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
})
|
})
|
||||||
notifications.success("Successfully created user")
|
notifications.success("Successfully created user")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating user")
|
notifications.error(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
Link,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { API } from "api"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
$: license = $auth.user.license
|
||||||
|
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
$: activateDisabled = !licenseKey || licenseKeyDisabled
|
||||||
|
|
||||||
|
let licenseInfo
|
||||||
|
|
||||||
|
let licenseKeyDisabled = false
|
||||||
|
let licenseKeyType = "text"
|
||||||
|
let licenseKey = ""
|
||||||
|
|
||||||
|
// Make sure page can't be visited directly in cloud
|
||||||
|
$: {
|
||||||
|
if ($admin.cloud) {
|
||||||
|
$redirect("../../portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activate = async () => {
|
||||||
|
await API.activateLicenseKey({ licenseKey })
|
||||||
|
await auth.getSelf()
|
||||||
|
await setLicenseInfo()
|
||||||
|
notifications.success("Successfully activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
try {
|
||||||
|
await API.refreshLicense()
|
||||||
|
await auth.getSelf()
|
||||||
|
notifications.success("Refreshed license")
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
notifications.error("Error refreshing license")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deactivate the license key field if there is a license key set
|
||||||
|
$: {
|
||||||
|
if (licenseInfo?.licenseKey) {
|
||||||
|
licenseKey = "**********************************************"
|
||||||
|
licenseKeyType = "password"
|
||||||
|
licenseKeyDisabled = true
|
||||||
|
activateDisabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLicenseInfo = async () => {
|
||||||
|
licenseInfo = await API.getLicenseInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await setLicenseInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.isAdmin}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="M">Upgrade</Heading>
|
||||||
|
<Body size="M">
|
||||||
|
{#if license.plan.type === "free"}
|
||||||
|
Upgrade your budibase installation to unlock additional features. To
|
||||||
|
subscribe to a plan visit your <Link size="L" href={upgradeUrl}
|
||||||
|
>Account</Link
|
||||||
|
>.
|
||||||
|
{:else}
|
||||||
|
To manage your plan visit your <Link size="L" href={upgradeUrl}
|
||||||
|
>Account</Link
|
||||||
|
>.
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Activate</Heading>
|
||||||
|
<Body size="S">Enter your license key below to activate your plan</Body>
|
||||||
|
</Layout>
|
||||||
|
<Layout noPadding>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">License Key</Label>
|
||||||
|
<Input
|
||||||
|
thin
|
||||||
|
bind:value={licenseKey}
|
||||||
|
type={licenseKeyType}
|
||||||
|
disabled={licenseKeyDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button cta on:click={activate} disabled={activateDisabled}
|
||||||
|
>Activate</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout gap="L" noPadding>
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Heading size="S">Plan</Heading>
|
||||||
|
<Layout noPadding gap="XXS">
|
||||||
|
<Body size="S">You are currently on the {license.plan.type} plan</Body
|
||||||
|
>
|
||||||
|
<Body size="XS">
|
||||||
|
{processStringSync(
|
||||||
|
"Updated {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(license.refreshedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button secondary on:click={refresh}>Refresh</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Publish Dev",
|
|
||||||
"program": "${workspaceFolder}/scripts/publishDev.js"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1834,7 +1834,12 @@
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": ["section", "form"],
|
"illegalChildren": ["section", "form"],
|
||||||
"actions": ["ValidateForm", "ClearForm", "ChangeFormStep"],
|
"actions": [
|
||||||
|
"ValidateForm",
|
||||||
|
"ClearForm",
|
||||||
|
"ChangeFormStep",
|
||||||
|
"UpdateFieldValue"
|
||||||
|
],
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
|
@ -1975,6 +1980,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2049,6 +2065,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2089,6 +2116,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2125,6 +2163,17 @@
|
||||||
"key": "placeholder",
|
"key": "placeholder",
|
||||||
"placeholder": "Choose an option"
|
"placeholder": "Choose an option"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
|
@ -2274,6 +2323,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Autocomplete",
|
"label": "Autocomplete",
|
||||||
|
@ -2399,6 +2459,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2439,6 +2510,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "Formatting",
|
"label": "Formatting",
|
||||||
|
@ -2512,6 +2594,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2657,6 +2750,17 @@
|
||||||
"label": "Extensions",
|
"label": "Extensions",
|
||||||
"key": "extensions"
|
"key": "extensions"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2697,6 +2801,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Autocomplete",
|
"label": "Autocomplete",
|
||||||
|
@ -2742,6 +2857,17 @@
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
@ -2750,6 +2876,62 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"s3upload": {
|
||||||
|
"name": "S3 File Upload",
|
||||||
|
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
||||||
|
"icon": "UploadToCloud",
|
||||||
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/attachment",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dataSource/s3",
|
||||||
|
"label": "S3 Datasource",
|
||||||
|
"key": "datasourceId"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Bucket",
|
||||||
|
"key": "bucket"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "File Name",
|
||||||
|
"key": "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On Change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/attachment",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dataprovider": {
|
"dataprovider": {
|
||||||
"name": "Data Provider",
|
"name": "Data Provider",
|
||||||
"info": "Pagination is only available for data stored in tables.",
|
"info": "Pagination is only available for data stored in tables.",
|
||||||
|
@ -3581,51 +3763,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"s3upload": {
|
|
||||||
"name": "S3 File Upload",
|
|
||||||
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
|
||||||
"icon": "UploadToCloud",
|
|
||||||
"styles": ["size"],
|
|
||||||
"editable": true,
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "field/attachment",
|
|
||||||
"label": "Field",
|
|
||||||
"key": "field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Label",
|
|
||||||
"key": "label"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "dataSource/s3",
|
|
||||||
"label": "S3 Datasource",
|
|
||||||
"key": "datasourceId"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Bucket",
|
|
||||||
"key": "bucket"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "File Name",
|
|
||||||
"key": "key"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "boolean",
|
|
||||||
"label": "Disabled",
|
|
||||||
"key": "disabled",
|
|
||||||
"defaultValue": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "validation/attachment",
|
|
||||||
"label": "Validation",
|
|
||||||
"key": "validation"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"markdownviewer": {
|
"markdownviewer": {
|
||||||
"name": "Markdown Viewer",
|
"name": "Markdown Viewer",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.105-alpha.20",
|
"version": "1.0.105-alpha.35",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.105-alpha.20",
|
"@budibase/bbui": "^1.0.105-alpha.35",
|
||||||
"@budibase/frontend-core": "^1.0.105-alpha.20",
|
"@budibase/frontend-core": "^1.0.105-alpha.35",
|
||||||
"@budibase/string-templates": "^1.0.105-alpha.20",
|
"@budibase/string-templates": "^1.0.105-alpha.35",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let validation
|
export let validation
|
||||||
export let extensions
|
export let extensions
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -38,6 +39,13 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -55,9 +63,7 @@
|
||||||
value={fieldState.value}
|
value={fieldState.value}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={e => {
|
on:change={handleChange}
|
||||||
fieldApi.setValue(e.detail)
|
|
||||||
}}
|
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{extensions}
|
{extensions}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let size
|
export let size
|
||||||
export let validation
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -25,6 +26,13 @@
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -44,8 +52,8 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
{size}
|
{size}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
|
||||||
{text}
|
{text}
|
||||||
|
on:change={handleChange}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -10,9 +10,17 @@
|
||||||
export let timeOnly = false
|
export let timeOnly = false
|
||||||
export let validation
|
export let validation
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -28,7 +36,7 @@
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreDatePicker
|
<CoreDatePicker
|
||||||
value={fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
|
|
|
@ -219,10 +219,10 @@
|
||||||
})
|
})
|
||||||
return valid
|
return valid
|
||||||
},
|
},
|
||||||
clear: () => {
|
reset: () => {
|
||||||
// Clear the form by clearing each individual field
|
// Reset the form by resetting each individual field
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
get(field).fieldApi.clearValue()
|
get(field).fieldApi.reset()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
changeStep: ({ type, number }) => {
|
changeStep: ({ type, number }) => {
|
||||||
|
@ -241,6 +241,22 @@
|
||||||
currentStep.set(step)
|
currentStep.set(step)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setFieldValue: (fieldName, value) => {
|
||||||
|
const field = getField(fieldName)
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { fieldApi } = get(field)
|
||||||
|
fieldApi.setValue(value)
|
||||||
|
},
|
||||||
|
resetField: fieldName => {
|
||||||
|
const field = getField(fieldName)
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { fieldApi } = get(field)
|
||||||
|
fieldApi.reset()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an API for a specific field
|
// Creates an API for a specific field
|
||||||
|
@ -268,11 +284,11 @@
|
||||||
return !error
|
return !error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clears the value of a certain field back to the initial value
|
// Clears the value of a certain field back to the default value
|
||||||
const clearValue = () => {
|
const reset = () => {
|
||||||
const fieldInfo = getField(field)
|
const fieldInfo = getField(field)
|
||||||
const { fieldState } = get(fieldInfo)
|
const { fieldState } = get(fieldInfo)
|
||||||
const newValue = initialValues[field] ?? fieldState.defaultValue
|
const newValue = fieldState.defaultValue
|
||||||
|
|
||||||
// Update field state
|
// Update field state
|
||||||
fieldInfo.update(state => {
|
fieldInfo.update(state => {
|
||||||
|
@ -329,7 +345,7 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setValue,
|
setValue,
|
||||||
clearValue,
|
reset,
|
||||||
updateValidation,
|
updateValidation,
|
||||||
setDisabled,
|
setDisabled,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
|
@ -354,11 +370,20 @@
|
||||||
// register their fields to step 1
|
// register their fields to step 1
|
||||||
setContext("form-step", writable(1))
|
setContext("form-step", writable(1))
|
||||||
|
|
||||||
|
const handleUpdateFieldValue = ({ type, field, value }) => {
|
||||||
|
if (type === "set") {
|
||||||
|
formApi.setFieldValue(field, value)
|
||||||
|
} else {
|
||||||
|
formApi.resetField(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Action context to pass to children
|
// Action context to pass to children
|
||||||
const actions = [
|
const actions = [
|
||||||
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
||||||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
{ type: ActionTypes.ClearForm, callback: formApi.reset },
|
||||||
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
||||||
|
{ type: ActionTypes.UpdateFieldValue, callback: handleUpdateFieldValue },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let defaultValue = ""
|
export let defaultValue = ""
|
||||||
|
export let onChange
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const validation = [
|
const validation = [
|
||||||
|
@ -33,6 +34,14 @@
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
const value = parseValue(e.detail)
|
||||||
|
fieldApi.setValue(value)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -49,7 +58,7 @@
|
||||||
<div style="--height: {height};">
|
<div style="--height: {height};">
|
||||||
<CoreTextArea
|
<CoreTextArea
|
||||||
value={serialiseValue(fieldState.value)}
|
value={serialiseValue(fieldState.value)}
|
||||||
on:change={e => fieldApi.setValue(parseValue(e.detail))}
|
on:change={handleChange}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let validation
|
export let validation
|
||||||
export let defaultValue = ""
|
export let defaultValue = ""
|
||||||
export let format = "auto"
|
export let format = "auto"
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -44,6 +45,13 @@
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -61,7 +69,7 @@
|
||||||
{#if useRichText}
|
{#if useRichText}
|
||||||
<CoreRichTextField
|
<CoreRichTextField
|
||||||
value={fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
|
@ -78,7 +86,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<CoreTextArea
|
<CoreTextArea
|
||||||
value={fieldState.value}
|
value={fieldState.value}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let valueColumn
|
export let valueColumn
|
||||||
export let customOptions
|
export let customOptions
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -34,13 +35,18 @@
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.split(",").map(value => value.trim())
|
return values.split(",").map(value => value.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -62,7 +68,7 @@
|
||||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{options}
|
{options}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
export let customOptions
|
export let customOptions
|
||||||
export let autocomplete = false
|
export let autocomplete = false
|
||||||
export let direction = "vertical"
|
export let direction = "vertical"
|
||||||
|
export let onChange
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -30,6 +31,13 @@
|
||||||
valueColumn,
|
valueColumn,
|
||||||
customOptions
|
customOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: e.detail })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -52,7 +60,7 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
{options}
|
{options}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
|
@ -66,7 +74,7 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
{options}
|
{options}
|
||||||
{direction}
|
{direction}
|
||||||
on:change={e => fieldApi.setValue(e.detail)}
|
on:change={handleChange}
|
||||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||||
/>
|
/>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue