Merge branch 'develop' into backmerge-master-20230727
This commit is contained in:
commit
25019aa31e
|
@ -5,7 +5,7 @@
|
||||||
"jest": true,
|
"jest": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"parser": "babel-eslint",
|
"parser": "@babel/eslint-parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2019,
|
"ecmaVersion": 2019,
|
||||||
"sourceType": "module",
|
"sourceType": "module",
|
||||||
|
@ -18,17 +18,23 @@
|
||||||
"*.spec.js",
|
"*.spec.js",
|
||||||
"bundle.js"
|
"bundle.js"
|
||||||
],
|
],
|
||||||
"plugins": ["svelte3"],
|
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.svelte"],
|
"files": ["**/*.svelte"],
|
||||||
"processor": "svelte3/svelte3"
|
"extends": "plugin:svelte/recommended",
|
||||||
|
"parser": "svelte-eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@babel/eslint-parser",
|
||||||
|
"ecmaVersion": 2019,
|
||||||
|
"sourceType": "module",
|
||||||
|
"allowImportExportEverywhere": true
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["**/*.ts"],
|
"files": ["**/*.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": [],
|
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
@ -41,7 +47,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-self-assign": "off"
|
"no-self-assign": "off",
|
||||||
|
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }]
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"GeolocationPositionError": true
|
"GeolocationPositionError": true
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Configuration for probot-stale - https://github.com/probot/stale
|
|
||||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
|
||||||
daysUntilStale: 60
|
|
||||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
|
||||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
|
||||||
daysUntilClose: false
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- pinned
|
|
||||||
- security
|
|
||||||
- roadmap
|
|
||||||
# Label to use when marking an issue as stale
|
|
||||||
staleLabel: stale
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
||||||
closeComment: false
|
|
|
@ -12,9 +12,6 @@ on:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -162,7 +159,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cd qa-core
|
cd qa-core
|
||||||
yarn setup
|
yarn setup
|
||||||
yarn test:ci
|
yarn serve:test:self:ci
|
||||||
env:
|
env:
|
||||||
BB_ADMIN_USER_EMAIL: admin
|
BB_ADMIN_USER_EMAIL: admin
|
||||||
BB_ADMIN_USER_PASSWORD: admin
|
BB_ADMIN_USER_PASSWORD: admin
|
||||||
|
@ -185,7 +182,7 @@ jobs:
|
||||||
pro_commit=$(git rev-parse HEAD)
|
pro_commit=$(git rev-parse HEAD)
|
||||||
|
|
||||||
branch="${{ github.base_ref || github.ref_name }}"
|
branch="${{ github.base_ref || github.ref_name }}"
|
||||||
echo "Running on branch `$branch` (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
||||||
|
|
||||||
if [[ $branch == "master" ]]; then
|
if [[ $branch == "master" ]]; then
|
||||||
base_commit=$(git rev-parse origin/master)
|
base_commit=$(git rev-parse origin/master)
|
||||||
|
|
|
@ -34,7 +34,6 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
@ -58,9 +57,12 @@ jobs:
|
||||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||||
yarn release
|
yarn release
|
||||||
|
|
||||||
- name: "Get Previous tag"
|
- name: "Get Current tag"
|
||||||
id: previoustag
|
id: currenttag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
run: |
|
||||||
|
version=v$(./scripts/getCurrentVersion.sh)
|
||||||
|
echo 'Using tag $version'
|
||||||
|
echo "::set-output name=tag::$resversionult"
|
||||||
|
|
||||||
- name: Build/release Docker images
|
- name: Build/release Docker images
|
||||||
run: |
|
run: |
|
||||||
|
@ -69,7 +71,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
|
||||||
|
|
||||||
release-helm-chart:
|
release-helm-chart:
|
||||||
needs: [release-images]
|
needs: [release-images]
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Close stale issues and PRs # https://github.com/actions/stale
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 1 * * *' # 1:30 every morning
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v8
|
||||||
|
with:
|
||||||
|
# stale rules
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-pr-stale: 7
|
||||||
|
stale-issue-label: stale
|
||||||
|
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
|
||||||
|
|
||||||
|
# close rules
|
||||||
|
# days after being marked as stale to close
|
||||||
|
days-before-close: 30
|
||||||
|
close-issue-label: closed-stale
|
||||||
|
close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
|
||||||
|
days-before-pr-close: 7
|
||||||
|
|
||||||
|
# exemptions
|
||||||
|
exempt-pr-labels: pinned,security,roadmap
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
nodejs 14.20.1
|
nodejs 14.21.3
|
||||||
python 3.10.0
|
python 3.10.0
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
|
||||||
|
}
|
|
@ -40,6 +40,24 @@ spec:
|
||||||
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: proxy-service
|
name: proxy-service
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: {{ .Values.services.proxy.port }}
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 2
|
||||||
|
timeoutSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: {{ .Values.services.proxy.port }}
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 5
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 2
|
||||||
|
timeoutSeconds: 3
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.proxy.port }}
|
- containerPort: {{ .Values.services.proxy.port }}
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
||||||
|
|
||||||
### Pro
|
### Pro
|
||||||
|
|
||||||
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g.
|
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro.
|
||||||
|
|
||||||
|
Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware.
|
||||||
|
|
||||||
|
From here - to develop a change in pro, you can follow the below flow:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
# enter the pro submodule
|
||||||
|_ budibase
|
cd packages/pro
|
||||||
|_ budibase-pro
|
# get the base branch you are working from (same as monorepo)
|
||||||
|
git fetch
|
||||||
|
git checkout <develop | master>
|
||||||
|
# create a branch, named the same as the branch in your monorepo
|
||||||
|
git checkout -b <some branch>
|
||||||
|
... make changes
|
||||||
|
# commit the changes you've made, with a message for pro
|
||||||
|
git commit <something>
|
||||||
|
# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref"
|
||||||
|
cd ../..
|
||||||
|
git add packages/pro
|
||||||
|
git commit <add the new reference to main repo>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline.
|
||||||
|
|
||||||
Note that only budibase maintainers will be able to access the pro repo.
|
Note that only budibase maintainers will be able to access the pro repo.
|
||||||
|
|
||||||
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation.
|
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation.
|
||||||
|
|
|
@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
|
||||||
|
|
||||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||||
PLUGINS_DIR=
|
PLUGINS_DIR=
|
||||||
|
ROLLING_LOG_MAX_SIZE=
|
5
nx.json
5
nx.json
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"tasksRunnerOptions": {
|
"tasksRunnerOptions": {
|
||||||
"default": {
|
"default": {
|
||||||
"runner": "nx/tasks-runners/default",
|
"runner": "nx-cloud",
|
||||||
"options": {
|
"options": {
|
||||||
"cacheableOperations": ["build", "test"]
|
"cacheableOperations": ["build", "test"],
|
||||||
|
"accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
28
package.json
28
package.json
|
@ -3,27 +3,32 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@nx/js": "16.2.1",
|
"@nx/js": "16.4.3",
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"babel-eslint": "^10.0.3",
|
|
||||||
"esbuild": "^0.18.17",
|
"esbuild": "^0.18.17",
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.8.0",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-plugin-cypress": "^2.11.3",
|
"eslint-plugin-cypress": "^2.11.3",
|
||||||
"eslint-plugin-svelte3": "^3.2.0",
|
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.0.2",
|
"lerna": "7.1.1",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
|
"nx": "16.4.3",
|
||||||
|
"nx-cloud": "16.0.5",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.38.2",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3",
|
||||||
|
"@babel/core": "^7.22.5",
|
||||||
|
"@babel/eslint-parser": "^7.22.5",
|
||||||
|
"@babel/preset-env": "^7.22.5",
|
||||||
|
"eslint-plugin-svelte": "^2.32.2",
|
||||||
|
"svelte-eslint-parser": "^0.32.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/syncProPackage.js",
|
"preinstall": "node scripts/syncProPackage.js",
|
||||||
|
@ -41,7 +46,7 @@
|
||||||
"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 --stream --parallel dev:stack:nuke",
|
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
|
@ -49,13 +54,13 @@
|
||||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder",
|
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages qa-core",
|
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"build:specs": "lerna run --stream specs",
|
"build:specs": "lerna run --stream specs",
|
||||||
|
@ -103,5 +108,8 @@
|
||||||
"@budibase/string-templates": "0.0.0",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.0"
|
"@budibase/types": "0.0.0"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0 <15.0.0"
|
||||||
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-find": "7.2.2",
|
"pouchdb-find": "7.2.2",
|
||||||
"redlock": "4.2.0",
|
"redlock": "4.2.0",
|
||||||
|
"rotating-file-stream": "3.1.0",
|
||||||
"sanitize-s3-objectkey": "0.0.1",
|
"sanitize-s3-objectkey": "0.0.1",
|
||||||
"semver": "7.3.7",
|
"semver": "7.3.7",
|
||||||
"tar-fs": "2.1.1",
|
"tar-fs": "2.1.1",
|
||||||
|
|
|
@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const dbUser = await db.get(userId)
|
const dbUser = await db.get<any>(userId)
|
||||||
|
|
||||||
//Do not overwrite the refresh token if a valid one is not provided.
|
//Do not overwrite the refresh token if a valid one is not provided.
|
||||||
if (typeof details.refreshToken !== "string") {
|
if (typeof details.refreshToken !== "string") {
|
||||||
|
|
|
@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init"
|
||||||
import { doWithDB, DocumentType } from "../db"
|
import { doWithDB, DocumentType } from "../db"
|
||||||
import { Database, App } from "@budibase/types"
|
import { Database, App } from "@budibase/types"
|
||||||
|
|
||||||
const AppState = {
|
export enum AppState {
|
||||||
INVALID: "invalid",
|
INVALID = "invalid",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeletedApp {
|
||||||
|
state: AppState
|
||||||
|
}
|
||||||
|
|
||||||
const EXPIRY_SECONDS = 3600
|
const EXPIRY_SECONDS = 3600
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) {
|
||||||
* @param {string} appId the id of the app to get metadata from.
|
* @param {string} appId the id of the app to get metadata from.
|
||||||
* @returns {object} the app metadata.
|
* @returns {object} the app metadata.
|
||||||
*/
|
*/
|
||||||
export async function getAppMetadata(appId: string) {
|
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
||||||
const client = await getAppClient()
|
const client = await getAppClient()
|
||||||
// try cache
|
// try cache
|
||||||
let metadata = await client.get(appId)
|
let metadata = await client.get(appId)
|
||||||
|
@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) {
|
||||||
}
|
}
|
||||||
await client.store(appId, metadata, expiry)
|
await client.store(appId, metadata, expiry)
|
||||||
}
|
}
|
||||||
// we've stored in the cache an object to tell us that it is currently invalid
|
|
||||||
if (isInvalid(metadata)) {
|
return metadata
|
||||||
throw { status: 404, message: "No app metadata found" }
|
|
||||||
}
|
|
||||||
return metadata as App
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
|
||||||
*/
|
*/
|
||||||
async function populateFromDB(userId: string, tenantId: string) {
|
async function populateFromDB(userId: string, tenantId: string) {
|
||||||
const db = tenancy.getTenantDB(tenantId)
|
const db = tenancy.getTenantDB(tenantId)
|
||||||
const user = await db.get(userId)
|
const user = await db.get<any>(userId)
|
||||||
user.budibaseAccess = true
|
user.budibaseAccess = true
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const account = await accounts.getAccount(user.email)
|
const account = await accounts.getAccount(user.email)
|
||||||
|
|
|
@ -20,6 +20,8 @@ export enum Header {
|
||||||
TYPE = "x-budibase-type",
|
TYPE = "x-budibase-type",
|
||||||
PREVIEW_ROLE = "x-budibase-role",
|
PREVIEW_ROLE = "x-budibase-role",
|
||||||
TENANT_ID = "x-budibase-tenant-id",
|
TENANT_ID = "x-budibase-tenant-id",
|
||||||
|
VERIFICATION_CODE = "x-budibase-verification-code",
|
||||||
|
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
||||||
TOKEN = "x-budibase-token",
|
TOKEN = "x-budibase-token",
|
||||||
CSRF_TOKEN = "x-csrf-token",
|
CSRF_TOKEN = "x-csrf-token",
|
||||||
CORRELATION_ID = "x-budibase-correlation-id",
|
CORRELATION_ID = "x-budibase-correlation-id",
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const CONSTANT_INTERNAL_ROW_COLS = [
|
||||||
|
"_id",
|
||||||
|
"_rev",
|
||||||
|
"type",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"tableId",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
|
|
@ -2,3 +2,4 @@ export * from "./connections"
|
||||||
export * from "./DatabaseImpl"
|
export * from "./DatabaseImpl"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB"
|
export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB"
|
||||||
|
export * from "../constants"
|
||||||
|
|
|
@ -5,7 +5,7 @@ export async function createUserIndex() {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get<any>("_design/database")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.status === 404) {
|
if (err.status === 404) {
|
||||||
designDoc = { _id: "_design/database" }
|
designDoc = { _id: "_design/database" }
|
||||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
||||||
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
||||||
import { getTenantId, getGlobalDBName } from "../context"
|
import { getTenantId, getGlobalDBName } from "../context"
|
||||||
import { doWithDB, directCouchAllDbs } from "./db"
|
import { doWithDB, directCouchAllDbs } from "./db"
|
||||||
import { getAppMetadata } from "../cache/appMetadata"
|
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
|
||||||
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
||||||
import { App, Database } from "@budibase/types"
|
import { App, Database } from "@budibase/types"
|
||||||
import { getStartEndKeyURL } from "../docIds"
|
import { getStartEndKeyURL } from "../docIds"
|
||||||
|
@ -101,7 +101,9 @@ export async function getAllApps({
|
||||||
const response = await Promise.allSettled(appPromises)
|
const response = await Promise.allSettled(appPromises)
|
||||||
const apps = response
|
const apps = response
|
||||||
.filter(
|
.filter(
|
||||||
(result: any) => result.status === "fulfilled" && result.value != null
|
(result: any) =>
|
||||||
|
result.status === "fulfilled" &&
|
||||||
|
result.value?.state !== AppState.INVALID
|
||||||
)
|
)
|
||||||
.map(({ value }: any) => value)
|
.map(({ value }: any) => value)
|
||||||
if (!all) {
|
if (!all) {
|
||||||
|
@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) {
|
||||||
)
|
)
|
||||||
// have to list the apps which exist, some may have been deleted
|
// have to list the apps which exist, some may have been deleted
|
||||||
return settled
|
return settled
|
||||||
.filter(promise => promise.status === "fulfilled")
|
.filter(
|
||||||
|
promise =>
|
||||||
|
promise.status === "fulfilled" &&
|
||||||
|
(promise.value as DeletedApp).state !== AppState.INVALID
|
||||||
|
)
|
||||||
.map(promise => (promise as PromiseFulfilledResult<App>).value)
|
.map(promise => (promise as PromiseFulfilledResult<App>).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,10 @@ function httpLogging() {
|
||||||
return process.env.HTTP_LOGGING
|
return process.env.HTTP_LOGGING
|
||||||
}
|
}
|
||||||
|
|
||||||
function findVersion() {
|
function getPackageJsonFields(): {
|
||||||
|
VERSION: string
|
||||||
|
SERVICE_NAME: string
|
||||||
|
} {
|
||||||
function findFileInAncestors(
|
function findFileInAncestors(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
currentDir: string
|
currentDir: string
|
||||||
|
@ -69,10 +72,14 @@ function findVersion() {
|
||||||
try {
|
try {
|
||||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||||
return JSON.parse(content).version
|
const parsedContent = JSON.parse(content)
|
||||||
|
return {
|
||||||
|
VERSION: parsedContent.version,
|
||||||
|
SERVICE_NAME: parsedContent.name,
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||||
return undefined
|
return { VERSION: "", SERVICE_NAME: "" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +161,7 @@ const environment = {
|
||||||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||||
: false,
|
: false,
|
||||||
VERSION: findVersion(),
|
...getPackageJsonFields(),
|
||||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
|
@ -162,6 +169,7 @@ const environment = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
environment[key] = value
|
environment[key] = value
|
||||||
},
|
},
|
||||||
|
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up any environment variable edge cases
|
// clean up any environment variable edge cases
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
export * as correlation from "./correlation/correlation"
|
export * as correlation from "./correlation/correlation"
|
||||||
export { logger } from "./pino/logger"
|
export { logger } from "./pino/logger"
|
||||||
export * from "./alerts"
|
export * from "./alerts"
|
||||||
|
export * as system from "./system"
|
||||||
// turn off or on context logging i.e. tenantId, appId etc
|
|
||||||
export let LOG_CONTEXT = true
|
|
||||||
|
|
|
@ -1,37 +1,60 @@
|
||||||
import env from "../../environment"
|
|
||||||
import pino, { LoggerOptions } from "pino"
|
import pino, { LoggerOptions } from "pino"
|
||||||
|
import pinoPretty from "pino-pretty"
|
||||||
|
|
||||||
|
import { IdentityType } from "@budibase/types"
|
||||||
|
import env from "../../environment"
|
||||||
import * as context from "../../context"
|
import * as context from "../../context"
|
||||||
import * as correlation from "../correlation"
|
import * as correlation from "../correlation"
|
||||||
import { IdentityType } from "@budibase/types"
|
|
||||||
import { LOG_CONTEXT } from "../index"
|
import { localFileDestination } from "../system"
|
||||||
|
|
||||||
// LOGGER
|
// LOGGER
|
||||||
|
|
||||||
let pinoInstance: pino.Logger | undefined
|
let pinoInstance: pino.Logger | undefined
|
||||||
if (!env.DISABLE_PINO_LOGGER) {
|
if (!env.DISABLE_PINO_LOGGER) {
|
||||||
|
const level = env.LOG_LEVEL
|
||||||
const pinoOptions: LoggerOptions = {
|
const pinoOptions: LoggerOptions = {
|
||||||
level: env.LOG_LEVEL,
|
level,
|
||||||
formatters: {
|
formatters: {
|
||||||
level: label => {
|
level: level => {
|
||||||
return { level: label.toUpperCase() }
|
return { level: level.toUpperCase() }
|
||||||
},
|
},
|
||||||
bindings: () => {
|
bindings: () => {
|
||||||
return {}
|
if (env.SELF_HOSTED) {
|
||||||
|
// "service" is being injected in datadog using the pod names,
|
||||||
|
// so we should leave it blank to allow the default behaviour if it's not running self-hosted
|
||||||
|
return {
|
||||||
|
service: env.SERVICE_NAME,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.isDev()) {
|
const destinations: pino.StreamEntry[] = []
|
||||||
pinoOptions.transport = {
|
|
||||||
target: "pino-pretty",
|
destinations.push(
|
||||||
options: {
|
env.isDev()
|
||||||
singleLine: true,
|
? {
|
||||||
},
|
stream: pinoPretty({ singleLine: true }),
|
||||||
}
|
level: level as pino.Level,
|
||||||
|
}
|
||||||
|
: { stream: process.stdout, level: level as pino.Level }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (env.SELF_HOSTED) {
|
||||||
|
destinations.push({
|
||||||
|
stream: localFileDestination(),
|
||||||
|
level: level as pino.Level,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pinoInstance = pino(pinoOptions)
|
pinoInstance = destinations.length
|
||||||
|
? pino(pinoOptions, pino.multistream(destinations))
|
||||||
|
: pino(pinoOptions)
|
||||||
|
|
||||||
// CONSOLE OVERRIDES
|
// CONSOLE OVERRIDES
|
||||||
|
|
||||||
|
@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
|
|
||||||
let contextObject = {}
|
let contextObject = {}
|
||||||
|
|
||||||
if (LOG_CONTEXT) {
|
contextObject = {
|
||||||
contextObject = {
|
tenantId: getTenantId(),
|
||||||
tenantId: getTenantId(),
|
appId: getAppId(),
|
||||||
appId: getAppId(),
|
automationId: getAutomationId(),
|
||||||
automationId: getAutomationId(),
|
identityId: identity?._id,
|
||||||
identityId: identity?._id,
|
identityType: identity?.type,
|
||||||
identityType: identity?.type,
|
correlationId: correlation.getId(),
|
||||||
correlationId: correlation.getId(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergingObject: any = {
|
const mergingObject: any = {
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import * as rfs from "rotating-file-stream"
|
||||||
|
|
||||||
|
import env from "../environment"
|
||||||
|
import { budibaseTempDir } from "../objectStore"
|
||||||
|
|
||||||
|
const logsFileName = `budibase.log`
|
||||||
|
const budibaseLogsHistoryFileName = "budibase-logs-history.txt"
|
||||||
|
|
||||||
|
const logsPath = path.join(budibaseTempDir(), "systemlogs")
|
||||||
|
|
||||||
|
function getFullPath(fileName: string) {
|
||||||
|
return path.join(logsPath, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSingleFileMaxSizeInfo(totalMaxSize: string) {
|
||||||
|
const regex = /(\d+)([A-Za-z])/
|
||||||
|
const match = totalMaxSize?.match(regex)
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`totalMaxSize does not have a valid value`, {
|
||||||
|
totalMaxSize,
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = +match[1]
|
||||||
|
const unit = match[2]
|
||||||
|
if (size === 1) {
|
||||||
|
switch (unit) {
|
||||||
|
case "B":
|
||||||
|
return { size: `${size}B`, totalHistoryFiles: 1 }
|
||||||
|
case "K":
|
||||||
|
return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 }
|
||||||
|
case "M":
|
||||||
|
return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 }
|
||||||
|
case "G":
|
||||||
|
return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 }
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size % 2 === 0) {
|
||||||
|
return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { size: `1${unit}`, totalHistoryFiles: size - 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localFileDestination() {
|
||||||
|
const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE)
|
||||||
|
const outFile = rfs.createStream(logsFileName, {
|
||||||
|
// As we have a rolling size, we want to half the max size
|
||||||
|
size: fileInfo?.size,
|
||||||
|
path: logsPath,
|
||||||
|
maxFiles: fileInfo?.totalHistoryFiles || 1,
|
||||||
|
immutable: true,
|
||||||
|
history: budibaseLogsHistoryFileName,
|
||||||
|
initialRotation: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return outFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogReadStream() {
|
||||||
|
const streams = []
|
||||||
|
const historyFile = getFullPath(budibaseLogsHistoryFileName)
|
||||||
|
if (fs.existsSync(historyFile)) {
|
||||||
|
const fileContent = fs.readFileSync(historyFile, "utf-8")
|
||||||
|
const historyFiles = fileContent.split("\n")
|
||||||
|
for (const historyFile of historyFiles.filter(x => x)) {
|
||||||
|
streams.push(fs.readFileSync(historyFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push(fs.readFileSync(getFullPath(logsFileName)))
|
||||||
|
|
||||||
|
const combinedContent = Buffer.concat(streams)
|
||||||
|
return combinedContent
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { getSingleFileMaxSizeInfo } from "../system"
|
||||||
|
|
||||||
|
describe("system", () => {
|
||||||
|
describe("getSingleFileMaxSizeInfo", () => {
|
||||||
|
it.each([
|
||||||
|
["100B", "50B"],
|
||||||
|
["200K", "100K"],
|
||||||
|
["20M", "10M"],
|
||||||
|
["4G", "2G"],
|
||||||
|
])(
|
||||||
|
"Halving even number (%s) returns halved size and 1 history file (%s)",
|
||||||
|
(totalValue, expectedMaxSize) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["5B", "1B", 4],
|
||||||
|
["17K", "1K", 16],
|
||||||
|
["21M", "1M", 20],
|
||||||
|
["3G", "1G", 2],
|
||||||
|
])(
|
||||||
|
"Halving an odd number (%s) returns as many files as size (-1) (%s)",
|
||||||
|
(totalValue, expectedMaxSize, totalHistoryFiles) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["1B", "1B"],
|
||||||
|
["1K", "500B"],
|
||||||
|
["1M", "500K"],
|
||||||
|
["1G", "500M"],
|
||||||
|
])(
|
||||||
|
"Halving '%s' returns halved unit (%s)",
|
||||||
|
(totalValue, expectedMaxSize) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([[undefined], [""], ["50"], ["wrongvalue"]])(
|
||||||
|
"Halving wrongly formatted value ('%s') returns undefined",
|
||||||
|
totalValue => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue!)
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
|
|
||||||
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
let user = await db.get(id)
|
let user = await db.get<User>(id)
|
||||||
if (opts?.cleanup) {
|
if (opts?.cleanup) {
|
||||||
user = removeUserPassword(user)
|
user = removeUserPassword(user) as User
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { db } from "../../../src"
|
||||||
|
|
||||||
export function expectFunctionWasCalledTimesWith(
|
export function expectFunctionWasCalledTimesWith(
|
||||||
jestFunction: any,
|
jestFunction: any,
|
||||||
times: number,
|
times: number,
|
||||||
|
@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith(
|
||||||
jestFunction.mock.calls.filter((call: any) => call[0] === argument).length
|
jestFunction.mock.calls.filter((call: any) => call[0] === argument).length
|
||||||
).toBe(times)
|
).toBe(times)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const expectAnyInternalColsAttributes: {
|
||||||
|
[K in (typeof db.CONSTANT_INTERNAL_ROW_COLS)[number]]: any
|
||||||
|
} = {
|
||||||
|
tableId: expect.anything(),
|
||||||
|
type: expect.anything(),
|
||||||
|
_id: expect.anything(),
|
||||||
|
_rev: expect.anything(),
|
||||||
|
createdAt: expect.anything(),
|
||||||
|
updatedAt: expect.anything(),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const expectAnyExternalColsAttributes: {
|
||||||
|
[K in (typeof db.CONSTANT_EXTERNAL_ROW_COLS)[number]]: any
|
||||||
|
} = {
|
||||||
|
tableId: expect.anything(),
|
||||||
|
_id: expect.anything(),
|
||||||
|
_rev: expect.anything(),
|
||||||
|
}
|
||||||
|
|
|
@ -96,7 +96,8 @@
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
"@budibase/string-templates"
|
"@budibase/string-templates",
|
||||||
|
"@budibase/shared-core"
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -17,65 +18,52 @@
|
||||||
export let newStyles = true
|
export let newStyles = true
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
let showTooltip = false
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<AbsTooltip text={tooltip}>
|
||||||
{id}
|
<button
|
||||||
{type}
|
{id}
|
||||||
class:spectrum-Button--cta={cta}
|
{type}
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--warning={warning}
|
class:spectrum-Button--secondary={secondary}
|
||||||
class:spectrum-Button--overBackground={overBackground}
|
class:spectrum-Button--warning={warning}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--overBackground={overBackground}
|
||||||
class:new-styles={newStyles}
|
class:spectrum-Button--quiet={quiet}
|
||||||
class:active
|
class:new-styles={newStyles}
|
||||||
class:disabled
|
class:active
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class:is-disabled={disabled}
|
||||||
{disabled}
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
on:click|preventDefault
|
on:click|preventDefault={() => {
|
||||||
on:mouseover={() => (showTooltip = true)}
|
if (!disabled) {
|
||||||
on:focus={() => (showTooltip = true)}
|
dispatch("click")
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
}
|
||||||
>
|
}}
|
||||||
{#if icon}
|
>
|
||||||
<svg
|
{#if icon}
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-label={icon}
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
{#if $$slots}
|
|
||||||
<span class="spectrum-Button-label"><slot /></span>
|
|
||||||
{/if}
|
|
||||||
{#if !disabled && tooltip}
|
|
||||||
<div class="tooltip-icon">
|
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-label="Info"
|
aria-label={icon}
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{#if $$slots}
|
||||||
{#if showTooltip && tooltip}
|
<span class="spectrum-Button-label"><slot /></span>
|
||||||
<div class="tooltip">
|
{/if}
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
</button>
|
||||||
</div>
|
</AbsTooltip>
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
button.is-disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -84,21 +72,6 @@
|
||||||
.active {
|
.active {
|
||||||
color: var(--spectrum-global-color-blue-600) !important;
|
color: var(--spectrum-global-color-blue-600) !important;
|
||||||
}
|
}
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
width: 160px;
|
|
||||||
text-align: center;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
left: 50%;
|
|
||||||
top: calc(100% - 3px);
|
|
||||||
}
|
|
||||||
.tooltip-icon {
|
|
||||||
padding-left: var(--spacing-m);
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
.spectrum-Button--primary.new-styles {
|
.spectrum-Button--primary.new-styles {
|
||||||
background: var(--spectrum-global-color-gray-800);
|
background: var(--spectrum-global-color-gray-800);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
@ -112,10 +85,10 @@
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
.spectrum-Button--secondary.new-styles:not(.disabled):hover {
|
.spectrum-Button--secondary.new-styles:not(.is-disabled):hover {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.spectrum-Button--secondary.new-styles.disabled {
|
.spectrum-Button--secondary.new-styles.is-disabled {
|
||||||
color: var(--spectrum-global-color-gray-500);
|
color: var(--spectrum-global-color-gray-500);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: placeholder = !value
|
|
||||||
|
|
||||||
const extractProperty = (value, property) => {
|
const extractProperty = (value, property) => {
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
return value[property]
|
return value[property]
|
||||||
|
|
|
@ -12,23 +12,24 @@
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
let tempValue = value
|
const optionValue = e.target.value
|
||||||
let isChecked = e.target.checked
|
if (e.target.checked && !value.includes(optionValue)) {
|
||||||
if (!tempValue.includes(e.target.value) && isChecked) {
|
dispatch("change", [...value, optionValue])
|
||||||
tempValue.push(e.target.value)
|
} else {
|
||||||
|
dispatch(
|
||||||
|
"change",
|
||||||
|
value.filter(x => x !== optionValue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
value = tempValue
|
|
||||||
dispatch(
|
|
||||||
"change",
|
|
||||||
tempValue.filter(val => val !== e.target.value || isChecked)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
||||||
{#if options && Array.isArray(options)}
|
{#if options && Array.isArray(options)}
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
|
{@const optionValue = getOptionValue(option)}
|
||||||
<div
|
<div
|
||||||
title={getOptionLabel(option)}
|
title={getOptionLabel(option)}
|
||||||
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
||||||
|
@ -39,11 +40,11 @@
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
value={getOptionValue(option)}
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="spectrum-Checkbox-input"
|
class="spectrum-Checkbox-input"
|
||||||
|
value={optionValue}
|
||||||
|
checked={value.includes(optionValue)}
|
||||||
{disabled}
|
{disabled}
|
||||||
checked={value.includes(getOptionValue(option))}
|
|
||||||
/>
|
/>
|
||||||
<span class="spectrum-Checkbox-box">
|
<span class="spectrum-Checkbox-box">
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if variables.length}
|
{:else if variables.length}
|
||||||
<div style="max-height: 100px">
|
<div style="max-height: 100px">
|
||||||
{#each variables as variable, idx}
|
{#each variables as variable}
|
||||||
<li
|
<li
|
||||||
class="spectrum-Menu-item"
|
class="spectrum-Menu-item"
|
||||||
role="option"
|
role="option"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/link/dist/index-vars.css"
|
import "@spectrum-css/link/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
export let href = "#"
|
export let href = "#"
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
@ -10,18 +11,61 @@
|
||||||
export let overBackground = false
|
export let overBackground = false
|
||||||
export let target
|
export let target
|
||||||
export let download
|
export let download
|
||||||
|
export let disabled = false
|
||||||
|
export let tooltip = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const onClick = e => {
|
||||||
|
if (!disabled) {
|
||||||
|
dispatch("click")
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
on:click={e => dispatch("click") && e.stopPropagation()}
|
on:click={onClick}
|
||||||
{href}
|
{href}
|
||||||
{target}
|
{target}
|
||||||
{download}
|
{download}
|
||||||
|
class:disabled
|
||||||
class:spectrum-Link--primary={primary}
|
class:spectrum-Link--primary={primary}
|
||||||
class:spectrum-Link--secondary={secondary}
|
class:spectrum-Link--secondary={secondary}
|
||||||
class:spectrum-Link--overBackground={overBackground}
|
class:spectrum-Link--overBackground={overBackground}
|
||||||
class:spectrum-Link--quiet={quiet}
|
class:spectrum-Link--quiet={quiet}
|
||||||
class="spectrum-Link spectrum-Link--size{size}"><slot /></a
|
class="spectrum-Link spectrum-Link--size{size}"
|
||||||
>
|
>
|
||||||
|
<slot />
|
||||||
|
{#if tooltip}
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
a.disabled {
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
a.disabled:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: 130ms ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
a:hover .tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
<script context="module">
|
||||||
|
export const keepOpen = Symbol("keepOpen")
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/dialog/dist/index-vars.css"
|
import "@spectrum-css/dialog/dist/index-vars.css"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
@ -30,7 +34,7 @@
|
||||||
|
|
||||||
async function secondary(e) {
|
async function secondary(e) {
|
||||||
loading = true
|
loading = true
|
||||||
if (!secondaryAction || (await secondaryAction(e)) !== false) {
|
if (!secondaryAction || (await secondaryAction(e)) !== keepOpen) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
|
@ -38,7 +42,7 @@
|
||||||
|
|
||||||
async function confirm() {
|
async function confirm() {
|
||||||
loading = true
|
loading = true
|
||||||
if (!onConfirm || (await onConfirm()) !== false) {
|
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
|
@ -46,7 +50,7 @@
|
||||||
|
|
||||||
async function close() {
|
async function close() {
|
||||||
loading = true
|
loading = true
|
||||||
if (!onCancel || (await onCancel()) !== false) {
|
if (!onCancel || (await onCancel()) !== keepOpen) {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
$: type = getType(schema)
|
$: type = getType(schema)
|
||||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||||
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
||||||
$: width = schema?.width || "150px"
|
|
||||||
$: cellValue = getCellValue(value, schema.template)
|
$: cellValue = getCellValue(value, schema.template)
|
||||||
|
|
||||||
const getType = schema => {
|
const getType = schema => {
|
||||||
|
|
|
@ -379,7 +379,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if sortedRows?.length}
|
{#if sortedRows?.length}
|
||||||
{#each sortedRows as row, idx}
|
{#each sortedRows as row}
|
||||||
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
|
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script context="module">
|
||||||
|
export const TooltipPosition = {
|
||||||
|
Top: "top",
|
||||||
|
Right: "right",
|
||||||
|
Bottom: "bottom",
|
||||||
|
Left: "left",
|
||||||
|
}
|
||||||
|
export const TooltipType = {
|
||||||
|
Default: "default",
|
||||||
|
Info: "info",
|
||||||
|
Positive: "positive",
|
||||||
|
Negative: "negative",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Portal from "svelte-portal"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import "@spectrum-css/tooltip/dist/index-vars.css"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
export let position = TooltipPosition.Top
|
||||||
|
export let type = TooltipType.Default
|
||||||
|
export let text = ""
|
||||||
|
export let fixed = false
|
||||||
|
export let color = null
|
||||||
|
|
||||||
|
let wrapper
|
||||||
|
let hovered = false
|
||||||
|
let left
|
||||||
|
let top
|
||||||
|
let visible = false
|
||||||
|
let timeout
|
||||||
|
let interval
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (hovered || fixed) {
|
||||||
|
// Debounce showing by 200ms to avoid flashing tooltip
|
||||||
|
timeout = setTimeout(show, 200)
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: tooltipStyle = color ? `background:${color};` : null
|
||||||
|
$: tipStyle = color ? `border-top-color:${color};` : null
|
||||||
|
|
||||||
|
// Computes the position of the tooltip
|
||||||
|
const updateTooltipPosition = () => {
|
||||||
|
const node = wrapper?.children?.[0]
|
||||||
|
if (!node) {
|
||||||
|
left = null
|
||||||
|
top = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const bounds = node.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Determine where to render tooltip based on position prop
|
||||||
|
if (position === TooltipPosition.Top) {
|
||||||
|
left = bounds.left + bounds.width / 2
|
||||||
|
top = bounds.top
|
||||||
|
} else if (position === TooltipPosition.Right) {
|
||||||
|
left = bounds.left + bounds.width
|
||||||
|
top = bounds.top + bounds.height / 2
|
||||||
|
} else if (position === TooltipPosition.Bottom) {
|
||||||
|
left = bounds.left + bounds.width / 2
|
||||||
|
top = bounds.top + bounds.height
|
||||||
|
} else if (position === TooltipPosition.Left) {
|
||||||
|
left = bounds.left
|
||||||
|
top = bounds.top + bounds.height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the position of the tooltip then shows it.
|
||||||
|
// We set up a poll to frequently update the position of the tooltip in case
|
||||||
|
// the target moves.
|
||||||
|
const show = () => {
|
||||||
|
updateTooltipPosition()
|
||||||
|
interval = setInterval(updateTooltipPosition, 100)
|
||||||
|
visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hides the tooltip
|
||||||
|
const hide = () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
clearInterval(interval)
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we clean up interval and timeout
|
||||||
|
onDestroy(hide)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={wrapper}
|
||||||
|
class="abs-tooltip"
|
||||||
|
on:focus={null}
|
||||||
|
on:mouseover={() => (hovered = true)}
|
||||||
|
on:mouseleave={() => (hovered = false)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if visible && text && left != null && top != null}
|
||||||
|
<Portal target=".spectrum">
|
||||||
|
<span
|
||||||
|
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
|
||||||
|
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
|
||||||
|
transition:fade|local={{ duration: 130 }}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Tooltip-label">{text}</span>
|
||||||
|
<span class="spectrum-Tooltip-tip" style={tipStyle} />
|
||||||
|
</span>
|
||||||
|
</Portal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.abs-tooltip {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 280px;
|
||||||
|
transition: top 130ms ease-out, left 130ms ease-out;
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colour overrides for default type */
|
||||||
|
.spectrum-Tooltip--default {
|
||||||
|
background: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--default .spectrum-Tooltip-tip {
|
||||||
|
border-top-color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position styles */
|
||||||
|
.spectrum-Tooltip--top {
|
||||||
|
transform: translateX(-50%) translateY(calc(-100% - 8px));
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--right {
|
||||||
|
transform: translateX(8px) translateY(-50%);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--bottom {
|
||||||
|
transform: translateX(-50%) translateY(8px);
|
||||||
|
}
|
||||||
|
.spectrum-Tooltip--left {
|
||||||
|
transform: translateX(calc(-100% - 8px)) translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import AbsTooltip from "./AbsTooltip.svelte"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
export let text = null
|
||||||
|
export let condition = true
|
||||||
|
export let duration = 3000
|
||||||
|
export let position
|
||||||
|
export let type
|
||||||
|
|
||||||
|
let visible = false
|
||||||
|
let timeout
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (condition) {
|
||||||
|
showTooltip()
|
||||||
|
} else {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTooltip = () => {
|
||||||
|
visible = true
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
visible = false
|
||||||
|
}, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltip = () => {
|
||||||
|
visible = false
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(hideTooltip)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
|
||||||
|
<slot />
|
||||||
|
</AbsTooltip>
|
|
@ -36,13 +36,19 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
||||||
export { default as Page } from "./Layout/Page.svelte"
|
export { default as Page } from "./Layout/Page.svelte"
|
||||||
export { default as Link } from "./Link/Link.svelte"
|
export { default as Link } from "./Link/Link.svelte"
|
||||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||||
|
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
|
||||||
|
export {
|
||||||
|
default as AbsTooltip,
|
||||||
|
TooltipPosition,
|
||||||
|
TooltipType,
|
||||||
|
} from "./Tooltip/AbsTooltip.svelte"
|
||||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||||
export { default as Menu } from "./Menu/Menu.svelte"
|
export { default as Menu } from "./Menu/Menu.svelte"
|
||||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||||
export { default as MenuItem } from "./Menu/Item.svelte"
|
export { default as MenuItem } from "./Menu/Item.svelte"
|
||||||
export { default as Modal } from "./Modal/Modal.svelte"
|
export { default as Modal } from "./Modal/Modal.svelte"
|
||||||
export { default as ModalContent } from "./Modal/ModalContent.svelte"
|
export { default as ModalContent, keepOpen } from "./Modal/ModalContent.svelte"
|
||||||
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
|
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
|
||||||
export { default as Notification } from "./Notification/Notification.svelte"
|
export { default as Notification } from "./Notification/Notification.svelte"
|
||||||
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
||||||
|
|
|
@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => {
|
||||||
readableBinding: `${table._instanceName}.Selected rows`,
|
readableBinding: `${table._instanceName}.Selected rows`,
|
||||||
category: "Selected rows",
|
category: "Selected rows",
|
||||||
icon: "ViewRow",
|
icon: "ViewRow",
|
||||||
|
display: { name: table._instanceName },
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => {
|
||||||
)}.${makePropSafe("selectedRows")}`,
|
)}.${makePropSafe("selectedRows")}`,
|
||||||
readableBinding: `${block._instanceName}.Selected rows`,
|
readableBinding: `${block._instanceName}.Selected rows`,
|
||||||
category: "Selected rows",
|
category: "Selected rows",
|
||||||
|
display: { name: block._instanceName },
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
||||||
import { getTemporalStore } from "./store/temporal"
|
import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { getUserStore } from "./store/users"
|
import { getUserStore } from "./store/users"
|
||||||
|
import { getDeploymentStore } from "./store/deployments"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
export const userStore = getUserStore()
|
export const userStore = getUserStore()
|
||||||
|
export const deploymentStore = getDeploymentStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
@ -118,3 +120,24 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||||
x => x._id === $automationStore.selectedAutomationId
|
x => x._id === $automationStore.selectedAutomationId
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derive map of resource IDs to other users.
|
||||||
|
// We only ever care about a single user in each resource, so if multiple users
|
||||||
|
// share the same datasource we can just overwrite them.
|
||||||
|
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
|
let map = {}
|
||||||
|
$userStore.forEach(user => {
|
||||||
|
const resource = user.builderMetadata?.selectedResourceId
|
||||||
|
if (resource) {
|
||||||
|
if (!map[resource]) {
|
||||||
|
map[resource] = []
|
||||||
|
}
|
||||||
|
map[resource].push(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
|
return $userStore.length < 2
|
||||||
|
})
|
||||||
|
|
|
@ -248,4 +248,36 @@ const automationActions = store => ({
|
||||||
}
|
}
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
|
replace: async (automationId, automation) => {
|
||||||
|
if (!automation) {
|
||||||
|
store.update(state => {
|
||||||
|
// Remove the automation
|
||||||
|
state.automations = state.automations.filter(
|
||||||
|
x => x._id !== automationId
|
||||||
|
)
|
||||||
|
// Select a new automation if required
|
||||||
|
if (automationId === state.selectedAutomationId) {
|
||||||
|
store.actions.select(state.automations[0]?._id)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const index = get(store).automations.findIndex(
|
||||||
|
x => x._id === automation._id
|
||||||
|
)
|
||||||
|
if (index === -1) {
|
||||||
|
// Automation addition
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
automations: [...state.automations, automation],
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Automation update
|
||||||
|
store.update(state => {
|
||||||
|
state.automations[index] = automation
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export const getDeploymentStore = () => {
|
||||||
|
let store = writable([])
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
store.set(await API.getAppDeployments())
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error fetching deployments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: {
|
||||||
|
load,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import {
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getComponentFieldOptions } from "helpers/formFields"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||||
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
initialised: false,
|
initialised: false,
|
||||||
|
@ -353,6 +354,33 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
return await sequentialScreenPatch(patchFn, screenId)
|
return await sequentialScreenPatch(patchFn, screenId)
|
||||||
},
|
},
|
||||||
|
replace: async (screenId, screen) => {
|
||||||
|
if (!screenId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!screen) {
|
||||||
|
// Screen deletion
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
screens: state.screens.filter(x => x._id !== screenId),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
const index = get(store).screens.findIndex(x => x._id === screen._id)
|
||||||
|
if (index === -1) {
|
||||||
|
// Screen addition
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
screens: [...state.screens, screen],
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Screen update
|
||||||
|
store.update(state => {
|
||||||
|
state.screens[index] = screen
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
delete: async screens => {
|
delete: async screens => {
|
||||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||||
|
|
||||||
|
@ -1305,7 +1333,7 @@ export const getFrontendStore = () => {
|
||||||
links: {
|
links: {
|
||||||
save: async (url, title) => {
|
save: async (url, title) => {
|
||||||
const navigation = get(store).navigation
|
const navigation = get(store).navigation
|
||||||
let links = [...navigation?.links]
|
let links = [...(navigation?.links ?? [])]
|
||||||
|
|
||||||
// Skip if we have an identical link
|
// Skip if we have an identical link
|
||||||
if (links.find(link => link.url === url && link.text === title)) {
|
if (links.find(link => link.url === url && link.text === title)) {
|
||||||
|
@ -1365,6 +1393,21 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
websocket: {
|
||||||
|
selectResource: id => {
|
||||||
|
websocket.emit(BuilderSocketEvent.SelectResource, {
|
||||||
|
resourceId: id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
replace: metadata => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
...metadata,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store
|
return store
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import { createWebsocket } from "@budibase/frontend-core"
|
import { createWebsocket } from "@budibase/frontend-core"
|
||||||
import { userStore, store } from "builderStore"
|
import {
|
||||||
|
userStore,
|
||||||
|
store,
|
||||||
|
deploymentStore,
|
||||||
|
automationStore,
|
||||||
|
} from "builderStore"
|
||||||
import { datasources, tables } from "stores/backend"
|
import { datasources, tables } from "stores/backend"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const createBuilderWebsocket = appId => {
|
export const createBuilderWebsocket = appId => {
|
||||||
const socket = createWebsocket("/socket/builder")
|
const socket = createWebsocket("/socket/builder")
|
||||||
|
@ -31,7 +38,6 @@ export const createBuilderWebsocket = appId => {
|
||||||
})
|
})
|
||||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||||
if (userId === get(auth)?.user?._id) {
|
if (userId === get(auth)?.user?._id) {
|
||||||
notifications.success("You can now edit screens and automations")
|
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
hasLock: true,
|
hasLock: true,
|
||||||
|
@ -39,15 +45,37 @@ export const createBuilderWebsocket = appId => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Table events
|
// Data section events
|
||||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||||
tables.replaceTable(id, table)
|
tables.replaceTable(id, table)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Datasource events
|
|
||||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||||
datasources.replaceDatasource(id, datasource)
|
datasources.replaceDatasource(id, datasource)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Design section events
|
||||||
|
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
||||||
|
store.actions.screens.replace(id, screen)
|
||||||
|
})
|
||||||
|
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
||||||
|
store.actions.metadata.replace(metadata)
|
||||||
|
})
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.AppPublishChange,
|
||||||
|
async ({ user, published }) => {
|
||||||
|
await apps.load()
|
||||||
|
if (published) {
|
||||||
|
await deploymentStore.actions.load()
|
||||||
|
}
|
||||||
|
const verb = published ? "published" : "unpublished"
|
||||||
|
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Automations
|
||||||
|
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||||
|
automationStore.actions.replace(id, automation)
|
||||||
|
})
|
||||||
|
|
||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Detail size="S">Plugins</Detail>
|
<Detail size="S">Plugins</Detail>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(plugins) as [idx, action]}
|
{#each Object.entries(plugins) as [_, action]}
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
class:selected={selectedAction === action.name}
|
class:selected={selectedAction === action.name}
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
{@html html}
|
{@html html}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import {
|
||||||
|
automationStore,
|
||||||
|
selectedAutomation,
|
||||||
|
userSelectedResourceMap,
|
||||||
|
} from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -21,13 +25,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="automations-list">
|
<div class="automations-list">
|
||||||
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
|
{#each $automationStore.automations.sort(aut => aut.name) as automation}
|
||||||
<NavItem
|
<NavItem
|
||||||
border={idx > 0}
|
|
||||||
icon="ShareAndroid"
|
icon="ShareAndroid"
|
||||||
text={automation.name}
|
text={automation.name}
|
||||||
selected={automation._id === selectedAutomationId}
|
selected={automation._id === selectedAutomationId}
|
||||||
on:click={() => selectAutomation(automation._id)}
|
on:click={() => selectAutomation(automation._id)}
|
||||||
|
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||||
>
|
>
|
||||||
<EditAutomationPopover {automation} />
|
<EditAutomationPopover {automation} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -40,6 +44,5 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
<Panel title="Automations" borderRight>
|
<Panel title="Automations" borderRight>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Button cta on:click={modal.show}>Add automation</Button>
|
<Button cta on:click={modal.show}>Add automation</Button>
|
||||||
<AutomationList />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<AutomationList />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label size="S">Trigger</Label>
|
<Label size="S">Trigger</Label>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each triggers as [idx, trigger]}
|
{#each triggers as [_, trigger]}
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
class:selected={selectedTrigger === trigger.name}
|
class:selected={selectedTrigger === trigger.name}
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
|
||||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
|
||||||
import {
|
import {
|
||||||
TableNames,
|
TableNames,
|
||||||
UNEDITABLE_USER_FIELDS,
|
UNEDITABLE_USER_FIELDS,
|
||||||
|
@ -33,7 +31,6 @@
|
||||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||||
$: isUsersTable = tableId === TableNames.USERS
|
$: isUsersTable = tableId === TableNames.USERS
|
||||||
$: data && resetSelectedRows()
|
$: data && resetSelectedRows()
|
||||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
|
||||||
$: {
|
$: {
|
||||||
UNSORTABLE_TYPES.forEach(type => {
|
UNSORTABLE_TYPES.forEach(type => {
|
||||||
Object.values(schema || {}).forEach(col => {
|
Object.values(schema || {}).forEach(col => {
|
||||||
|
@ -112,6 +109,7 @@
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
{customPlaceholder}
|
{customPlaceholder}
|
||||||
allowEditRows={allowEditing}
|
allowEditRows={allowEditing}
|
||||||
|
allowEditColumns={allowEditing}
|
||||||
showAutoColumns={!hideAutocolumns}
|
showAutoColumns={!hideAutocolumns}
|
||||||
{allowClickRows}
|
{allowClickRows}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
import {
|
import {
|
||||||
FIELDS,
|
FIELDS,
|
||||||
RelationshipTypes,
|
RelationshipType,
|
||||||
ALLOWABLE_STRING_OPTIONS,
|
ALLOWABLE_STRING_OPTIONS,
|
||||||
ALLOWABLE_NUMBER_OPTIONS,
|
ALLOWABLE_NUMBER_OPTIONS,
|
||||||
ALLOWABLE_STRING_TYPES,
|
ALLOWABLE_STRING_TYPES,
|
||||||
|
@ -58,7 +58,6 @@
|
||||||
|
|
||||||
let table = $tables.selected
|
let table = $tables.selected
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let deletion
|
|
||||||
let savingColumn
|
let savingColumn
|
||||||
let deleteColName
|
let deleteColName
|
||||||
let jsonSchemaModal
|
let jsonSchemaModal
|
||||||
|
@ -185,7 +184,7 @@
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
if (
|
if (
|
||||||
saveColumn.type === LINK_TYPE &&
|
saveColumn.type === LINK_TYPE &&
|
||||||
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
|
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
) {
|
) {
|
||||||
// Fetching the new tables
|
// Fetching the new tables
|
||||||
tables.fetch()
|
tables.fetch()
|
||||||
|
@ -216,7 +215,6 @@
|
||||||
notifications.success(`Column ${editableColumn.name} deleted`)
|
notifications.success(`Column ${editableColumn.name} deleted`)
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
hide()
|
hide()
|
||||||
deletion = false
|
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -240,7 +238,7 @@
|
||||||
|
|
||||||
// Default relationships many to many
|
// Default relationships many to many
|
||||||
if (editableColumn.type === LINK_TYPE) {
|
if (editableColumn.type === LINK_TYPE) {
|
||||||
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
}
|
}
|
||||||
if (editableColumn.type === FORMULA_TYPE) {
|
if (editableColumn.type === FORMULA_TYPE) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
|
@ -267,13 +265,11 @@
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
deletion = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideDeleteDialog() {
|
function hideDeleteDialog() {
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
deleteColName = ""
|
deleteColName = ""
|
||||||
deletion = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRelationshipOptions(field) {
|
function getRelationshipOptions(field) {
|
||||||
|
@ -290,17 +286,17 @@
|
||||||
{
|
{
|
||||||
name: `Many ${thisName} rows → many ${linkName} rows`,
|
name: `Many ${thisName} rows → many ${linkName} rows`,
|
||||||
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
value: RelationshipTypes.MANY_TO_MANY,
|
value: RelationshipType.MANY_TO_MANY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `One ${linkName} row → many ${thisName} rows`,
|
name: `One ${linkName} row → many ${thisName} rows`,
|
||||||
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
|
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
|
||||||
value: RelationshipTypes.ONE_TO_MANY,
|
value: RelationshipType.ONE_TO_MANY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `One ${thisName} row → many ${linkName} rows`,
|
name: `One ${thisName} row → many ${linkName} rows`,
|
||||||
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
value: RelationshipTypes.MANY_TO_ONE,
|
value: RelationshipType.MANY_TO_ONE,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
|
@ -41,8 +40,8 @@
|
||||||
} else {
|
} else {
|
||||||
notifications.error(`Failed to save row - ${error.message}`)
|
notifications.error(`Failed to save row - ${error.message}`)
|
||||||
}
|
}
|
||||||
// Prevent modal closing if there were errors
|
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ModalContent, Select, Link } from "@budibase/bbui"
|
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
errors = [...errors, { message: "Role is required" }]
|
errors = [...errors, { message: "Role is required" }]
|
||||||
}
|
}
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -79,8 +79,8 @@
|
||||||
} else {
|
} else {
|
||||||
notifications.error("Error saving user")
|
notifications.error("Error saving user")
|
||||||
}
|
}
|
||||||
// Prevent closing the modal on errors
|
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -95,9 +95,9 @@
|
||||||
{#if !creating}
|
{#if !creating}
|
||||||
<div>
|
<div>
|
||||||
A user's email, role, first and last names cannot be changed from within
|
A user's email, role, first and last names cannot be changed from within
|
||||||
the app builder. Please go to the <Link
|
the app builder. Please go to the
|
||||||
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
|
<Link on:click={$goto("/builder/portal/users/users")}>user portal</Link>
|
||||||
> to do this.
|
to do this.
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<RowFieldControl
|
<RowFieldControl
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
|
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
errors.push({ message: "Please choose permissions" })
|
errors.push({ message: "Please choose permissions" })
|
||||||
}
|
}
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save/create the role
|
// Save/create the role
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
notifications.success("Role saved successfully")
|
notifications.success("Role saved successfully")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error saving role - ${error.message}`)
|
notifications.error(`Error saving role - ${error.message}`)
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
} from "helpers/data/utils"
|
} from "helpers/data/utils"
|
||||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
|
|
||||||
|
@ -166,8 +167,9 @@
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === TableNames.USERS}
|
$tables.selected?._id === TableNames.USERS}
|
||||||
on:click={() => selectTable(TableNames.USERS)}
|
on:click={() => selectTable(TableNames.USERS)}
|
||||||
|
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||||
/>
|
/>
|
||||||
{#each enrichedDataSources as datasource, idx}
|
{#each enrichedDataSources as datasource}
|
||||||
<NavItem
|
<NavItem
|
||||||
border
|
border
|
||||||
text={datasource.name}
|
text={datasource.name}
|
||||||
|
@ -176,6 +178,7 @@
|
||||||
withArrow={true}
|
withArrow={true}
|
||||||
on:click={() => selectDatasource(datasource)}
|
on:click={() => selectDatasource(datasource)}
|
||||||
on:iconClick={() => toggleNode(datasource)}
|
on:iconClick={() => toggleNode(datasource)}
|
||||||
|
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||||
>
|
>
|
||||||
<div class="datasource-icon" slot="icon">
|
<div class="datasource-icon" slot="icon">
|
||||||
<IntegrationIcon
|
<IntegrationIcon
|
||||||
|
@ -201,6 +204,7 @@
|
||||||
selected={$isActive("./query/:queryId") &&
|
selected={$isActive("./query/:queryId") &&
|
||||||
$queries.selectedQueryId === query._id}
|
$queries.selectedQueryId === query._id}
|
||||||
on:click={() => $goto(`./query/${query._id}`)}
|
on:click={() => $goto(`./query/${query._id}`)}
|
||||||
|
selectedBy={$userSelectedResourceMap[query._id]}
|
||||||
>
|
>
|
||||||
<EditQueryPopover {query} />
|
<EditQueryPopover {query} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -212,7 +216,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hierarchy-items-container {
|
.hierarchy-items-container {
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
margin: 0 calc(-1 * var(--spacing-l));
|
||||||
}
|
}
|
||||||
.datasource-icon {
|
.datasource-icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -31,65 +31,65 @@
|
||||||
<path
|
<path
|
||||||
class="st1"
|
class="st1"
|
||||||
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
|
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
|
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
|
||||||
/>
|
/>
|
||||||
<g>
|
<g>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
class="st0"
|
class="st0"
|
||||||
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
||||||
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
class="st0"
|
class="st0"
|
||||||
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
|
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
|
||||||
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
||||||
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
||||||
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path
|
<path
|
||||||
class="st2"
|
class="st2"
|
||||||
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
|
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
|
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
|
||||||
/>
|
/>
|
||||||
<g>
|
<g>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
class="st1"
|
class="st1"
|
||||||
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
||||||
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
class="st1"
|
class="st1"
|
||||||
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
||||||
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
||||||
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
||||||
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
||||||
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
|
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
@ -102,24 +102,24 @@
|
||||||
<path
|
<path
|
||||||
class="st1"
|
class="st1"
|
||||||
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||||
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
||||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
class="st1"
|
class="st1"
|
||||||
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||||
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
||||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
|
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
|
keepOpen,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
|
@ -70,10 +71,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success(`Imported successfully.`)
|
notifications.success(`Imported successfully.`)
|
||||||
return true
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error importing queries")
|
notifications.error("Error importing queries")
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
keepOpen,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
let createVariableModal
|
let createVariableModal
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { RelationshipTypes } from "constants/backend"
|
import { RelationshipType } from "constants/backend"
|
||||||
import {
|
import {
|
||||||
|
keepOpen,
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
@ -24,11 +25,11 @@
|
||||||
const relationshipTypes = [
|
const relationshipTypes = [
|
||||||
{
|
{
|
||||||
label: "One to Many",
|
label: "One to Many",
|
||||||
value: RelationshipTypes.MANY_TO_ONE,
|
value: RelationshipType.MANY_TO_ONE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Many to Many",
|
label: "Many to Many",
|
||||||
value: RelationshipTypes.MANY_TO_MANY,
|
value: RelationshipType.MANY_TO_MANY,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -57,8 +58,8 @@
|
||||||
value: table._id,
|
value: table._id,
|
||||||
}))
|
}))
|
||||||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
||||||
|
|
||||||
function getTable(id) {
|
function getTable(id) {
|
||||||
return plusTables.find(table => table._id === id)
|
return plusTables.find(table => table._id === id)
|
||||||
|
@ -115,7 +116,7 @@
|
||||||
|
|
||||||
function allRequiredAttributesSet() {
|
function allRequiredAttributesSet() {
|
||||||
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
||||||
if (relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
||||||
return base && fromPrimary && fromForeign
|
return base && fromPrimary && fromForeign
|
||||||
} else {
|
} else {
|
||||||
return base && getTable(throughId) && throughFromKey && throughToKey
|
return base && getTable(throughId) && throughFromKey && throughToKey
|
||||||
|
@ -180,12 +181,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function otherRelationshipType(type) {
|
function otherRelationshipType(type) {
|
||||||
if (type === RelationshipTypes.MANY_TO_ONE) {
|
if (type === RelationshipType.MANY_TO_ONE) {
|
||||||
return RelationshipTypes.ONE_TO_MANY
|
return RelationshipType.ONE_TO_MANY
|
||||||
} else if (type === RelationshipTypes.ONE_TO_MANY) {
|
} else if (type === RelationshipType.ONE_TO_MANY) {
|
||||||
return RelationshipTypes.MANY_TO_ONE
|
return RelationshipType.MANY_TO_ONE
|
||||||
} else if (type === RelationshipTypes.MANY_TO_MANY) {
|
} else if (type === RelationshipType.MANY_TO_MANY) {
|
||||||
return RelationshipTypes.MANY_TO_MANY
|
return RelationshipType.MANY_TO_MANY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,7 +218,7 @@
|
||||||
|
|
||||||
// if any to many only need to check from
|
// if any to many only need to check from
|
||||||
const manyToMany =
|
const manyToMany =
|
||||||
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY
|
relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
|
|
||||||
if (!manyToMany) {
|
if (!manyToMany) {
|
||||||
delete relateFrom.through
|
delete relateFrom.through
|
||||||
|
@ -252,7 +253,7 @@
|
||||||
}
|
}
|
||||||
relateTo = {
|
relateTo = {
|
||||||
...relateTo,
|
...relateTo,
|
||||||
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
foreignKey: relateFrom.fieldName,
|
foreignKey: relateFrom.fieldName,
|
||||||
fieldName: fromPrimary,
|
fieldName: fromPrimary,
|
||||||
}
|
}
|
||||||
|
@ -277,7 +278,7 @@
|
||||||
|
|
||||||
async function saveRelationship() {
|
async function saveRelationship() {
|
||||||
if (!validate()) {
|
if (!validate()) {
|
||||||
return false
|
return keepOpen
|
||||||
}
|
}
|
||||||
buildRelationships()
|
buildRelationships()
|
||||||
removeExistingRelationship()
|
removeExistingRelationship()
|
||||||
|
@ -320,7 +321,7 @@
|
||||||
fromColumn = toRelationship.name
|
fromColumn = toRelationship.name
|
||||||
}
|
}
|
||||||
relationshipType =
|
relationshipType =
|
||||||
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
|
fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
|
||||||
if (selectedFromTable) {
|
if (selectedFromTable) {
|
||||||
fromId = selectedFromTable._id
|
fromId = selectedFromTable._id
|
||||||
fromColumn = selectedFromTable.name
|
fromColumn = selectedFromTable.name
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { derived, writable, get } from "svelte/store"
|
import { derived, writable, get } from "svelte/store"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { keepOpen, notifications } from "@budibase/bbui"
|
||||||
import { datasources, ImportTableError, tables } from "stores/backend"
|
import { datasources, ImportTableError, tables } from "stores/backend"
|
||||||
|
|
||||||
export const createTableSelectionStore = (integration, datasource) => {
|
export const createTableSelectionStore = (integration, datasource) => {
|
||||||
|
@ -36,8 +36,7 @@ export const createTableSelectionStore = (integration, datasource) => {
|
||||||
notifications.error("Error fetching tables.")
|
notifications.error("Error fetching tables.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent modal closing
|
return keepOpen
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RelationshipTypes } from "constants/backend"
|
import { RelationshipType } from "constants/backend"
|
||||||
|
|
||||||
const typeMismatch = "Column type of the foreign key must match the primary key"
|
const typeMismatch = "Column type of the foreign key must match the primary key"
|
||||||
const columnBeingUsed = "Column name cannot be an existing column"
|
const columnBeingUsed = "Column name cannot be an existing column"
|
||||||
|
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
isMany() {
|
isMany() {
|
||||||
return this.type === RelationshipTypes.MANY_TO_MANY
|
return this.type === RelationshipType.MANY_TO_MANY
|
||||||
}
|
}
|
||||||
|
|
||||||
relationshipTypeSet(type) {
|
relationshipTypeSet(type) {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
let error = null
|
let error = null
|
||||||
let fileName = null
|
let fileName = null
|
||||||
let fileType = null
|
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
let updateExistingRows = false
|
let updateExistingRows = false
|
||||||
|
@ -74,7 +73,6 @@
|
||||||
const response = await parseFile(e)
|
const response = await parseFile(e)
|
||||||
rows = response.rows
|
rows = response.rows
|
||||||
fileName = response.fileName
|
fileName = response.fileName
|
||||||
fileType = response.fileType
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
error = e
|
error = e
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
let fileInput
|
let fileInput
|
||||||
let error = null
|
let error = null
|
||||||
let fileName = null
|
let fileName = null
|
||||||
let fileType = null
|
|
||||||
let loading = false
|
let loading = false
|
||||||
let validation = {}
|
let validation = {}
|
||||||
let validateHash = ""
|
let validateHash = ""
|
||||||
|
@ -73,7 +72,6 @@
|
||||||
rows = response.rows
|
rows = response.rows
|
||||||
schema = response.schema
|
schema = response.schema
|
||||||
fileName = response.fileName
|
fileName = response.fileName
|
||||||
fileType = response.fileType
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
error = e
|
error = e
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
const alphabetical = (a, b) =>
|
const alphabetical = (a, b) =>
|
||||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === table._id}
|
$tables.selected?._id === table._id}
|
||||||
on:click={() => selectTable(table._id)}
|
on:click={() => selectTable(table._id)}
|
||||||
|
selectedBy={$userSelectedResourceMap[table._id]}
|
||||||
>
|
>
|
||||||
{#if table._id !== TableNames.USERS}
|
{#if table._id !== TableNames.USERS}
|
||||||
<EditTablePopover {table} />
|
<EditTablePopover {table} />
|
||||||
|
@ -42,6 +44,7 @@
|
||||||
text={viewName}
|
text={viewName}
|
||||||
selected={$isActive("./view") && $views.selected?.name === viewName}
|
selected={$isActive("./view") && $views.selected?.name === viewName}
|
||||||
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
||||||
|
selectedBy={$userSelectedResourceMap[viewName]}
|
||||||
>
|
>
|
||||||
<EditViewPopover
|
<EditViewPopover
|
||||||
view={{ name: viewName, ...table.views[viewName] }}
|
view={{ name: viewName, ...table.views[viewName] }}
|
||||||
|
|
|
@ -2,21 +2,13 @@
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
ModalContent,
|
|
||||||
Toggle,
|
|
||||||
Divider,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import {
|
import {
|
||||||
BUDIBASE_INTERNAL_DB_ID,
|
BUDIBASE_INTERNAL_DB_ID,
|
||||||
BUDIBASE_DATASOURCE_TYPE,
|
BUDIBASE_DATASOURCE_TYPE,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
|
||||||
|
|
||||||
$: tableNames = $tables.list.map(table => table.name)
|
$: tableNames = $tables.list.map(table => table.name)
|
||||||
$: selectedSource = $datasources.list.find(
|
$: selectedSource = $datasources.list.find(
|
||||||
|
@ -43,28 +35,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = ""
|
let error = ""
|
||||||
let autoColumns = getAutoColumnInformation()
|
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let rows = []
|
let rows = []
|
||||||
let allValid = true
|
let allValid = true
|
||||||
let displayColumn = null
|
let displayColumn = null
|
||||||
|
|
||||||
function getAutoColumns() {
|
|
||||||
const selectedAutoColumns = {}
|
|
||||||
|
|
||||||
Object.entries(autoColumns).forEach(([subtype, column]) => {
|
|
||||||
if (column.enabled) {
|
|
||||||
selectedAutoColumns[column.name] = buildAutoColumn(
|
|
||||||
name,
|
|
||||||
column.name,
|
|
||||||
subtype
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return selectedAutoColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValid(evt) {
|
function checkValid(evt) {
|
||||||
const tableName = evt.target.value
|
const tableName = evt.target.value
|
||||||
if (tableNames.includes(tableName)) {
|
if (tableNames.includes(tableName)) {
|
||||||
|
@ -77,7 +53,7 @@
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
let newTable = {
|
let newTable = {
|
||||||
name,
|
name,
|
||||||
schema: { ...schema, ...getAutoColumns() },
|
schema: { ...schema },
|
||||||
rows,
|
rows,
|
||||||
type: "internal",
|
type: "internal",
|
||||||
sourceId: targetDatasourceId,
|
sourceId: targetDatasourceId,
|
||||||
|
@ -118,21 +94,6 @@
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
<div class="autocolumns">
|
|
||||||
<Label extraSmall grey>Auto Columns</Label>
|
|
||||||
<div class="toggles">
|
|
||||||
<div class="toggle-1">
|
|
||||||
<Toggle text="Created by" bind:value={autoColumns.createdBy.enabled} />
|
|
||||||
<Toggle text="Created at" bind:value={autoColumns.createdAt.enabled} />
|
|
||||||
<Toggle text="Auto ID" bind:value={autoColumns.autoID.enabled} />
|
|
||||||
</div>
|
|
||||||
<div class="toggle-2">
|
|
||||||
<Toggle text="Updated by" bind:value={autoColumns.updatedBy.enabled} />
|
|
||||||
<Toggle text="Updated at" bind:value={autoColumns.updatedAt.enabled} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Label grey extraSmall
|
<Label grey extraSmall
|
||||||
|
@ -148,24 +109,3 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
|
||||||
.autocolumns {
|
|
||||||
margin-bottom: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggles {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-1 :global(> *) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-2 :global(> *) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -35,9 +35,8 @@
|
||||||
try {
|
try {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||||
const name = view.name
|
|
||||||
const id = view.tableId
|
const id = view.tableId
|
||||||
await views.delete(name)
|
await views.delete(view)
|
||||||
notifications.success("View deleted")
|
notifications.success("View deleted")
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
$goto(`./table/${id}`)
|
$goto(`./table/${id}`)
|
||||||
|
|
|
@ -93,42 +93,42 @@
|
||||||
`https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=`
|
`https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=`
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...$datasources?.list.map(datasource => ({
|
...($datasources?.list?.map(datasource => ({
|
||||||
type: "Datasource",
|
type: "Datasource",
|
||||||
name: `${datasource.name}`,
|
name: `${datasource.name}`,
|
||||||
icon: "Data",
|
icon: "Data",
|
||||||
action: () => $goto(`./data/datasource/${datasource._id}`),
|
action: () => $goto(`./data/datasource/${datasource._id}`),
|
||||||
})),
|
})) ?? []),
|
||||||
...$tables?.list.map(table => ({
|
...($tables?.list?.map(table => ({
|
||||||
type: "Table",
|
type: "Table",
|
||||||
name: table.name,
|
name: table.name,
|
||||||
icon: "Table",
|
icon: "Table",
|
||||||
action: () => $goto(`./data/table/${table._id}`),
|
action: () => $goto(`./data/table/${table._id}`),
|
||||||
})),
|
})) ?? []),
|
||||||
...$views?.list.map(view => ({
|
...($views?.list?.map(view => ({
|
||||||
type: "View",
|
type: "View",
|
||||||
name: view.name,
|
name: view.name,
|
||||||
icon: "Remove",
|
icon: "Remove",
|
||||||
action: () => $goto(`./data/view/${view.name}`),
|
action: () => $goto(`./data/view/${view.name}`),
|
||||||
})),
|
})) ?? []),
|
||||||
...$queries?.list.map(query => ({
|
...($queries?.list?.map(query => ({
|
||||||
type: "Query",
|
type: "Query",
|
||||||
name: query.name,
|
name: query.name,
|
||||||
icon: "SQLQuery",
|
icon: "SQLQuery",
|
||||||
action: () => $goto(`./data/query/${query._id}`),
|
action: () => $goto(`./data/query/${query._id}`),
|
||||||
})),
|
})) ?? []),
|
||||||
...$sortedScreens.map(screen => ({
|
...$sortedScreens.map(screen => ({
|
||||||
type: "Screen",
|
type: "Screen",
|
||||||
name: screen.routing.route,
|
name: screen.routing.route,
|
||||||
icon: "WebPage",
|
icon: "WebPage",
|
||||||
action: () => $goto(`./design/${screen._id}/components`),
|
action: () => $goto(`./design/${screen._id}/components`),
|
||||||
})),
|
})),
|
||||||
...$automationStore?.automations.map(automation => ({
|
...($automationStore?.automations?.map(automation => ({
|
||||||
type: "Automation",
|
type: "Automation",
|
||||||
name: automation.name,
|
name: automation.name,
|
||||||
icon: "ShareAndroid",
|
icon: "ShareAndroid",
|
||||||
action: () => $goto(`./automation/${automation._id}`),
|
action: () => $goto(`./automation/${automation._id}`),
|
||||||
})),
|
})) ?? []),
|
||||||
...Constants.Themes.map(theme => ({
|
...Constants.Themes.map(theme => ({
|
||||||
type: "Change Builder Theme",
|
type: "Change Builder Theme",
|
||||||
name: theme.name,
|
name: theme.name,
|
||||||
|
@ -208,8 +208,8 @@
|
||||||
|
|
||||||
async function deployApp() {
|
async function deployApp() {
|
||||||
try {
|
try {
|
||||||
await API.deployAppChanges()
|
await API.publishAppChanges($store.appId)
|
||||||
notifications.success("Application published successfully")
|
notifications.success("App published successfully")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error publishing app")
|
notifications.error("Error publishing app")
|
||||||
}
|
}
|
||||||
|
@ -237,11 +237,11 @@
|
||||||
<Input bind:value={search} quiet placeholder="Search for command" />
|
<Input bind:value={search} quiet placeholder="Search for command" />
|
||||||
</div>
|
</div>
|
||||||
<div class="commands">
|
<div class="commands">
|
||||||
{#each categories as [name, results], catIdx}
|
{#each categories as [name, results]}
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<Detail>{name}</Detail>
|
<Detail>{name}</Detail>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{#each results as command, cmdIdx}
|
{#each results as command}
|
||||||
<div
|
<div
|
||||||
class="command"
|
class="command"
|
||||||
on:click={() => runAction(command)}
|
on:click={() => runAction(command)}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
closeBrackets,
|
closeBrackets,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
closeBracketsKeymap,
|
closeBracketsKeymap,
|
||||||
|
acceptCompletion,
|
||||||
|
completionStatus,
|
||||||
} from "@codemirror/autocomplete"
|
} from "@codemirror/autocomplete"
|
||||||
import {
|
import {
|
||||||
EditorView,
|
EditorView,
|
||||||
|
@ -35,7 +37,8 @@
|
||||||
defaultKeymap,
|
defaultKeymap,
|
||||||
historyKeymap,
|
historyKeymap,
|
||||||
history,
|
history,
|
||||||
indentWithTab,
|
indentMore,
|
||||||
|
indentLess,
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
import { Compartment } from "@codemirror/state"
|
import { Compartment } from "@codemirror/state"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
|
@ -109,6 +112,22 @@
|
||||||
let isDark = !currentTheme.includes("light")
|
let isDark = !currentTheme.includes("light")
|
||||||
let themeConfig = new Compartment()
|
let themeConfig = new Compartment()
|
||||||
|
|
||||||
|
const indentWithTabCustom = {
|
||||||
|
key: "Tab",
|
||||||
|
run: view => {
|
||||||
|
if (completionStatus(view.state) == "active") {
|
||||||
|
acceptCompletion(view)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
indentMore(view)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
shift: view => {
|
||||||
|
indentLess(view)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const buildKeymap = () => {
|
const buildKeymap = () => {
|
||||||
const baseMap = [
|
const baseMap = [
|
||||||
...closeBracketsKeymap,
|
...closeBracketsKeymap,
|
||||||
|
@ -116,7 +135,7 @@
|
||||||
...historyKeymap,
|
...historyKeymap,
|
||||||
...foldKeymap,
|
...foldKeymap,
|
||||||
...completionKeymap,
|
...completionKeymap,
|
||||||
indentWithTab,
|
indentWithTabCustom,
|
||||||
]
|
]
|
||||||
return baseMap
|
return baseMap
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Heading } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let showClose = false
|
||||||
|
export let onClose = () => {}
|
||||||
|
export let heading = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="page">
|
||||||
|
<div class="closeButton">
|
||||||
|
{#if showClose}
|
||||||
|
<Icon hoverable name="Close" on:click={onClose} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="heading">
|
||||||
|
<Heading weight="light">{heading}</Heading>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -20,4 +20,5 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
{@html substituteSize(svgHtml)}
|
{@html substituteSize(svgHtml)}
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
import { licensing } from "stores/portal"
|
import { licensing } from "stores/portal"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
|
|
||||||
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan
|
$: isBusinessAndAbove =
|
||||||
|
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
|
||||||
|
|
||||||
let show
|
let show
|
||||||
let hide
|
let hide
|
||||||
|
@ -55,22 +56,22 @@
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
||||||
<a
|
<a
|
||||||
href={isPremiumUser
|
href={isBusinessAndAbove
|
||||||
? "mailto:support@budibase.com"
|
? "mailto:support@budibase.com"
|
||||||
: "/builder/portal/account/usage"}
|
: "/builder/portal/account/usage"}
|
||||||
>
|
>
|
||||||
<div class="premiumLinkContent" class:disabled={!isPremiumUser}>
|
<div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">Email support</Body>
|
<Body size="S">Email support</Body>
|
||||||
</div>
|
</div>
|
||||||
{#if !isPremiumUser}
|
{#if !isBusinessAndAbove}
|
||||||
<div class="premiumBadge">
|
<div class="premiumBadge">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||||
</div>
|
</div>
|
||||||
<Body size="XS">Premium</Body>
|
<Body size="XS">Business</Body>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
|
@ -18,12 +20,15 @@
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
export let id
|
export let id
|
||||||
export let showTooltip = false
|
export let showTooltip = false
|
||||||
|
export let selectedBy = null
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let contentRef
|
let contentRef
|
||||||
|
|
||||||
$: selected && contentRef && scrollToView()
|
$: selected && contentRef && scrollToView()
|
||||||
|
$: style = getStyle(indentLevel, selectedBy)
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
scrollToView()
|
scrollToView()
|
||||||
|
@ -42,6 +47,14 @@
|
||||||
const bounds = contentRef.getBoundingClientRect()
|
const bounds = contentRef.getBoundingClientRect()
|
||||||
scrollApi.scrollTo(bounds)
|
scrollApi.scrollTo(bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyle = (indentLevel, selectedBy) => {
|
||||||
|
let style = `padding-left:calc(${indentLevel * 14}px);`
|
||||||
|
if (selectedBy) {
|
||||||
|
style += `--selected-by-color:${helpers.getUserColor(selectedBy)};`
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -51,8 +64,7 @@
|
||||||
class:withActions
|
class:withActions
|
||||||
class:scrollable
|
class:scrollable
|
||||||
class:highlighted
|
class:highlighted
|
||||||
style={`padding-left: calc(${indentLevel * 14}px)`}
|
class:selectedBy
|
||||||
{draggable}
|
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
|
@ -61,6 +73,8 @@
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
{id}
|
{id}
|
||||||
|
{style}
|
||||||
|
{draggable}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
|
@ -85,12 +99,19 @@
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
<div class="text" title={showTooltip ? text : null}>
|
||||||
|
{text}
|
||||||
|
{#if selectedBy}
|
||||||
|
<UserAvatars size="XS" users={selectedBy} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if withActions}
|
{#if withActions}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $$slots.right}
|
{#if $$slots.right}
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<slot name="right" />
|
<slot name="right" />
|
||||||
|
@ -119,13 +140,16 @@
|
||||||
}
|
}
|
||||||
.nav-item.highlighted {
|
.nav-item.highlighted {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||||
}
|
}
|
||||||
.nav-item.selected {
|
.nav-item.selected {
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.nav-item:hover .actions {
|
.nav-item:hover .actions {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
@ -197,6 +221,9 @@
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
order: 2;
|
order: 2;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.scrollable .text {
|
.scrollable .text {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
@ -208,7 +208,9 @@
|
||||||
<div class="syntax-error">
|
<div class="syntax-error">
|
||||||
Current Handlebars syntax is invalid, please check the
|
Current Handlebars syntax is invalid, please check the
|
||||||
guide
|
guide
|
||||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
<a href="https://handlebarsjs.com/guide/" target="_blank"
|
||||||
|
>here</a
|
||||||
|
>
|
||||||
for more details.
|
for more details.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if hoverTarget.description}
|
{#if hoverTarget.description}
|
||||||
<div class="helper__description">
|
<div class="helper__description">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
{@html hoverTarget.description}
|
{@html hoverTarget.description}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -124,7 +125,6 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<span
|
<span
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -162,7 +162,6 @@
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{#each category.bindings as binding}
|
{#each category.bindings as binding}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<li
|
<li
|
||||||
class="binding"
|
class="binding"
|
||||||
on:mouseenter={e => {
|
on:mouseenter={e => {
|
||||||
|
|
|
@ -10,19 +10,17 @@
|
||||||
Link,
|
Link,
|
||||||
Modal,
|
Modal,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { store } from "builderStore"
|
import { deploymentStore, store, isOnlyUser } from "builderStore"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
@ -34,37 +32,31 @@
|
||||||
let updateAppModal
|
let updateAppModal
|
||||||
let revertModal
|
let revertModal
|
||||||
let versionModal
|
let versionModal
|
||||||
|
|
||||||
let appActionPopover
|
let appActionPopover
|
||||||
let appActionPopoverOpen = false
|
let appActionPopoverOpen = false
|
||||||
let appActionPopoverAnchor
|
let appActionPopoverAnchor
|
||||||
|
|
||||||
let publishing = false
|
let publishing = false
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
$: latestDeployments = $deploymentStore
|
||||||
$: deployments = []
|
|
||||||
$: latestDeployments = deployments
|
|
||||||
.filter(deployment => deployment.status === "SUCCESS")
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
$: isPublished =
|
$: isPublished =
|
||||||
selectedApp?.status === "published" && latestDeployments?.length > 0
|
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||||
|
|
||||||
$: updateAvailable =
|
$: updateAvailable =
|
||||||
$store.upgradableVersion &&
|
$store.upgradableVersion &&
|
||||||
$store.version &&
|
$store.version &&
|
||||||
$store.upgradableVersion !== $store.version
|
$store.upgradableVersion !== $store.version
|
||||||
|
|
||||||
$: canPublish = !publishing && loaded
|
$: canPublish = !publishing && loaded
|
||||||
|
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
const initialiseApp = async () => {
|
||||||
const applicationPkg = await API.fetchAppPackage($store.devId)
|
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||||
await store.actions.initialise(applicationPkg)
|
await store.actions.initialise(applicationPkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDeploymentString = () => {
|
const getLastDeployedString = deployments => {
|
||||||
return deployments?.length
|
return deployments?.length
|
||||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
time:
|
time:
|
||||||
|
@ -73,27 +65,6 @@
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
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 overview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewApp = () => {
|
const previewApp = () => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -116,14 +87,11 @@
|
||||||
async function publishApp() {
|
async function publishApp() {
|
||||||
try {
|
try {
|
||||||
publishing = true
|
publishing = true
|
||||||
|
|
||||||
await API.publishAppChanges($store.appId)
|
await API.publishAppChanges($store.appId)
|
||||||
|
notifications.send("App published successfully", {
|
||||||
notifications.send("App published", {
|
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "GlobeCheck",
|
icon: "GlobeCheck",
|
||||||
})
|
})
|
||||||
|
|
||||||
await completePublish()
|
await completePublish()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -163,210 +131,201 @@
|
||||||
const completePublish = async () => {
|
const completePublish = async () => {
|
||||||
try {
|
try {
|
||||||
await apps.load()
|
await apps.load()
|
||||||
deployments = await fetchDeployments()
|
await deploymentStore.actions.load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error refreshing app")
|
notifications.error("Error refreshing app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!$apps.length) {
|
|
||||||
await apps.load()
|
|
||||||
}
|
|
||||||
deployments = await fetchDeployments()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $store.hasLock}
|
<div class="action-top-nav">
|
||||||
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
<div class="action-buttons">
|
||||||
<div class="action-buttons">
|
{#if updateAvailable && $isOnlyUser}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<div class="app-action-button version" on:click={versionModal.show}>
|
||||||
{#if updateAvailable}
|
|
||||||
<div class="app-action-button version" on:click={versionModal.show}>
|
|
||||||
<div class="app-action">
|
|
||||||
<ActionButton quiet>
|
|
||||||
<StatusLight notice />
|
|
||||||
Update
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<TourWrap
|
|
||||||
tourStepKey={$store.onboarding
|
|
||||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
|
||||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
|
||||||
>
|
|
||||||
<div class="app-action-button users">
|
|
||||||
<div class="app-action" id="builder-app-users-button">
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="UserGroup"
|
|
||||||
on:click={() => {
|
|
||||||
store.update(state => {
|
|
||||||
state.builderSidePanel = true
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TourWrap>
|
|
||||||
|
|
||||||
<div class="app-action-button preview">
|
|
||||||
<div class="app-action">
|
<div class="app-action">
|
||||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
<ActionButton quiet>
|
||||||
Preview
|
<StatusLight notice />
|
||||||
|
Update
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<TourWrap
|
||||||
<div
|
tourStepKey={$store.onboarding
|
||||||
class="app-action-button publish app-action-popover"
|
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||||
on:click={() => {
|
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||||
if (!appActionPopoverOpen) {
|
>
|
||||||
appActionPopover.show()
|
<div class="app-action-button users">
|
||||||
} else {
|
<div class="app-action" id="builder-app-users-button">
|
||||||
appActionPopover.hide()
|
<ActionButton
|
||||||
}
|
quiet
|
||||||
}}
|
icon="UserGroup"
|
||||||
>
|
on:click={() => {
|
||||||
<div bind:this={appActionPopoverAnchor}>
|
store.update(state => {
|
||||||
<div class="app-action">
|
state.builderSidePanel = true
|
||||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
return state
|
||||||
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
})
|
||||||
<span class="publish-open" id="builder-app-publish-button">
|
}}
|
||||||
Publish
|
>
|
||||||
<Icon
|
Users
|
||||||
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
</ActionButton>
|
||||||
size="M"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TourWrap>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
</div>
|
||||||
bind:this={appActionPopover}
|
</TourWrap>
|
||||||
align="right"
|
|
||||||
disabled={!isPublished}
|
|
||||||
anchor={appActionPopoverAnchor}
|
|
||||||
offset={35}
|
|
||||||
on:close={() => {
|
|
||||||
appActionPopoverOpen = false
|
|
||||||
}}
|
|
||||||
on:open={() => {
|
|
||||||
appActionPopoverOpen = true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="app-action-popover-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<Body size="M">
|
|
||||||
<span
|
|
||||||
class="app-link"
|
|
||||||
on:click={() => {
|
|
||||||
if (isPublished) {
|
|
||||||
viewApp()
|
|
||||||
} else {
|
|
||||||
appActionPopover.hide()
|
|
||||||
updateAppModal.show()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$store.url}
|
|
||||||
{#if isPublished}
|
|
||||||
<Icon size="S" name="LinkOut" />
|
|
||||||
{:else}
|
|
||||||
<Icon size="S" name="Edit" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<Body size="S">
|
<div class="app-action-button preview">
|
||||||
<span class="publish-popover-status">
|
<div class="app-action">
|
||||||
{#if isPublished}
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
<span class="status-text">
|
Preview
|
||||||
{updateDeploymentString(deployments)}
|
</ActionButton>
|
||||||
</span>
|
|
||||||
<span class="unpublish-link">
|
|
||||||
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
|
||||||
</span>
|
|
||||||
<span class="revert-link">
|
|
||||||
<Link quiet secondary on:click={revertApp}>Revert</Link>
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-text unpublished">Not published</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="action-buttons">
|
|
||||||
{#if $store.hasLock}
|
|
||||||
{#if isPublished}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Code"
|
|
||||||
on:click={() => {
|
|
||||||
$goto("./settings/embed")
|
|
||||||
appActionPopover.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Embed
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
on:click={publishApp}
|
|
||||||
id={"builder-app-publish-button"}
|
|
||||||
disabled={!canPublish}
|
|
||||||
>
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modals -->
|
<div
|
||||||
<ConfirmDialog
|
class="app-action-button publish app-action-popover"
|
||||||
bind:this={unpublishModal}
|
on:click={() => {
|
||||||
title="Confirm unpublish"
|
if (!appActionPopoverOpen) {
|
||||||
okText="Unpublish app"
|
appActionPopover.show()
|
||||||
onOk={confirmUnpublishApp}
|
} else {
|
||||||
>
|
appActionPopover.hide()
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
}
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $store.name,
|
|
||||||
url: $store.url,
|
|
||||||
icon: $store.icon,
|
|
||||||
appId: $store.appId,
|
|
||||||
}}
|
}}
|
||||||
onUpdateComplete={async () => {
|
>
|
||||||
await initialiseApp()
|
<div bind:this={appActionPopoverAnchor}>
|
||||||
}}
|
<div class="app-action">
|
||||||
/>
|
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||||
</Modal>
|
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||||
|
<span class="publish-open" id="builder-app-publish-button">
|
||||||
|
Publish
|
||||||
|
<Icon
|
||||||
|
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
||||||
|
size="M"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</TourWrap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={appActionPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
anchor={appActionPopoverAnchor}
|
||||||
|
offset={35}
|
||||||
|
on:close={() => {
|
||||||
|
appActionPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
appActionPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="app-action-popover-content">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<Body size="M">
|
||||||
|
<span
|
||||||
|
class="app-link"
|
||||||
|
on:click={() => {
|
||||||
|
if (isPublished) {
|
||||||
|
viewApp()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
updateAppModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$store.url}
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon size="S" name="LinkOut" />
|
||||||
|
{:else}
|
||||||
|
<Icon size="S" name="Edit" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
|
||||||
<RevertModal bind:this={revertModal} />
|
<Body size="S">
|
||||||
<VersionModal hideIcon bind:this={versionModal} />
|
<span class="publish-popover-status">
|
||||||
{:else}
|
{#if isPublished}
|
||||||
<div class="app-action-button preview-locked">
|
<span class="status-text">
|
||||||
<div class="app-action">
|
{lastDeployed}
|
||||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
</span>
|
||||||
Preview
|
<span class="unpublish-link">
|
||||||
</ActionButton>
|
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||||
|
</span>
|
||||||
|
<span class="revert-link">
|
||||||
|
<AbsTooltip
|
||||||
|
text={$isOnlyUser
|
||||||
|
? null
|
||||||
|
: "Unavailable - another user is editing this app"}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={revertApp}
|
||||||
|
>
|
||||||
|
Revert
|
||||||
|
</Link>
|
||||||
|
</AbsTooltip>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="status-text unpublished">Not published</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="action-buttons">
|
||||||
|
{#if isPublished}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Code"
|
||||||
|
on:click={() => {
|
||||||
|
$goto("./settings/embed")
|
||||||
|
appActionPopover.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Embed
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={publishApp}
|
||||||
|
id={"builder-app-publish-button"}
|
||||||
|
disabled={!canPublish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal
|
||||||
|
app={{
|
||||||
|
name: $store.name,
|
||||||
|
url: $store.url,
|
||||||
|
icon: $store.icon,
|
||||||
|
appId: $store.appId,
|
||||||
|
}}
|
||||||
|
onUpdateComplete={async () => {
|
||||||
|
await initialiseApp()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<RevertModal bind:this={revertModal} />
|
||||||
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-action-popover-content {
|
.app-action-popover-content {
|
||||||
|
@ -450,10 +409,6 @@
|
||||||
gap: var(--spectrum-actionbutton-icon-gap);
|
gap: var(--spectrum-actionbutton-icon-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-action-button.preview-locked {
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-action {
|
.app-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
ProgressCircle,
|
|
||||||
CopyInput,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { API } from "api"
|
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import TourWrap from "../portal/onboarding/TourWrap.svelte"
|
|
||||||
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
|
|
||||||
|
|
||||||
let publishModal
|
|
||||||
let asyncModal
|
|
||||||
let publishCompleteModal
|
|
||||||
|
|
||||||
let published
|
|
||||||
|
|
||||||
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
|
|
||||||
|
|
||||||
export let onOk
|
|
||||||
|
|
||||||
async function publishApp() {
|
|
||||||
try {
|
|
||||||
//In Progress
|
|
||||||
asyncModal.show()
|
|
||||||
publishModal.hide()
|
|
||||||
|
|
||||||
published = await API.publishAppChanges($store.appId)
|
|
||||||
|
|
||||||
if (typeof onOk === "function") {
|
|
||||||
await onOk()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Request completed
|
|
||||||
asyncModal.hide()
|
|
||||||
publishCompleteModal.show()
|
|
||||||
} catch (error) {
|
|
||||||
analytics.captureException(error)
|
|
||||||
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>
|
|
||||||
|
|
||||||
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
|
||||||
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
</TourWrap>
|
|
||||||
<Modal bind:this={publishModal}>
|
|
||||||
<ModalContent
|
|
||||||
title="Publish to production"
|
|
||||||
confirmText="Publish"
|
|
||||||
onConfirm={publishApp}
|
|
||||||
>
|
|
||||||
The changes you have made will be published to the production version of the
|
|
||||||
application.
|
|
||||||
</ModalContent>
|
|
||||||
</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 -->
|
|
||||||
<Modal bind:this={publishCompleteModal}>
|
|
||||||
<ModalContent confirmText="Done" cancelText="View App" onCancel={viewApp}>
|
|
||||||
<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:" />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<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>
|
|
|
@ -1,236 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount, onDestroy } from "svelte"
|
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
|
||||||
import { slide } from "svelte/transition"
|
|
||||||
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
|
|
||||||
import { API } from "api"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import {
|
|
||||||
checkIncomingDeploymentStatus,
|
|
||||||
DeploymentStatus,
|
|
||||||
} from "components/deploy/utils"
|
|
||||||
|
|
||||||
const DATE_OPTIONS = {
|
|
||||||
fullDate: {
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
},
|
|
||||||
timeOnly: {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "numeric",
|
|
||||||
hourCycle: "h12",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const POLL_INTERVAL = 5000
|
|
||||||
|
|
||||||
export let appId
|
|
||||||
|
|
||||||
let modal
|
|
||||||
let errorReasonModal
|
|
||||||
let errorReason
|
|
||||||
let poll
|
|
||||||
let deployments = []
|
|
||||||
let urlComponent = $store.url || `/${appId}`
|
|
||||||
let deploymentUrl = `${urlComponent}`
|
|
||||||
|
|
||||||
const formatDate = (date, format) =>
|
|
||||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
|
||||||
|
|
||||||
async function fetchDeployments() {
|
|
||||||
try {
|
|
||||||
const newDeployments = await API.getAppDeployments()
|
|
||||||
if (deployments.length > 0) {
|
|
||||||
const pendingDeployments = checkIncomingDeploymentStatus(
|
|
||||||
deployments,
|
|
||||||
newDeployments
|
|
||||||
)
|
|
||||||
if (pendingDeployments.length) {
|
|
||||||
showErrorReasonModal(pendingDeployments[0].err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deployments = newDeployments
|
|
||||||
} catch (err) {
|
|
||||||
clearInterval(poll)
|
|
||||||
notifications.error("Error fetching deployment overview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showErrorReasonModal(err) {
|
|
||||||
if (!err) return
|
|
||||||
errorReason = err
|
|
||||||
errorReasonModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchDeployments()
|
|
||||||
poll = setInterval(fetchDeployments, POLL_INTERVAL)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => clearInterval(poll))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if deployments.length > 0}
|
|
||||||
<section class="deployment-history" in:slide>
|
|
||||||
<header>
|
|
||||||
<Heading>Deployment History</Heading>
|
|
||||||
<div class="deploy-div">
|
|
||||||
{#if deployments.some(deployment => deployment.status === DeploymentStatus.SUCCESS)}
|
|
||||||
<a target="_blank" href={deploymentUrl}> View Your Deployed App → </a>
|
|
||||||
<Button primary on:click={() => modal.show()}>View webhooks</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="deployment-list">
|
|
||||||
{#each deployments as deployment}
|
|
||||||
<article class="deployment">
|
|
||||||
<div class="deployment-info">
|
|
||||||
<span class="deploy-date">
|
|
||||||
{formatDate(deployment.updatedAt, "fullDate")}
|
|
||||||
</span>
|
|
||||||
<span class="deploy-time">
|
|
||||||
{formatDate(deployment.updatedAt, "timeOnly")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="deployment-right">
|
|
||||||
{#if deployment.status.toLowerCase() === "pending"}
|
|
||||||
<Spinner size="10" />
|
|
||||||
{/if}
|
|
||||||
<div
|
|
||||||
on:click={() => showErrorReasonModal(deployment.err)}
|
|
||||||
class={`deployment-status ${deployment.status}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{deployment.status}
|
|
||||||
{#if deployment.status === DeploymentStatus.FAILURE}
|
|
||||||
<i class="ri-information-line" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
<Modal bind:this={modal} width="30%">
|
|
||||||
<CreateWebhookDeploymentModal />
|
|
||||||
</Modal>
|
|
||||||
<Modal bind:this={errorReasonModal} width="30%">
|
|
||||||
<ModalContent
|
|
||||||
title="Deployment Error"
|
|
||||||
confirmText="OK"
|
|
||||||
showCancelButton={false}
|
|
||||||
>
|
|
||||||
{errorReason}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
section {
|
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-list {
|
|
||||||
height: 40vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding-left: var(--spacing-l);
|
|
||||||
padding-bottom: var(--spacing-xl);
|
|
||||||
padding-right: var(--spacing-l);
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-history {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment {
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
}
|
|
||||||
.deployment:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-date {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-time {
|
|
||||||
color: var(--grey-7);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-status {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-s);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: lowercase;
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.deployment-status:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--blue);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.SUCCESS {
|
|
||||||
color: var(--green);
|
|
||||||
background: var(--green-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PENDING {
|
|
||||||
color: var(--yellow);
|
|
||||||
background: var(--yellow-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.FAILURE {
|
|
||||||
color: var(--red);
|
|
||||||
background: var(--red-light);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,25 +0,0 @@
|
||||||
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
|
|
||||||
}, [])
|
|
||||||
}
|
|
|
@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
|
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableInput,
|
text: DrawerBindableInput,
|
||||||
|
@ -44,6 +45,7 @@ const componentMap = {
|
||||||
schema: SchemaSelect,
|
schema: SchemaSelect,
|
||||||
section: SectionSelect,
|
section: SectionSelect,
|
||||||
filter: FilterEditor,
|
filter: FilterEditor,
|
||||||
|
"filter/relationship": RelationshipFilterEditor,
|
||||||
url: URLSelect,
|
url: URLSelect,
|
||||||
fieldConfiguration: FieldConfiguration,
|
fieldConfiguration: FieldConfiguration,
|
||||||
columns: ColumnEditor,
|
columns: ColumnEditor,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
makeStateBinding,
|
makeStateBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||||
|
@ -29,6 +30,26 @@
|
||||||
let actionQuery
|
let actionQuery
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
|
||||||
|
const setUpdateActions = actions => {
|
||||||
|
return actions
|
||||||
|
? cloneDeep(actions)
|
||||||
|
.filter(action => {
|
||||||
|
return (
|
||||||
|
action[EVENT_TYPE_KEY] === "Update State" &&
|
||||||
|
action.parameters?.type === "set" &&
|
||||||
|
action.parameters.key
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.reduce((acc, action) => {
|
||||||
|
acc[action.id] = action
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot original action state
|
||||||
|
let updateStateActions = setUpdateActions(actions)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Ensure parameters object is never null
|
// Ensure parameters object is never null
|
||||||
if (selectedAction && !selectedAction.parameters) {
|
if (selectedAction && !selectedAction.parameters) {
|
||||||
|
@ -125,8 +146,9 @@
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||||
let allBindings = []
|
let allBindings = []
|
||||||
|
let cloneActionBindings = cloneDeep(actionBindings)
|
||||||
if (!actions) {
|
if (!actions) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -144,11 +166,19 @@
|
||||||
.forEach(action => {
|
.forEach(action => {
|
||||||
// Check we have a binding for this action, and generate one if not
|
// Check we have a binding for this action, and generate one if not
|
||||||
const stateBinding = makeStateBinding(action.parameters.key)
|
const stateBinding = makeStateBinding(action.parameters.key)
|
||||||
const hasKey = bindings.some(binding => {
|
const hasKey = actionBindings.some(binding => {
|
||||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||||
})
|
})
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
bindings.push(stateBinding)
|
let existing = updateStateActions[action.id]
|
||||||
|
if (existing) {
|
||||||
|
const existingBinding = makeStateBinding(existing.parameters.key)
|
||||||
|
cloneActionBindings = cloneActionBindings.filter(
|
||||||
|
binding =>
|
||||||
|
binding.runtimeBinding !== existingBinding.runtimeBinding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
allBindings.push(stateBinding)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||||
|
@ -164,17 +194,23 @@
|
||||||
.filter(index => index !== undefined)
|
.filter(index => index !== undefined)
|
||||||
|
|
||||||
// Based on the above, filter out the asynchronous automations from the bindings
|
// Based on the above, filter out the asynchronous automations from the bindings
|
||||||
if (asynchronousAutomationIndexes) {
|
let contextBindings = asynchronousAutomationIndexes
|
||||||
allBindings = eventContextBindings
|
? eventContextBindings.filter((binding, index) => {
|
||||||
.filter((binding, index) => {
|
|
||||||
return !asynchronousAutomationIndexes.includes(index)
|
return !asynchronousAutomationIndexes.includes(index)
|
||||||
})
|
})
|
||||||
.concat(bindings)
|
: eventContextBindings
|
||||||
} else {
|
|
||||||
allBindings = eventContextBindings.concat(bindings)
|
allBindings = contextBindings
|
||||||
}
|
.concat(cloneActionBindings)
|
||||||
|
.concat(allBindings)
|
||||||
|
|
||||||
return allBindings
|
return allBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toDisplay = eventKey => {
|
||||||
|
const type = actionTypes.find(action => action.name == eventKey)
|
||||||
|
return type?.displayName || type?.name
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
|
@ -200,7 +236,9 @@
|
||||||
<ul>
|
<ul>
|
||||||
{#each category as actionType}
|
{#each category as actionType}
|
||||||
<li on:click={onAddAction(actionType)}>
|
<li on:click={onAddAction(actionType)}>
|
||||||
<span class="action-name">{actionType.name}</span>
|
<span class="action-name">
|
||||||
|
{actionType.displayName || actionType.name}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -231,7 +269,7 @@
|
||||||
>
|
>
|
||||||
<Icon name="DragHandle" size="XL" />
|
<Icon name="DragHandle" size="XL" />
|
||||||
<div class="action-header">
|
<div class="action-header">
|
||||||
{index + 1}. {action[EVENT_TYPE_KEY]}
|
{index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
name="Close"
|
name="Close"
|
||||||
|
|
|
@ -70,8 +70,9 @@
|
||||||
} set`
|
} set`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="action-count">{actionText}</div>
|
<div class="action-editor">
|
||||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title={"Actions"}>
|
<Drawer bind:this={drawer} title={"Actions"}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
|
@ -89,9 +90,7 @@
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-count {
|
.action-editor :global(.spectrum-ActionButton) {
|
||||||
padding-top: 6px;
|
width: 100%;
|
||||||
padding-bottom: var(--spacing-s);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
|
@ -10,47 +10,59 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Label>Table</Label>
|
<Body size="small">Please specify one or more rows to delete.</Body>
|
||||||
<Select
|
<div class="params">
|
||||||
bind:value={parameters.tableId}
|
<Label>Table</Label>
|
||||||
options={tableOptions}
|
<Select
|
||||||
getOptionLabel={table => table.name}
|
bind:value={parameters.tableId}
|
||||||
getOptionValue={table => table._id}
|
options={tableOptions}
|
||||||
/>
|
getOptionLabel={table => table.name}
|
||||||
|
getOptionValue={table => table._id}
|
||||||
<Label small>Row ID</Label>
|
|
||||||
<DrawerBindableInput
|
|
||||||
{bindings}
|
|
||||||
title="Row ID to delete"
|
|
||||||
value={parameters.rowId}
|
|
||||||
on:change={value => (parameters.rowId = value.detail)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Label small />
|
|
||||||
<Checkbox
|
|
||||||
text="Do not display default notification"
|
|
||||||
bind:value={parameters.notificationOverride}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
|
||||||
|
|
||||||
{#if parameters.confirm}
|
|
||||||
<Label small>Confirm text</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Are you sure you want to delete this row?"
|
|
||||||
bind:value={parameters.confirmText}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
|
<Label small>Row IDs</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
title="Rows to delete"
|
||||||
|
value={parameters.rowId}
|
||||||
|
on:change={value => (parameters.rowId = value.detail)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label small />
|
||||||
|
<Checkbox
|
||||||
|
text="Do not display default notification"
|
||||||
|
bind:value={parameters.notificationOverride}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||||
|
|
||||||
|
{#if parameters.confirm}
|
||||||
|
<Label small>Confirm text</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Are you sure you want to delete?"
|
||||||
|
bind:value={parameters.confirmText}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: 60px 1fr;
|
grid-template-columns: 60px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: hasAutomations = automations && automations.length > 0
|
|
||||||
$: selectedAutomation = automations?.find(
|
$: selectedAutomation = automations?.find(
|
||||||
a => a._id === parameters?.automationId
|
a => a._id === parameters?.automationId
|
||||||
)
|
)
|
||||||
|
@ -145,12 +144,6 @@
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.params {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.synchronous-info {
|
.synchronous-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Delete Row",
|
"name": "Delete Row",
|
||||||
|
"displayName": "Delete Rows",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
"component": "DeleteRow"
|
"component": "DeleteRow"
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
let drawer
|
let drawer
|
||||||
let boundValue
|
let boundValue
|
||||||
|
|
||||||
|
$: text = getText(value)
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
$: schema = getSchema($currentAsset, datasource)
|
||||||
$: options = allowCellEditing
|
$: options = allowCellEditing
|
||||||
|
@ -31,6 +32,17 @@
|
||||||
allowLinks: true,
|
allowLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getText = value => {
|
||||||
|
if (!value?.length) {
|
||||||
|
return "All columns"
|
||||||
|
}
|
||||||
|
let text = `${value.length} column`
|
||||||
|
if (value.length !== 1) {
|
||||||
|
text += "s"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
const getSchema = (asset, datasource) => {
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||||
|
|
||||||
|
@ -76,7 +88,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column-editor">
|
<div class="column-editor">
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
<ActionButton on:click={open}>{text}</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Columns">
|
<Drawer bind:this={drawer} title="Columns">
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
|
|
|
@ -8,32 +8,39 @@
|
||||||
getSchemaForDatasource,
|
getSchemaForDatasource,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
import { getFields } from "helpers/searchFields"
|
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value = []
|
export let value = []
|
||||||
|
|
||||||
const convertOldColumnFormat = oldColumns => {
|
|
||||||
if (typeof oldColumns?.[0] === "string") {
|
|
||||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: convertOldColumnFormat(value)
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let boundValue
|
let boundValue
|
||||||
|
|
||||||
|
$: text = getText(value)
|
||||||
|
$: convertOldColumnFormat(value)
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
$: schema = getSchema($currentAsset, datasource)
|
||||||
$: options = Object.keys(schema || {})
|
$: options = Object.keys(schema || {})
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
$: sanitisedValue = getValidColumns(value, options)
|
||||||
$: updateBoundValue(sanitisedValue)
|
$: updateBoundValue(sanitisedValue)
|
||||||
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
|
|
||||||
allowLinks: true,
|
const getText = value => {
|
||||||
})
|
if (!value?.length) {
|
||||||
|
return "All fields"
|
||||||
|
}
|
||||||
|
let text = `${value.length} field`
|
||||||
|
if (value.length !== 1) {
|
||||||
|
text += "s"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertOldColumnFormat = oldColumns => {
|
||||||
|
if (typeof oldColumns?.[0] === "string") {
|
||||||
|
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
const getSchema = (asset, datasource) => {
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||||
|
@ -79,7 +86,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure fields</ActionButton>
|
<div class="field-configuration">
|
||||||
|
<ActionButton on:click={open}>{text}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title="Form Fields">
|
<Drawer bind:this={drawer} title="Form Fields">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure the fields in your form.
|
Configure the fields in your form.
|
||||||
|
@ -87,3 +97,9 @@
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-configuration :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -192,7 +192,7 @@
|
||||||
<Label>Filters</Label>
|
<Label>Filters</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each rawFilters as filter, idx}
|
{#each rawFilters as filter}
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.field}
|
bind:value={filter.field}
|
||||||
options={fieldOptions}
|
options={fieldOptions}
|
||||||
|
|
|
@ -13,13 +13,14 @@
|
||||||
export let value = []
|
export let value = []
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let schema = null
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
|
|
||||||
$: tempValue = value
|
$: tempValue = value
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
$: dsSchema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || dsSchema || {})
|
||||||
$: text = getText(value?.filter(filter => filter.field))
|
$: text = getText(value?.filter(filter => filter.field))
|
||||||
|
|
||||||
async function saveFilter() {
|
async function saveFilter() {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<i class={icon} />
|
<i class={icon} />
|
||||||
{:else}
|
{:else}
|
||||||
<span>
|
<span>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
{@html text}
|
{@html text}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
$: nullishValue = value == null || value === ""
|
$: nullishValue = value == null || value === ""
|
||||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||||
$: tempValue = safeValue
|
|
||||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||||
|
|
||||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
const getAllBindings = (bindings, componentBindings, nested) => {
|
||||||
|
@ -104,6 +103,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if info}
|
{#if info}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
<div class="text">{@html info}</div>
|
<div class="text">{@html info}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/componentUtils"
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
|
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
||||||
|
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
// Extract which relationship column we're using
|
||||||
|
$: column = componentInstance.field
|
||||||
|
|
||||||
|
// Find the closest parent form
|
||||||
|
$: form = findClosestMatchingComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
componentInstance._id,
|
||||||
|
component => component._component.endsWith("/form")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get that form's schema
|
||||||
|
$: datasource = getDatasourceForProvider($currentAsset, form)
|
||||||
|
$: formSchema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||||
|
|
||||||
|
// Get the schema for the relationship field that this picker is using
|
||||||
|
$: columnSchema = formSchema?.[column]
|
||||||
|
|
||||||
|
// Get the schema for the table on the other side of this relationship
|
||||||
|
$: linkedTable = $tables.list.find(x => x._id === columnSchema?.tableId)
|
||||||
|
$: schema = linkedTable?.schema
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FilterEditor on:change {...$$props} {schema} />
|
|
@ -8,16 +8,29 @@
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
export let type
|
export let type
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let drawer
|
let drawer
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
$: text = getText(value)
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getText = rules => {
|
||||||
|
if (!rules?.length) {
|
||||||
|
return "No rules set"
|
||||||
|
} else {
|
||||||
|
return `${rules.length} rule${rules.length === 1 ? "" : "s"} set`
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Configure validation</ActionButton>
|
<div class="validation-editor">
|
||||||
|
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title="Validation Rules">
|
<Drawer bind:this={drawer} title="Validation Rules">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure validation rules for this field.
|
Configure validation rules for this field.
|
||||||
|
@ -31,3 +44,9 @@
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.validation-editor :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
Tab,
|
Tab,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Divider,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { notifications, Divider } from "@budibase/bbui"
|
|
||||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||||
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
||||||
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
||||||
|
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
let saveModal
|
let saveModal
|
||||||
let override = false
|
let override = false
|
||||||
let navigateTo = null
|
let navigateTo = null
|
||||||
|
let nameError = null
|
||||||
|
|
||||||
// seed the transformer
|
// seed the transformer
|
||||||
if (query && !query.transformer) {
|
if (query && !query.transformer) {
|
||||||
|
@ -77,7 +80,7 @@
|
||||||
$: queryConfig = integrationInfo?.query
|
$: queryConfig = integrationInfo?.query
|
||||||
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
||||||
$: readQuery = query.queryVerb === "read" || query.readable
|
$: readQuery = query.queryVerb === "read" || query.readable
|
||||||
$: queryInvalid = !query.name || (readQuery && data.length === 0)
|
$: queryInvalid = !query.name || nameError || (readQuery && data.length === 0)
|
||||||
|
|
||||||
//Cast field in query preview response to number if specified by schema
|
//Cast field in query preview response to number if specified by schema
|
||||||
$: {
|
$: {
|
||||||
|
@ -139,9 +142,10 @@
|
||||||
queryStr = JSON.stringify(query)
|
queryStr = JSON.stringify(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifications.success("Query saved successfully")
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving query")
|
notifications.error(error.message || "Error saving query")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -183,8 +187,14 @@
|
||||||
value={query.name}
|
value={query.name}
|
||||||
on:input={e => {
|
on:input={e => {
|
||||||
let newValue = e.target.value || ""
|
let newValue = e.target.value || ""
|
||||||
query.name = newValue.trim()
|
if (newValue.match(ValidQueryNameRegex)) {
|
||||||
|
query.name = newValue.trim()
|
||||||
|
nameError = null
|
||||||
|
} else {
|
||||||
|
nameError = "Invalid query name"
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
error={nameError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if queryConfig}
|
{#if queryConfig}
|
||||||
|
@ -250,9 +260,9 @@
|
||||||
size="L"
|
size="L"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S"
|
<Body size="S">
|
||||||
>Add a JavaScript function to transform the query result.</Body
|
Add a JavaScript function to transform the query result.
|
||||||
>
|
</Body>
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor
|
||||||
height={200}
|
height={200}
|
||||||
label="Transformer"
|
label="Transformer"
|
||||||
|
@ -264,13 +274,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer-controls">
|
<div class="viewer-controls">
|
||||||
<Heading size="S">Results</Heading>
|
<Heading size="S">Results</Heading>
|
||||||
<ButtonGroup gap="XS">
|
<ButtonGroup gap="S">
|
||||||
<Button
|
<Button
|
||||||
cta
|
cta
|
||||||
disabled={queryInvalid}
|
disabled={queryInvalid}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await saveQuery()
|
await saveQuery()
|
||||||
notifications.success(`Query saved successfully`)
|
|
||||||
// Go to the correct URL if we just created a new query
|
// Go to the correct URL if we just created a new query
|
||||||
if (!query._rev) {
|
if (!query._rev) {
|
||||||
$goto(`../../${query._id}`)
|
$goto(`../../${query._id}`)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { TOURS } from "./tours.js"
|
import { TOURS } from "./tours.js"
|
||||||
import { goto, layout, isActive } from "@roxi/routify"
|
import { goto, layout, isActive } from "@roxi/routify"
|
||||||
|
@ -10,17 +10,20 @@
|
||||||
let tourStep
|
let tourStep
|
||||||
let tourStepIdx
|
let tourStepIdx
|
||||||
let lastStep
|
let lastStep
|
||||||
|
let skipping = false
|
||||||
|
|
||||||
$: tourNodes = { ...$store.tourNodes }
|
$: tourNodes = { ...$store.tourNodes }
|
||||||
$: tourKey = $store.tourKey
|
$: tourKey = $store.tourKey
|
||||||
$: tourStepKey = $store.tourStepKey
|
$: tourStepKey = $store.tourStepKey
|
||||||
|
$: tour = TOURS[tourKey]
|
||||||
|
$: tourOnSkip = tour?.onSkip
|
||||||
|
|
||||||
const updateTourStep = (targetStepKey, tourKey) => {
|
const updateTourStep = (targetStepKey, tourKey) => {
|
||||||
if (!tourKey) {
|
if (!tourKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!tourSteps?.length) {
|
if (!tourSteps?.length) {
|
||||||
tourSteps = [...TOURS[tourKey]]
|
tourSteps = [...tour.steps]
|
||||||
}
|
}
|
||||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||||
|
@ -71,23 +74,8 @@
|
||||||
tourStep.onComplete()
|
tourStep.onComplete()
|
||||||
}
|
}
|
||||||
popover.hide()
|
popover.hide()
|
||||||
if (tourStep.endRoute) {
|
if (tour.endRoute) {
|
||||||
$goto(tourStep.endRoute)
|
$goto(tour.endRoute)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = async () => {
|
|
||||||
if (tourStepIdx > 0) {
|
|
||||||
let target = tourSteps[tourStepIdx - 1]
|
|
||||||
if (target) {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourStepKey: target.id,
|
|
||||||
}))
|
|
||||||
navigateStep(target)
|
|
||||||
} else {
|
|
||||||
console.log("Could not retrieve step")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,16 +120,23 @@
|
||||||
</Body>
|
</Body>
|
||||||
<div class="tour-footer">
|
<div class="tour-footer">
|
||||||
<div class="tour-navigation">
|
<div class="tour-navigation">
|
||||||
{#if tourStepIdx > 0}
|
{#if typeof tourOnSkip === "function"}
|
||||||
<Button
|
<Link
|
||||||
secondary
|
secondary
|
||||||
on:click={previousStep}
|
quiet
|
||||||
disabled={tourStepIdx == 0}
|
on:click={() => {
|
||||||
|
skipping = true
|
||||||
|
tourOnSkip()
|
||||||
|
if (tour.endRoute) {
|
||||||
|
$goto(tour.endRoute)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={skipping}
|
||||||
>
|
>
|
||||||
<div>Back</div>
|
Skip
|
||||||
</Button>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Button cta on:click={nextStep}>
|
<Button cta on:click={nextStep} disabled={skipping}>
|
||||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,9 +152,10 @@
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.tour-navigation {
|
.tour-navigation {
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
grid-gap: var(--spacing-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.tour-body :global(.feature-list) {
|
.tour-body :global(.feature-list) {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
const registerTourNode = (tourKey, stepKey) => {
|
const registerTourNode = (tourKey, stepKey) => {
|
||||||
if (ready && !registered && tourKey) {
|
if (ready && !registered && tourKey) {
|
||||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||||
if (!currentTourStep) {
|
if (!currentTourStep) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||||
|
|
||||||
export const TOUR_STEP_KEYS = {
|
export const TOUR_STEP_KEYS = {
|
||||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
||||||
FEATURE_ONBOARDING: "feature-onboarding",
|
FEATURE_ONBOARDING: "feature-onboarding",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endUserOnboarding = async ({ skipped = false } = {}) => {
|
||||||
|
// Mark the users onboarding as complete
|
||||||
|
// Clear all tour related state
|
||||||
|
if (get(auth).user) {
|
||||||
|
try {
|
||||||
|
await API.updateSelf({
|
||||||
|
onboardedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (skipped) {
|
||||||
|
tourEvent("skipped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
tourNodes: undefined,
|
||||||
|
tourKey: undefined,
|
||||||
|
tourKeyStep: undefined,
|
||||||
|
onboarding: false,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Onboarding failed", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tourEvent = eventKey => {
|
const tourEvent = eventKey => {
|
||||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||||
eventSource: EventSource.PORTAL,
|
eventSource: EventSource.PORTAL,
|
||||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
||||||
|
|
||||||
const getTours = () => {
|
const getTours = () => {
|
||||||
return {
|
return {
|
||||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
||||||
{
|
steps: [
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
{
|
||||||
title: "Data",
|
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||||
route: "/builder/app/:application/data",
|
title: "Data",
|
||||||
layout: OnboardingData,
|
route: "/builder/app/:application/data",
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
layout: OnboardingData,
|
||||||
onLoad: async () => {
|
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
onLoad: async () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
},
|
},
|
||||||
align: "left",
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
||||||
|
title: "Design",
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
layout: OnboardingDesign,
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||||
|
title: "Automations",
|
||||||
|
route: "/builder/app/:application/automation",
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||||
|
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||||
|
title: "Users",
|
||||||
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||||
|
title: "Publish",
|
||||||
|
layout: OnboardingPublish,
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
query: ".toprightnav #builder-app-publish-button",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSkip: async () => {
|
||||||
|
await endUserOnboarding({ skipped: true })
|
||||||
},
|
},
|
||||||
{
|
endRoute: "/builder/app/:application/data",
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
},
|
||||||
title: "Design",
|
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
||||||
route: "/builder/app/:application/design",
|
steps: [
|
||||||
layout: OnboardingDesign,
|
{
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||||
onLoad: () => {
|
title: "Users",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
},
|
},
|
||||||
align: "left",
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
|
||||||
title: "Automations",
|
|
||||||
route: "/builder/app/:application/automation",
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
|
||||||
title: "Publish",
|
|
||||||
layout: OnboardingPublish,
|
|
||||||
route: "/builder/app/:application/design",
|
|
||||||
endRoute: "/builder/app/:application/data",
|
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Mark the users onboarding as complete
|
|
||||||
// Clear all tour related state
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[TOUR_KEYS.FEATURE_ONBOARDING]: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Push the onboarding forward
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,31 +2,39 @@
|
||||||
export let text
|
export let text
|
||||||
export let url
|
export let url
|
||||||
export let active = false
|
export let active = false
|
||||||
|
export let disabled = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url}
|
<div class="side-nav-item">
|
||||||
<a on:click href={url} class:active>
|
{#if url}
|
||||||
{text || ""}
|
<a class="text" on:click href={url} class:active class:disabled>
|
||||||
</a>
|
{text || ""}
|
||||||
{:else}
|
</a>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
{:else}
|
||||||
<span on:click class:active>
|
<div class="text" on:click class:active class:disabled>
|
||||||
{text || ""}
|
{text || ""}
|
||||||
</span>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a,
|
.side-nav-item {
|
||||||
span {
|
position: relative;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
display: block;
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 130ms ease-out;
|
transition: background 130ms ease-out;
|
||||||
}
|
}
|
||||||
.active,
|
.active,
|
||||||
span:hover,
|
.text:hover {
|
||||||
a:hover {
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue