Merge branch 'develop' into backmerge-master-20230727
This commit is contained in:
commit
25019aa31e
|
@ -5,7 +5,7 @@
|
|||
"jest": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module",
|
||||
|
@ -18,17 +18,23 @@
|
|||
"*.spec.js",
|
||||
"bundle.js"
|
||||
],
|
||||
"plugins": ["svelte3"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.svelte"],
|
||||
"processor": "svelte3/svelte3"
|
||||
"files": ["**/*.svelte"],
|
||||
"extends": "plugin:svelte/recommended",
|
||||
"parser": "svelte-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
@ -41,7 +47,8 @@
|
|||
}
|
||||
],
|
||||
"rules": {
|
||||
"no-self-assign": "off"
|
||||
"no-self-assign": "off",
|
||||
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }]
|
||||
},
|
||||
"globals": {
|
||||
"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
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
@ -162,7 +159,7 @@ jobs:
|
|||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
yarn serve:test:self:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
|
@ -185,7 +182,7 @@ jobs:
|
|||
pro_commit=$(git rev-parse HEAD)
|
||||
|
||||
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
|
||||
base_commit=$(git rev-parse origin/master)
|
||||
|
|
|
@ -34,7 +34,6 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
@ -58,9 +57,12 @@ jobs:
|
|||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release
|
||||
|
||||
- name: "Get Previous tag"
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
- name: "Get Current tag"
|
||||
id: currenttag
|
||||
run: |
|
||||
version=v$(./scripts/getCurrentVersion.sh)
|
||||
echo 'Using tag $version'
|
||||
echo "::set-output name=tag::$resversionult"
|
||||
|
||||
- name: Build/release Docker images
|
||||
run: |
|
||||
|
@ -69,7 +71,7 @@ jobs:
|
|||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
|
||||
|
||||
release-helm-chart:
|
||||
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
|
|
@ -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 }}
|
||||
imagePullPolicy: Always
|
||||
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:
|
||||
- containerPort: {{ .Values.services.proxy.port }}
|
||||
env:
|
||||
|
|
|
@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
|||
|
||||
### 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:
|
||||
|
||||
```
|
||||
.
|
||||
|_ budibase
|
||||
|_ budibase-pro
|
||||
# enter the pro submodule
|
||||
cd packages/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.
|
||||
|
||||
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
|
||||
|
||||
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/
|
||||
PLUGINS_DIR=
|
||||
ROLLING_LOG_MAX_SIZE=
|
5
nx.json
5
nx.json
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"runner": "nx-cloud",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test"]
|
||||
"cacheableOperations": ["build", "test"],
|
||||
"accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
28
package.json
28
package.json
|
@ -3,27 +3,32 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||
"@nx/js": "16.2.1",
|
||||
"@nx/js": "16.4.3",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"esbuild": "^0.18.17",
|
||||
"esbuild-node-externals": "^1.8.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "7.0.2",
|
||||
"lerna": "7.1.1",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "16.4.3",
|
||||
"nx-cloud": "16.0.5",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"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": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
|
@ -41,7 +46,7 @@
|
|||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"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",
|
||||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
|
@ -49,13 +54,13 @@
|
|||
"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: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",
|
||||
"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": "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": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"build:specs": "lerna run --stream specs",
|
||||
|
@ -103,5 +108,8 @@
|
|||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <15.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"pouchdb": "7.3.0",
|
||||
"pouchdb-find": "7.2.2",
|
||||
"redlock": "4.2.0",
|
||||
"rotating-file-stream": "3.1.0",
|
||||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "7.3.7",
|
||||
"tar-fs": "2.1.1",
|
||||
|
|
|
@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
|||
|
||||
try {
|
||||
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.
|
||||
if (typeof details.refreshToken !== "string") {
|
||||
|
|
|
@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init"
|
|||
import { doWithDB, DocumentType } from "../db"
|
||||
import { Database, App } from "@budibase/types"
|
||||
|
||||
const AppState = {
|
||||
INVALID: "invalid",
|
||||
export enum AppState {
|
||||
INVALID = "invalid",
|
||||
}
|
||||
|
||||
export interface DeletedApp {
|
||||
state: AppState
|
||||
}
|
||||
|
||||
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.
|
||||
* @returns {object} the app metadata.
|
||||
*/
|
||||
export async function getAppMetadata(appId: string) {
|
||||
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
||||
const client = await getAppClient()
|
||||
// try cache
|
||||
let metadata = await client.get(appId)
|
||||
|
@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) {
|
|||
}
|
||||
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)) {
|
||||
throw { status: 404, message: "No app metadata found" }
|
||||
}
|
||||
return metadata as App
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
|
|||
*/
|
||||
async function populateFromDB(userId: string, tenantId: string) {
|
||||
const db = tenancy.getTenantDB(tenantId)
|
||||
const user = await db.get(userId)
|
||||
const user = await db.get<any>(userId)
|
||||
user.budibaseAccess = true
|
||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||
const account = await accounts.getAccount(user.email)
|
||||
|
|
|
@ -20,6 +20,8 @@ export enum Header {
|
|||
TYPE = "x-budibase-type",
|
||||
PREVIEW_ROLE = "x-budibase-role",
|
||||
TENANT_ID = "x-budibase-tenant-id",
|
||||
VERIFICATION_CODE = "x-budibase-verification-code",
|
||||
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
||||
TOKEN = "x-budibase-token",
|
||||
CSRF_TOKEN = "x-csrf-token",
|
||||
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 "./utils"
|
||||
export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB"
|
||||
export * from "../constants"
|
||||
|
|
|
@ -5,7 +5,7 @@ export async function createUserIndex() {
|
|||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
designDoc = await db.get<any>("_design/database")
|
||||
} catch (err: any) {
|
||||
if (err.status === 404) {
|
||||
designDoc = { _id: "_design/database" }
|
||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
|||
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
||||
import { getTenantId, getGlobalDBName } from "../context"
|
||||
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 { App, Database } from "@budibase/types"
|
||||
import { getStartEndKeyURL } from "../docIds"
|
||||
|
@ -101,7 +101,9 @@ export async function getAllApps({
|
|||
const response = await Promise.allSettled(appPromises)
|
||||
const apps = response
|
||||
.filter(
|
||||
(result: any) => result.status === "fulfilled" && result.value != null
|
||||
(result: any) =>
|
||||
result.status === "fulfilled" &&
|
||||
result.value?.state !== AppState.INVALID
|
||||
)
|
||||
.map(({ value }: any) => value)
|
||||
if (!all) {
|
||||
|
@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) {
|
|||
)
|
||||
// have to list the apps which exist, some may have been deleted
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ function httpLogging() {
|
|||
return process.env.HTTP_LOGGING
|
||||
}
|
||||
|
||||
function findVersion() {
|
||||
function getPackageJsonFields(): {
|
||||
VERSION: string
|
||||
SERVICE_NAME: string
|
||||
} {
|
||||
function findFileInAncestors(
|
||||
fileName: string,
|
||||
currentDir: string
|
||||
|
@ -69,10 +72,14 @@ function findVersion() {
|
|||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
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 {
|
||||
// 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
|
||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||
: false,
|
||||
VERSION: findVersion(),
|
||||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
_set(key: any, value: any) {
|
||||
|
@ -162,6 +169,7 @@ const environment = {
|
|||
// @ts-ignore
|
||||
environment[key] = value
|
||||
},
|
||||
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||
}
|
||||
|
||||
// clean up any environment variable edge cases
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export * as correlation from "./correlation/correlation"
|
||||
export { logger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
export let LOG_CONTEXT = true
|
||||
export * as system from "./system"
|
||||
|
|
|
@ -1,37 +1,60 @@
|
|||
import env from "../../environment"
|
||||
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 correlation from "../correlation"
|
||||
import { IdentityType } from "@budibase/types"
|
||||
import { LOG_CONTEXT } from "../index"
|
||||
|
||||
import { localFileDestination } from "../system"
|
||||
|
||||
// LOGGER
|
||||
|
||||
let pinoInstance: pino.Logger | undefined
|
||||
if (!env.DISABLE_PINO_LOGGER) {
|
||||
const level = env.LOG_LEVEL
|
||||
const pinoOptions: LoggerOptions = {
|
||||
level: env.LOG_LEVEL,
|
||||
level,
|
||||
formatters: {
|
||||
level: label => {
|
||||
return { level: label.toUpperCase() }
|
||||
level: level => {
|
||||
return { level: level.toUpperCase() }
|
||||
},
|
||||
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()}"`,
|
||||
}
|
||||
|
||||
if (env.isDev()) {
|
||||
pinoOptions.transport = {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
const destinations: pino.StreamEntry[] = []
|
||||
|
||||
destinations.push(
|
||||
env.isDev()
|
||||
? {
|
||||
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
|
||||
|
||||
|
@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
|
||||
let contextObject = {}
|
||||
|
||||
if (LOG_CONTEXT) {
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
|
||||
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> {
|
||||
const db = context.getGlobalDB()
|
||||
let user = await db.get(id)
|
||||
let user = await db.get<User>(id)
|
||||
if (opts?.cleanup) {
|
||||
user = removeUserPassword(user)
|
||||
user = removeUserPassword(user) as User
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { db } from "../../../src"
|
||||
|
||||
export function expectFunctionWasCalledTimesWith(
|
||||
jestFunction: any,
|
||||
times: number,
|
||||
|
@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith(
|
|||
jestFunction.mock.calls.filter((call: any) => call[0] === argument).length
|
||||
).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": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/string-templates"
|
||||
"@budibase/string-templates",
|
||||
"@budibase/shared-core"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
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 disabled = false
|
||||
|
@ -17,65 +18,52 @@
|
|||
export let newStyles = true
|
||||
export let id
|
||||
|
||||
let showTooltip = false
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
class:spectrum-Button--warning={warning}
|
||||
class:spectrum-Button--overBackground={overBackground}
|
||||
class:spectrum-Button--quiet={quiet}
|
||||
class:new-styles={newStyles}
|
||||
class:active
|
||||
class:disabled
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
{disabled}
|
||||
on:click|preventDefault
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
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">
|
||||
<AbsTooltip text={tooltip}>
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
class:spectrum-Button--warning={warning}
|
||||
class:spectrum-Button--overBackground={overBackground}
|
||||
class:spectrum-Button--quiet={quiet}
|
||||
class:new-styles={newStyles}
|
||||
class:active
|
||||
class:is-disabled={disabled}
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
on:click|preventDefault={() => {
|
||||
if (!disabled) {
|
||||
dispatch("click")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Info"
|
||||
aria-label={icon}
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTooltip && tooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if $$slots}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
</button>
|
||||
</AbsTooltip>
|
||||
|
||||
<style>
|
||||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button.is-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -84,21 +72,6 @@
|
|||
.active {
|
||||
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 {
|
||||
background: var(--spectrum-global-color-gray-800);
|
||||
border-color: transparent;
|
||||
|
@ -112,10 +85,10 @@
|
|||
border-color: transparent;
|
||||
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);
|
||||
}
|
||||
.spectrum-Button--secondary.new-styles.disabled {
|
||||
.spectrum-Button--secondary.new-styles.is-disabled {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: placeholder = !value
|
||||
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
|
|
|
@ -12,23 +12,24 @@
|
|||
export let getOptionValue = option => option
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onChange = e => {
|
||||
let tempValue = value
|
||||
let isChecked = e.target.checked
|
||||
if (!tempValue.includes(e.target.value) && isChecked) {
|
||||
tempValue.push(e.target.value)
|
||||
const optionValue = e.target.value
|
||||
if (e.target.checked && !value.includes(optionValue)) {
|
||||
dispatch("change", [...value, optionValue])
|
||||
} else {
|
||||
dispatch(
|
||||
"change",
|
||||
value.filter(x => x !== optionValue)
|
||||
)
|
||||
}
|
||||
value = tempValue
|
||||
dispatch(
|
||||
"change",
|
||||
tempValue.filter(val => val !== e.target.value || isChecked)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
||||
{#if options && Array.isArray(options)}
|
||||
{#each options as option}
|
||||
{@const optionValue = getOptionValue(option)}
|
||||
<div
|
||||
title={getOptionLabel(option)}
|
||||
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
||||
|
@ -39,11 +40,11 @@
|
|||
>
|
||||
<input
|
||||
on:change={onChange}
|
||||
value={getOptionValue(option)}
|
||||
type="checkbox"
|
||||
class="spectrum-Checkbox-input"
|
||||
value={optionValue}
|
||||
checked={value.includes(optionValue)}
|
||||
{disabled}
|
||||
checked={value.includes(getOptionValue(option))}
|
||||
/>
|
||||
<span class="spectrum-Checkbox-box">
|
||||
<svg
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
</div>
|
||||
{:else if variables.length}
|
||||
<div style="max-height: 100px">
|
||||
{#each variables as variable, idx}
|
||||
{#each variables as variable}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
role="option"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/link/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let href = "#"
|
||||
export let size = "M"
|
||||
|
@ -10,18 +11,61 @@
|
|||
export let overBackground = false
|
||||
export let target
|
||||
export let download
|
||||
export let disabled = false
|
||||
export let tooltip = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onClick = e => {
|
||||
if (!disabled) {
|
||||
dispatch("click")
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
on:click={e => dispatch("click") && e.stopPropagation()}
|
||||
on:click={onClick}
|
||||
{href}
|
||||
{target}
|
||||
{download}
|
||||
class:disabled
|
||||
class:spectrum-Link--primary={primary}
|
||||
class:spectrum-Link--secondary={secondary}
|
||||
class:spectrum-Link--overBackground={overBackground}
|
||||
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>
|
||||
import "@spectrum-css/dialog/dist/index-vars.css"
|
||||
import { getContext } from "svelte"
|
||||
|
@ -30,7 +34,7 @@
|
|||
|
||||
async function secondary(e) {
|
||||
loading = true
|
||||
if (!secondaryAction || (await secondaryAction(e)) !== false) {
|
||||
if (!secondaryAction || (await secondaryAction(e)) !== keepOpen) {
|
||||
hide()
|
||||
}
|
||||
loading = false
|
||||
|
@ -38,7 +42,7 @@
|
|||
|
||||
async function confirm() {
|
||||
loading = true
|
||||
if (!onConfirm || (await onConfirm()) !== false) {
|
||||
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
||||
hide()
|
||||
}
|
||||
loading = false
|
||||
|
@ -46,7 +50,7 @@
|
|||
|
||||
async function close() {
|
||||
loading = true
|
||||
if (!onCancel || (await onCancel()) !== false) {
|
||||
if (!onCancel || (await onCancel()) !== keepOpen) {
|
||||
cancel()
|
||||
}
|
||||
loading = false
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
$: type = getType(schema)
|
||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
||||
$: width = schema?.width || "150px"
|
||||
$: cellValue = getCellValue(value, schema.template)
|
||||
|
||||
const getType = schema => {
|
||||
|
|
|
@ -379,7 +379,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if sortedRows?.length}
|
||||
{#each sortedRows as row, idx}
|
||||
{#each sortedRows as row}
|
||||
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
|
||||
{#if showEditColumn}
|
||||
<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 Link } from "./Link/Link.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 Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
export { default as MenuItem } from "./Menu/Item.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 Notification } from "./Notification/Notification.svelte"
|
||||
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
||||
|
|
|
@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => {
|
|||
readableBinding: `${table._instanceName}.Selected rows`,
|
||||
category: "Selected rows",
|
||||
icon: "ViewRow",
|
||||
display: { name: table._instanceName },
|
||||
}))
|
||||
)
|
||||
|
||||
|
@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => {
|
|||
)}.${makePropSafe("selectedRows")}`,
|
||||
readableBinding: `${block._instanceName}.Selected rows`,
|
||||
category: "Selected rows",
|
||||
display: { name: block._instanceName },
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { getDeploymentStore } from "./store/deployments"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
|
|||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
export const deploymentStore = getDeploymentStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
@ -118,3 +120,24 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
|||
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)
|
||||
},
|
||||
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 { getComponentFieldOptions } from "helpers/formFields"
|
||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
initialised: false,
|
||||
|
@ -353,6 +354,33 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
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 => {
|
||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||
|
||||
|
@ -1305,7 +1333,7 @@ export const getFrontendStore = () => {
|
|||
links: {
|
||||
save: async (url, title) => {
|
||||
const navigation = get(store).navigation
|
||||
let links = [...navigation?.links]
|
||||
let links = [...(navigation?.links ?? [])]
|
||||
|
||||
// Skip if we have an identical link
|
||||
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
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
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 { get } from "svelte/store"
|
||||
import { auth } from "stores/portal"
|
||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||
import { apps } from "stores/portal"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
@ -31,7 +38,6 @@ export const createBuilderWebsocket = appId => {
|
|||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
notifications.success("You can now edit screens and automations")
|
||||
store.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
|
@ -39,15 +45,37 @@ export const createBuilderWebsocket = appId => {
|
|||
}
|
||||
})
|
||||
|
||||
// Table events
|
||||
// Data section events
|
||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
|
||||
// Datasource events
|
||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ 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
|
||||
}
|
||||
|
|
|
@ -168,7 +168,7 @@
|
|||
<Layout noPadding gap="XS">
|
||||
<Detail size="S">Plugins</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(plugins) as [idx, action]}
|
||||
{#each Object.entries(plugins) as [_, action]}
|
||||
<div
|
||||
class="item"
|
||||
class:selected={selectedAction === action.name}
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html html}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
userSelectedResourceMap,
|
||||
} from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -21,13 +25,13 @@
|
|||
</script>
|
||||
|
||||
<div class="automations-list">
|
||||
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
|
||||
{#each $automationStore.automations.sort(aut => aut.name) as automation}
|
||||
<NavItem
|
||||
border={idx > 0}
|
||||
icon="ShareAndroid"
|
||||
text={automation.name}
|
||||
selected={automation._id === selectedAutomationId}
|
||||
on:click={() => selectAutomation(automation._id)}
|
||||
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||
>
|
||||
<EditAutomationPopover {automation} />
|
||||
</NavItem>
|
||||
|
@ -40,6 +44,5 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<Panel title="Automations" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button cta on:click={modal.show}>Add automation</Button>
|
||||
<AutomationList />
|
||||
</Layout>
|
||||
<AutomationList />
|
||||
</Panel>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<Layout noPadding gap="XS">
|
||||
<Label size="S">Trigger</Label>
|
||||
<div class="item-list">
|
||||
{#each triggers as [idx, trigger]}
|
||||
{#each triggers as [_, trigger]}
|
||||
<div
|
||||
class="item"
|
||||
class:selected={selectedTrigger === trigger.name}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
import { goto, params } from "@roxi/routify"
|
||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
|
@ -33,7 +31,6 @@
|
|||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
$: data && resetSelectedRows()
|
||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
||||
$: {
|
||||
UNSORTABLE_TYPES.forEach(type => {
|
||||
Object.values(schema || {}).forEach(col => {
|
||||
|
@ -112,6 +109,7 @@
|
|||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
{allowClickRows}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import {
|
||||
FIELDS,
|
||||
RelationshipTypes,
|
||||
RelationshipType,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
|
@ -58,7 +58,6 @@
|
|||
|
||||
let table = $tables.selected
|
||||
let confirmDeleteDialog
|
||||
let deletion
|
||||
let savingColumn
|
||||
let deleteColName
|
||||
let jsonSchemaModal
|
||||
|
@ -185,7 +184,7 @@
|
|||
dispatch("updatecolumns")
|
||||
if (
|
||||
saveColumn.type === LINK_TYPE &&
|
||||
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
|
||||
) {
|
||||
// Fetching the new tables
|
||||
tables.fetch()
|
||||
|
@ -216,7 +215,6 @@
|
|||
notifications.success(`Column ${editableColumn.name} deleted`)
|
||||
confirmDeleteDialog.hide()
|
||||
hide()
|
||||
deletion = false
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -240,7 +238,7 @@
|
|||
|
||||
// Default relationships many to many
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
if (editableColumn.type === FORMULA_TYPE) {
|
||||
editableColumn.formulaType = "dynamic"
|
||||
|
@ -267,13 +265,11 @@
|
|||
|
||||
function confirmDelete() {
|
||||
confirmDeleteDialog.show()
|
||||
deletion = true
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
confirmDeleteDialog.hide()
|
||||
deleteColName = ""
|
||||
deletion = false
|
||||
}
|
||||
|
||||
function getRelationshipOptions(field) {
|
||||
|
@ -290,17 +286,17 @@
|
|||
{
|
||||
name: `Many ${thisName} rows → many ${linkName} 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`,
|
||||
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`,
|
||||
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
||||
value: RelationshipTypes.MANY_TO_ONE,
|
||||
value: RelationshipType.MANY_TO_ONE,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import { API } from "api"
|
||||
import { ModalContent } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
|
@ -41,8 +40,8 @@
|
|||
} else {
|
||||
notifications.error(`Failed to save row - ${error.message}`)
|
||||
}
|
||||
// Prevent modal closing if there were errors
|
||||
return false
|
||||
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
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 { goto } from "@roxi/routify"
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
errors = [...errors, { message: "Role is required" }]
|
||||
}
|
||||
if (errors.length) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -79,8 +79,8 @@
|
|||
} else {
|
||||
notifications.error("Error saving user")
|
||||
}
|
||||
// Prevent closing the modal on errors
|
||||
return false
|
||||
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -95,9 +95,9 @@
|
|||
{#if !creating}
|
||||
<div>
|
||||
A user's email, role, first and last names cannot be changed from within
|
||||
the app builder. Please go to the <Link
|
||||
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
|
||||
> to do this.
|
||||
the app builder. Please go to the
|
||||
<Link on:click={$goto("/builder/portal/users/users")}>user portal</Link>
|
||||
to do this.
|
||||
</div>
|
||||
{/if}
|
||||
<RowFieldControl
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -76,7 +76,7 @@
|
|||
errors.push({ message: "Please choose permissions" })
|
||||
}
|
||||
if (errors.length) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
// Save/create the role
|
||||
|
@ -85,7 +85,7 @@
|
|||
notifications.success("Role saved successfully")
|
||||
} catch (error) {
|
||||
notifications.error(`Error saving role - ${error.message}`)
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
} from "helpers/data/utils"
|
||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
|
||||
let openDataSources = []
|
||||
|
||||
|
@ -166,8 +167,9 @@
|
|||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === TableNames.USERS}
|
||||
on:click={() => selectTable(TableNames.USERS)}
|
||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||
/>
|
||||
{#each enrichedDataSources as datasource, idx}
|
||||
{#each enrichedDataSources as datasource}
|
||||
<NavItem
|
||||
border
|
||||
text={datasource.name}
|
||||
|
@ -176,6 +178,7 @@
|
|||
withArrow={true}
|
||||
on:click={() => selectDatasource(datasource)}
|
||||
on:iconClick={() => toggleNode(datasource)}
|
||||
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||
>
|
||||
<div class="datasource-icon" slot="icon">
|
||||
<IntegrationIcon
|
||||
|
@ -201,6 +204,7 @@
|
|||
selected={$isActive("./query/:queryId") &&
|
||||
$queries.selectedQueryId === query._id}
|
||||
on:click={() => $goto(`./query/${query._id}`)}
|
||||
selectedBy={$userSelectedResourceMap[query._id]}
|
||||
>
|
||||
<EditQueryPopover {query} />
|
||||
</NavItem>
|
||||
|
@ -212,7 +216,7 @@
|
|||
|
||||
<style>
|
||||
.hierarchy-items-container {
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
margin: 0 calc(-1 * var(--spacing-l));
|
||||
}
|
||||
.datasource-icon {
|
||||
display: grid;
|
||||
|
|
|
@ -31,65 +31,65 @@
|
|||
<path
|
||||
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
|
||||
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>
|
||||
<path
|
||||
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
|
||||
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
|
||||
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.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
|
||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
||||
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
|
||||
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.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
|
||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
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
|
||||
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
|
||||
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.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
|
||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
||||
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
|
||||
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.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
|
||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
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
|
||||
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>
|
||||
<path
|
||||
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
|
||||
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
|
||||
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.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
|
||||
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
||||
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
|
||||
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.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
|
||||
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
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
|
||||
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
|
||||
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
|
||||
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.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
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
@ -102,24 +102,24 @@
|
|||
<path
|
||||
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
|
||||
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
|
||||
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
|
||||
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.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
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
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
|
||||
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
|
||||
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
|
||||
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.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
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
keepOpen,
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
|
@ -70,10 +71,9 @@
|
|||
}
|
||||
|
||||
notifications.success(`Imported successfully.`)
|
||||
return true
|
||||
} catch (error) {
|
||||
notifications.error("Error importing queries")
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
keepOpen,
|
||||
Modal,
|
||||
notifications,
|
||||
Body,
|
||||
|
@ -36,7 +37,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
let createVariableModal
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { RelationshipTypes } from "constants/backend"
|
||||
import { RelationshipType } from "constants/backend"
|
||||
import {
|
||||
keepOpen,
|
||||
Button,
|
||||
Input,
|
||||
ModalContent,
|
||||
|
@ -24,11 +25,11 @@
|
|||
const relationshipTypes = [
|
||||
{
|
||||
label: "One to Many",
|
||||
value: RelationshipTypes.MANY_TO_ONE,
|
||||
value: RelationshipType.MANY_TO_ONE,
|
||||
},
|
||||
{
|
||||
label: "Many to Many",
|
||||
value: RelationshipTypes.MANY_TO_MANY,
|
||||
value: RelationshipType.MANY_TO_MANY,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -57,8 +58,8 @@
|
|||
value: table._id,
|
||||
}))
|
||||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
||||
|
||||
function getTable(id) {
|
||||
return plusTables.find(table => table._id === id)
|
||||
|
@ -115,7 +116,7 @@
|
|||
|
||||
function allRequiredAttributesSet() {
|
||||
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
||||
if (relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
||||
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
||||
return base && fromPrimary && fromForeign
|
||||
} else {
|
||||
return base && getTable(throughId) && throughFromKey && throughToKey
|
||||
|
@ -180,12 +181,12 @@
|
|||
}
|
||||
|
||||
function otherRelationshipType(type) {
|
||||
if (type === RelationshipTypes.MANY_TO_ONE) {
|
||||
return RelationshipTypes.ONE_TO_MANY
|
||||
} else if (type === RelationshipTypes.ONE_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_ONE
|
||||
} else if (type === RelationshipTypes.MANY_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_MANY
|
||||
if (type === RelationshipType.MANY_TO_ONE) {
|
||||
return RelationshipType.ONE_TO_MANY
|
||||
} else if (type === RelationshipType.ONE_TO_MANY) {
|
||||
return RelationshipType.MANY_TO_ONE
|
||||
} else if (type === RelationshipType.MANY_TO_MANY) {
|
||||
return RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,7 +218,7 @@
|
|||
|
||||
// if any to many only need to check from
|
||||
const manyToMany =
|
||||
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
|
||||
|
||||
if (!manyToMany) {
|
||||
delete relateFrom.through
|
||||
|
@ -252,7 +253,7 @@
|
|||
}
|
||||
relateTo = {
|
||||
...relateTo,
|
||||
relationshipType: RelationshipTypes.ONE_TO_MANY,
|
||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||
foreignKey: relateFrom.fieldName,
|
||||
fieldName: fromPrimary,
|
||||
}
|
||||
|
@ -277,7 +278,7 @@
|
|||
|
||||
async function saveRelationship() {
|
||||
if (!validate()) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
buildRelationships()
|
||||
removeExistingRelationship()
|
||||
|
@ -320,7 +321,7 @@
|
|||
fromColumn = toRelationship.name
|
||||
}
|
||||
relationshipType =
|
||||
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
|
||||
fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
|
||||
if (selectedFromTable) {
|
||||
fromId = selectedFromTable._id
|
||||
fromColumn = selectedFromTable.name
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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"
|
||||
|
||||
export const createTableSelectionStore = (integration, datasource) => {
|
||||
|
@ -36,8 +36,7 @@ export const createTableSelectionStore = (integration, datasource) => {
|
|||
notifications.error("Error fetching tables.")
|
||||
}
|
||||
|
||||
// Prevent modal closing
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 columnBeingUsed = "Column name cannot be an existing column"
|
||||
|
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
|
|||
}
|
||||
|
||||
isMany() {
|
||||
return this.type === RelationshipTypes.MANY_TO_MANY
|
||||
return this.type === RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
|
||||
relationshipTypeSet(type) {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
let error = null
|
||||
let fileName = null
|
||||
let fileType = null
|
||||
|
||||
let loading = false
|
||||
let updateExistingRows = false
|
||||
|
@ -74,7 +73,6 @@
|
|||
const response = await parseFile(e)
|
||||
rows = response.rows
|
||||
fileName = response.fileName
|
||||
fileType = response.fileType
|
||||
} catch (e) {
|
||||
loading = false
|
||||
error = e
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
let fileInput
|
||||
let error = null
|
||||
let fileName = null
|
||||
let fileType = null
|
||||
let loading = false
|
||||
let validation = {}
|
||||
let validateHash = ""
|
||||
|
@ -73,7 +72,6 @@
|
|||
rows = response.rows
|
||||
schema = response.schema
|
||||
fileName = response.fileName
|
||||
fileType = response.fileType
|
||||
} catch (e) {
|
||||
loading = false
|
||||
error = e
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
|
||||
const alphabetical = (a, b) =>
|
||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
|
@ -30,6 +31,7 @@
|
|||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === table._id}
|
||||
on:click={() => selectTable(table._id)}
|
||||
selectedBy={$userSelectedResourceMap[table._id]}
|
||||
>
|
||||
{#if table._id !== TableNames.USERS}
|
||||
<EditTablePopover {table} />
|
||||
|
@ -42,6 +44,7 @@
|
|||
text={viewName}
|
||||
selected={$isActive("./view") && $views.selected?.name === viewName}
|
||||
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
||||
selectedBy={$userSelectedResourceMap[viewName]}
|
||||
>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...table.views[viewName] }}
|
||||
|
|
|
@ -2,21 +2,13 @@
|
|||
import { goto, url } from "@roxi/routify"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
ModalContent,
|
||||
Toggle,
|
||||
Divider,
|
||||
Layout,
|
||||
} from "@budibase/bbui"
|
||||
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
import TableDataImport from "../TableDataImport.svelte"
|
||||
import {
|
||||
BUDIBASE_INTERNAL_DB_ID,
|
||||
BUDIBASE_DATASOURCE_TYPE,
|
||||
} from "constants/backend"
|
||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
||||
|
||||
$: tableNames = $tables.list.map(table => table.name)
|
||||
$: selectedSource = $datasources.list.find(
|
||||
|
@ -43,28 +35,12 @@
|
|||
}
|
||||
|
||||
let error = ""
|
||||
let autoColumns = getAutoColumnInformation()
|
||||
|
||||
let schema = {}
|
||||
let rows = []
|
||||
let allValid = true
|
||||
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) {
|
||||
const tableName = evt.target.value
|
||||
if (tableNames.includes(tableName)) {
|
||||
|
@ -77,7 +53,7 @@
|
|||
async function saveTable() {
|
||||
let newTable = {
|
||||
name,
|
||||
schema: { ...schema, ...getAutoColumns() },
|
||||
schema: { ...schema },
|
||||
rows,
|
||||
type: "internal",
|
||||
sourceId: targetDatasourceId,
|
||||
|
@ -118,21 +94,6 @@
|
|||
bind:value={name}
|
||||
{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>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall
|
||||
|
@ -148,24 +109,3 @@
|
|||
</Layout>
|
||||
</div>
|
||||
</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 {
|
||||
const isSelected =
|
||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||
const name = view.name
|
||||
const id = view.tableId
|
||||
await views.delete(name)
|
||||
await views.delete(view)
|
||||
notifications.success("View deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./table/${id}`)
|
||||
|
|
|
@ -93,42 +93,42 @@
|
|||
`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",
|
||||
name: `${datasource.name}`,
|
||||
icon: "Data",
|
||||
action: () => $goto(`./data/datasource/${datasource._id}`),
|
||||
})),
|
||||
...$tables?.list.map(table => ({
|
||||
})) ?? []),
|
||||
...($tables?.list?.map(table => ({
|
||||
type: "Table",
|
||||
name: table.name,
|
||||
icon: "Table",
|
||||
action: () => $goto(`./data/table/${table._id}`),
|
||||
})),
|
||||
...$views?.list.map(view => ({
|
||||
})) ?? []),
|
||||
...($views?.list?.map(view => ({
|
||||
type: "View",
|
||||
name: view.name,
|
||||
icon: "Remove",
|
||||
action: () => $goto(`./data/view/${view.name}`),
|
||||
})),
|
||||
...$queries?.list.map(query => ({
|
||||
})) ?? []),
|
||||
...($queries?.list?.map(query => ({
|
||||
type: "Query",
|
||||
name: query.name,
|
||||
icon: "SQLQuery",
|
||||
action: () => $goto(`./data/query/${query._id}`),
|
||||
})),
|
||||
})) ?? []),
|
||||
...$sortedScreens.map(screen => ({
|
||||
type: "Screen",
|
||||
name: screen.routing.route,
|
||||
icon: "WebPage",
|
||||
action: () => $goto(`./design/${screen._id}/components`),
|
||||
})),
|
||||
...$automationStore?.automations.map(automation => ({
|
||||
...($automationStore?.automations?.map(automation => ({
|
||||
type: "Automation",
|
||||
name: automation.name,
|
||||
icon: "ShareAndroid",
|
||||
action: () => $goto(`./automation/${automation._id}`),
|
||||
})),
|
||||
})) ?? []),
|
||||
...Constants.Themes.map(theme => ({
|
||||
type: "Change Builder Theme",
|
||||
name: theme.name,
|
||||
|
@ -208,8 +208,8 @@
|
|||
|
||||
async function deployApp() {
|
||||
try {
|
||||
await API.deployAppChanges()
|
||||
notifications.success("Application published successfully")
|
||||
await API.publishAppChanges($store.appId)
|
||||
notifications.success("App published successfully")
|
||||
} catch (error) {
|
||||
notifications.error("Error publishing app")
|
||||
}
|
||||
|
@ -237,11 +237,11 @@
|
|||
<Input bind:value={search} quiet placeholder="Search for command" />
|
||||
</div>
|
||||
<div class="commands">
|
||||
{#each categories as [name, results], catIdx}
|
||||
{#each categories as [name, results]}
|
||||
<div class="category">
|
||||
<Detail>{name}</Detail>
|
||||
<div class="options">
|
||||
{#each results as command, cmdIdx}
|
||||
{#each results as command}
|
||||
<div
|
||||
class="command"
|
||||
on:click={() => runAction(command)}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
closeBrackets,
|
||||
completionKeymap,
|
||||
closeBracketsKeymap,
|
||||
acceptCompletion,
|
||||
completionStatus,
|
||||
} from "@codemirror/autocomplete"
|
||||
import {
|
||||
EditorView,
|
||||
|
@ -35,7 +37,8 @@
|
|||
defaultKeymap,
|
||||
historyKeymap,
|
||||
history,
|
||||
indentWithTab,
|
||||
indentMore,
|
||||
indentLess,
|
||||
} from "@codemirror/commands"
|
||||
import { Compartment } from "@codemirror/state"
|
||||
import { javascript } from "@codemirror/lang-javascript"
|
||||
|
@ -109,6 +112,22 @@
|
|||
let isDark = !currentTheme.includes("light")
|
||||
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 baseMap = [
|
||||
...closeBracketsKeymap,
|
||||
|
@ -116,7 +135,7 @@
|
|||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
indentWithTabCustom,
|
||||
]
|
||||
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>
|
||||
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html substituteSize(svgHtml)}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
import { licensing } from "stores/portal"
|
||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
|
||||
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan
|
||||
$: isBusinessAndAbove =
|
||||
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
|
||||
|
||||
let show
|
||||
let hide
|
||||
|
@ -55,22 +56,22 @@
|
|||
<div class="divider" />
|
||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
||||
<a
|
||||
href={isPremiumUser
|
||||
href={isBusinessAndAbove
|
||||
? "mailto:support@budibase.com"
|
||||
: "/builder/portal/account/usage"}
|
||||
>
|
||||
<div class="premiumLinkContent" class:disabled={!isPremiumUser}>
|
||||
<div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||
</div>
|
||||
<Body size="S">Email support</Body>
|
||||
</div>
|
||||
{#if !isPremiumUser}
|
||||
{#if !isBusinessAndAbove}
|
||||
<div class="premiumBadge">
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||
</div>
|
||||
<Body size="XS">Premium</Body>
|
||||
<Body size="XS">Business</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -18,12 +20,15 @@
|
|||
export let rightAlignIcon = false
|
||||
export let id
|
||||
export let showTooltip = false
|
||||
export let selectedBy = null
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let contentRef
|
||||
|
||||
$: selected && contentRef && scrollToView()
|
||||
$: style = getStyle(indentLevel, selectedBy)
|
||||
|
||||
const onClick = () => {
|
||||
scrollToView()
|
||||
|
@ -42,6 +47,14 @@
|
|||
const bounds = contentRef.getBoundingClientRect()
|
||||
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>
|
||||
|
||||
<div
|
||||
|
@ -51,8 +64,7 @@
|
|||
class:withActions
|
||||
class:scrollable
|
||||
class:highlighted
|
||||
style={`padding-left: calc(${indentLevel * 14}px)`}
|
||||
{draggable}
|
||||
class:selectedBy
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
|
@ -61,6 +73,8 @@
|
|||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
{id}
|
||||
{style}
|
||||
{draggable}
|
||||
>
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
|
@ -85,12 +99,19 @@
|
|||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/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}
|
||||
<div class="actions">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $$slots.right}
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
|
@ -119,13 +140,16 @@
|
|||
}
|
||||
.nav-item.highlighted {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-item:hover {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.nav-item:hover .actions {
|
||||
visibility: visible;
|
||||
|
@ -197,6 +221,9 @@
|
|||
color: var(--spectrum-global-color-gray-900);
|
||||
order: 2;
|
||||
width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.scrollable .text {
|
||||
flex: 0 0 auto;
|
||||
|
|
|
@ -208,7 +208,9 @@
|
|||
<div class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the
|
||||
guide
|
||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
||||
<a href="https://handlebarsjs.com/guide/" target="_blank"
|
||||
>here</a
|
||||
>
|
||||
for more details.
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
@ -88,6 +88,7 @@
|
|||
{/if}
|
||||
{#if hoverTarget.description}
|
||||
<div class="helper__description">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html hoverTarget.description}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -124,7 +125,6 @@
|
|||
/>
|
||||
</span>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="search-input-icon"
|
||||
on:click={() => {
|
||||
|
@ -162,7 +162,6 @@
|
|||
</div>
|
||||
<ul>
|
||||
{#each category.bindings as binding}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li
|
||||
class="binding"
|
||||
on:mouseenter={e => {
|
||||
|
|
|
@ -10,19 +10,17 @@
|
|||
Link,
|
||||
Modal,
|
||||
StatusLight,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { store } from "builderStore"
|
||||
import { deploymentStore, store, isOnlyUser } from "builderStore"
|
||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
@ -34,37 +32,31 @@
|
|||
let updateAppModal
|
||||
let revertModal
|
||||
let versionModal
|
||||
|
||||
let appActionPopover
|
||||
let appActionPopoverOpen = false
|
||||
let appActionPopoverAnchor
|
||||
|
||||
let publishing = false
|
||||
|
||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||
|
||||
$: deployments = []
|
||||
$: latestDeployments = deployments
|
||||
$: latestDeployments = $deploymentStore
|
||||
.filter(deployment => deployment.status === "SUCCESS")
|
||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||
|
||||
$: isPublished =
|
||||
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||
|
||||
$: updateAvailable =
|
||||
$store.upgradableVersion &&
|
||||
$store.version &&
|
||||
$store.upgradableVersion !== $store.version
|
||||
|
||||
$: canPublish = !publishing && loaded
|
||||
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||
await store.actions.initialise(applicationPkg)
|
||||
}
|
||||
|
||||
const updateDeploymentString = () => {
|
||||
const getLastDeployedString = deployments => {
|
||||
return deployments?.length
|
||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||
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 = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
|
@ -116,14 +87,11 @@
|
|||
async function publishApp() {
|
||||
try {
|
||||
publishing = true
|
||||
|
||||
await API.publishAppChanges($store.appId)
|
||||
|
||||
notifications.send("App published", {
|
||||
notifications.send("App published successfully", {
|
||||
type: "success",
|
||||
icon: "GlobeCheck",
|
||||
})
|
||||
|
||||
await completePublish()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -163,210 +131,201 @@
|
|||
const completePublish = async () => {
|
||||
try {
|
||||
await apps.load()
|
||||
deployments = await fetchDeployments()
|
||||
await deploymentStore.actions.load()
|
||||
} catch (err) {
|
||||
notifications.error("Error refreshing app")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!$apps.length) {
|
||||
await apps.load()
|
||||
}
|
||||
deployments = await fetchDeployments()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $store.hasLock}
|
||||
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
||||
<div class="action-buttons">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#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="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
{#if updateAvailable && $isOnlyUser}
|
||||
<div class="app-action-button version" on:click={versionModal.show}>
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
<ActionButton quiet>
|
||||
<StatusLight notice />
|
||||
Update
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<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>
|
||||
{/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>
|
||||
<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">
|
||||
<!-- 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>
|
||||
</div>
|
||||
</TourWrap>
|
||||
|
||||
<Body size="S">
|
||||
<span class="publish-popover-status">
|
||||
{#if isPublished}
|
||||
<span class="status-text">
|
||||
{updateDeploymentString(deployments)}
|
||||
</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 class="app-action-button preview">
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
<div
|
||||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
}
|
||||
}}
|
||||
onUpdateComplete={async () => {
|
||||
await initialiseApp()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<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} />
|
||||
<VersionModal hideIcon bind:this={versionModal} />
|
||||
{:else}
|
||||
<div class="app-action-button preview-locked">
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
</ActionButton>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-status">
|
||||
{#if isPublished}
|
||||
<span class="status-text">
|
||||
{lastDeployed}
|
||||
</span>
|
||||
<span class="unpublish-link">
|
||||
<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>
|
||||
{/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>
|
||||
.app-action-popover-content {
|
||||
|
@ -450,10 +409,6 @@
|
|||
gap: var(--spectrum-actionbutton-icon-gap);
|
||||
}
|
||||
|
||||
.app-action-button.preview-locked {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.app-action {
|
||||
display: flex;
|
||||
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 BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: DrawerBindableInput,
|
||||
|
@ -44,6 +45,7 @@ const componentMap = {
|
|||
schema: SchemaSelect,
|
||||
section: SectionSelect,
|
||||
filter: FilterEditor,
|
||||
"filter/relationship": RelationshipFilterEditor,
|
||||
url: URLSelect,
|
||||
fieldConfiguration: FieldConfiguration,
|
||||
columns: ColumnEditor,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
makeStateBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
@ -29,6 +30,26 @@
|
|||
let actionQuery
|
||||
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
|
||||
if (selectedAction && !selectedAction.parameters) {
|
||||
|
@ -125,8 +146,9 @@
|
|||
actions = e.detail.items
|
||||
}
|
||||
|
||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||
let allBindings = []
|
||||
let cloneActionBindings = cloneDeep(actionBindings)
|
||||
if (!actions) {
|
||||
return []
|
||||
}
|
||||
|
@ -144,11 +166,19 @@
|
|||
.forEach(action => {
|
||||
// Check we have a binding for this action, and generate one if not
|
||||
const stateBinding = makeStateBinding(action.parameters.key)
|
||||
const hasKey = bindings.some(binding => {
|
||||
const hasKey = actionBindings.some(binding => {
|
||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||
})
|
||||
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
|
||||
|
@ -164,17 +194,23 @@
|
|||
.filter(index => index !== undefined)
|
||||
|
||||
// Based on the above, filter out the asynchronous automations from the bindings
|
||||
if (asynchronousAutomationIndexes) {
|
||||
allBindings = eventContextBindings
|
||||
.filter((binding, index) => {
|
||||
let contextBindings = asynchronousAutomationIndexes
|
||||
? eventContextBindings.filter((binding, index) => {
|
||||
return !asynchronousAutomationIndexes.includes(index)
|
||||
})
|
||||
.concat(bindings)
|
||||
} else {
|
||||
allBindings = eventContextBindings.concat(bindings)
|
||||
}
|
||||
: eventContextBindings
|
||||
|
||||
allBindings = contextBindings
|
||||
.concat(cloneActionBindings)
|
||||
.concat(allBindings)
|
||||
|
||||
return allBindings
|
||||
}
|
||||
|
||||
const toDisplay = eventKey => {
|
||||
const type = actionTypes.find(action => action.name == eventKey)
|
||||
return type?.displayName || type?.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
|
@ -200,7 +236,9 @@
|
|||
<ul>
|
||||
{#each category as actionType}
|
||||
<li on:click={onAddAction(actionType)}>
|
||||
<span class="action-name">{actionType.name}</span>
|
||||
<span class="action-name">
|
||||
{actionType.displayName || actionType.name}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
@ -231,7 +269,7 @@
|
|||
>
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
<div class="action-header">
|
||||
{index + 1}. {action[EVENT_TYPE_KEY]}
|
||||
{index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
|
||||
</div>
|
||||
<Icon
|
||||
name="Close"
|
||||
|
|
|
@ -70,8 +70,9 @@
|
|||
} set`
|
||||
</script>
|
||||
|
||||
<div class="action-count">{actionText}</div>
|
||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
||||
<div class="action-editor">
|
||||
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer} title={"Actions"}>
|
||||
<svelte:fragment slot="description">
|
||||
|
@ -89,9 +90,7 @@
|
|||
</Drawer>
|
||||
|
||||
<style>
|
||||
.action-count {
|
||||
padding-top: 6px;
|
||||
padding-bottom: var(--spacing-s);
|
||||
font-weight: 600;
|
||||
.action-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
|
||||
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
|
@ -10,47 +10,59 @@
|
|||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
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}
|
||||
<Body size="small">Please specify one or more rows to delete.</Body>
|
||||
<div class="params">
|
||||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
{/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>
|
||||
|
||||
<style>
|
||||
.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;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 60px 1fr;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
$: hasAutomations = automations && automations.length > 0
|
||||
$: selectedAutomation = automations?.find(
|
||||
a => a._id === parameters?.automationId
|
||||
)
|
||||
|
@ -145,12 +144,6 @@
|
|||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.synchronous-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Delete Row",
|
||||
"displayName": "Delete Rows",
|
||||
"type": "data",
|
||||
"component": "DeleteRow"
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = allowCellEditing
|
||||
|
@ -31,6 +32,17 @@
|
|||
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 schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
||||
|
@ -76,7 +88,7 @@
|
|||
</script>
|
||||
|
||||
<div class="column-editor">
|
||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||
<ActionButton on:click={open}>{text}</ActionButton>
|
||||
</div>
|
||||
<Drawer bind:this={drawer} title="Columns">
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
|
|
|
@ -8,32 +8,39 @@
|
|||
getSchemaForDatasource,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
|
||||
export let componentInstance
|
||||
export let value = []
|
||||
|
||||
const convertOldColumnFormat = oldColumns => {
|
||||
if (typeof oldColumns?.[0] === "string") {
|
||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||
}
|
||||
}
|
||||
|
||||
$: convertOldColumnFormat(value)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: convertOldColumnFormat(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = Object.keys(schema || {})
|
||||
$: sanitisedValue = getValidColumns(value, options)
|
||||
$: 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 schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
@ -79,7 +86,10 @@
|
|||
}
|
||||
</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">
|
||||
<svelte:fragment slot="description">
|
||||
Configure the fields in your form.
|
||||
|
@ -87,3 +97,9 @@
|
|||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.field-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -192,7 +192,7 @@
|
|||
<Label>Filters</Label>
|
||||
</div>
|
||||
<div class="fields">
|
||||
{#each rawFilters as filter, idx}
|
||||
{#each rawFilters as filter}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
export let value = []
|
||||
export let componentInstance
|
||||
export let bindings = []
|
||||
export let schema = null
|
||||
|
||||
let drawer
|
||||
|
||||
$: tempValue = value
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: dsSchema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||
$: schemaFields = Object.values(schema || dsSchema || {})
|
||||
$: text = getText(value?.filter(filter => filter.field))
|
||||
|
||||
async function saveFilter() {
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<i class={icon} />
|
||||
{:else}
|
||||
<span>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html text}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
$: nullishValue = value == null || value === ""
|
||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||
$: tempValue = safeValue
|
||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||
|
||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
||||
|
@ -104,6 +103,7 @@
|
|||
/>
|
||||
</div>
|
||||
{#if info}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div class="text">{@html info}</div>
|
||||
{/if}
|
||||
</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 type
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let drawer
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: text = getText(value)
|
||||
|
||||
const save = () => {
|
||||
dispatch("change", value)
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
const getText = rules => {
|
||||
if (!rules?.length) {
|
||||
return "No rules set"
|
||||
} else {
|
||||
return `${rules.length} rule${rules.length === 1 ? "" : "s"} set`
|
||||
}
|
||||
}
|
||||
</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">
|
||||
<svelte:fragment slot="description">
|
||||
Configure validation rules for this field.
|
||||
|
@ -31,3 +44,9 @@
|
|||
{componentDefinition}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.validation-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
Tab,
|
||||
Modal,
|
||||
ModalContent,
|
||||
notifications,
|
||||
Divider,
|
||||
} from "@budibase/bbui"
|
||||
import { notifications, Divider } from "@budibase/bbui"
|
||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||
|
@ -28,6 +29,7 @@
|
|||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
||||
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||
|
||||
export let query
|
||||
|
||||
|
@ -47,6 +49,7 @@
|
|||
let saveModal
|
||||
let override = false
|
||||
let navigateTo = null
|
||||
let nameError = null
|
||||
|
||||
// seed the transformer
|
||||
if (query && !query.transformer) {
|
||||
|
@ -77,7 +80,7 @@
|
|||
$: queryConfig = integrationInfo?.query
|
||||
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
||||
$: 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
|
||||
$: {
|
||||
|
@ -139,9 +142,10 @@
|
|||
queryStr = JSON.stringify(query)
|
||||
}
|
||||
|
||||
notifications.success("Query saved successfully")
|
||||
return response
|
||||
} catch (error) {
|
||||
notifications.error("Error saving query")
|
||||
notifications.error(error.message || "Error saving query")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -183,8 +187,14 @@
|
|||
value={query.name}
|
||||
on:input={e => {
|
||||
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>
|
||||
{#if queryConfig}
|
||||
|
@ -250,9 +260,9 @@
|
|||
size="L"
|
||||
/>
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Add a JavaScript function to transform the query result.</Body
|
||||
>
|
||||
<Body size="S">
|
||||
Add a JavaScript function to transform the query result.
|
||||
</Body>
|
||||
<CodeMirrorEditor
|
||||
height={200}
|
||||
label="Transformer"
|
||||
|
@ -264,13 +274,12 @@
|
|||
</div>
|
||||
<div class="viewer-controls">
|
||||
<Heading size="S">Results</Heading>
|
||||
<ButtonGroup gap="XS">
|
||||
<ButtonGroup gap="S">
|
||||
<Button
|
||||
cta
|
||||
disabled={queryInvalid}
|
||||
on:click={async () => {
|
||||
await saveQuery()
|
||||
notifications.success(`Query saved successfully`)
|
||||
// Go to the correct URL if we just created a new query
|
||||
if (!query._rev) {
|
||||
$goto(`../../${query._id}`)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 { TOURS } from "./tours.js"
|
||||
import { goto, layout, isActive } from "@roxi/routify"
|
||||
|
@ -10,17 +10,20 @@
|
|||
let tourStep
|
||||
let tourStepIdx
|
||||
let lastStep
|
||||
let skipping = false
|
||||
|
||||
$: tourNodes = { ...$store.tourNodes }
|
||||
$: tourKey = $store.tourKey
|
||||
$: tourStepKey = $store.tourStepKey
|
||||
$: tour = TOURS[tourKey]
|
||||
$: tourOnSkip = tour?.onSkip
|
||||
|
||||
const updateTourStep = (targetStepKey, tourKey) => {
|
||||
if (!tourKey) {
|
||||
return
|
||||
}
|
||||
if (!tourSteps?.length) {
|
||||
tourSteps = [...TOURS[tourKey]]
|
||||
tourSteps = [...tour.steps]
|
||||
}
|
||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||
|
@ -71,23 +74,8 @@
|
|||
tourStep.onComplete()
|
||||
}
|
||||
popover.hide()
|
||||
if (tourStep.endRoute) {
|
||||
$goto(tourStep.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")
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,16 +120,23 @@
|
|||
</Body>
|
||||
<div class="tour-footer">
|
||||
<div class="tour-navigation">
|
||||
{#if tourStepIdx > 0}
|
||||
<Button
|
||||
{#if typeof tourOnSkip === "function"}
|
||||
<Link
|
||||
secondary
|
||||
on:click={previousStep}
|
||||
disabled={tourStepIdx == 0}
|
||||
quiet
|
||||
on:click={() => {
|
||||
skipping = true
|
||||
tourOnSkip()
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}}
|
||||
disabled={skipping}
|
||||
>
|
||||
<div>Back</div>
|
||||
</Button>
|
||||
Skip
|
||||
</Link>
|
||||
{/if}
|
||||
<Button cta on:click={nextStep}>
|
||||
<Button cta on:click={nextStep} disabled={skipping}>
|
||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -157,9 +152,10 @@
|
|||
padding: var(--spacing-xl);
|
||||
}
|
||||
.tour-navigation {
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
grid-gap: var(--spacing-xl);
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
.tour-body :global(.feature-list) {
|
||||
margin-bottom: 0px;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
const registerTourNode = (tourKey, stepKey) => {
|
||||
if (ready && !registered && tourKey) {
|
||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
||||
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||
if (!currentTourStep) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
|||
import analytics from "analytics"
|
||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||
import { API } from "api"
|
||||
|
||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||
|
||||
export const TOUR_STEP_KEYS = {
|
||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
|||
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 => {
|
||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||
eventSource: EventSource.PORTAL,
|
||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
|||
|
||||
const getTours = () => {
|
||||
return {
|
||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||
title: "Data",
|
||||
route: "/builder/app/:application/data",
|
||||
layout: OnboardingData,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||
onLoad: async () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
||||
steps: [
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||
title: "Data",
|
||||
route: "/builder/app/:application/data",
|
||||
layout: OnboardingData,
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||
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 })
|
||||
},
|
||||
{
|
||||
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)
|
||||
endRoute: "/builder/app/:application/data",
|
||||
},
|
||||
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
||||
steps: [
|
||||
{
|
||||
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: 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 url
|
||||
export let active = false
|
||||
export let disabled = false
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
<a on:click href={url} class:active>
|
||||
{text || ""}
|
||||
</a>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span on:click class:active>
|
||||
{text || ""}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="side-nav-item">
|
||||
{#if url}
|
||||
<a class="text" on:click href={url} class:active class:disabled>
|
||||
{text || ""}
|
||||
</a>
|
||||
{:else}
|
||||
<div class="text" on:click class:active class:disabled>
|
||||
{text || ""}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
a,
|
||||
span {
|
||||
.side-nav-item {
|
||||
position: relative;
|
||||
}
|
||||
.text {
|
||||
display: block;
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
border-radius: 4px;
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.active,
|
||||
span:hover,
|
||||
a:hover {
|
||||
.text:hover {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--spectrum-global-color-gray-500) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue