Merge branch 'feature/offline-license' into tests/offline-license
This commit is contained in:
commit
0d08a38ec7
|
@ -1,19 +0,0 @@
|
||||||
# Configuration for probot-stale - https://github.com/probot/stale
|
|
||||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
|
||||||
daysUntilStale: 60
|
|
||||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
|
||||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
|
||||||
daysUntilClose: false
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- pinned
|
|
||||||
- security
|
|
||||||
- roadmap
|
|
||||||
# Label to use when marking an issue as stale
|
|
||||||
staleLabel: stale
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
||||||
closeComment: false
|
|
|
@ -12,9 +12,6 @@ on:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -162,7 +159,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cd qa-core
|
cd qa-core
|
||||||
yarn setup
|
yarn setup
|
||||||
yarn test:ci
|
yarn serve:test:self:ci
|
||||||
env:
|
env:
|
||||||
BB_ADMIN_USER_EMAIL: admin
|
BB_ADMIN_USER_EMAIL: admin
|
||||||
BB_ADMIN_USER_PASSWORD: admin
|
BB_ADMIN_USER_PASSWORD: admin
|
||||||
|
|
|
@ -6,7 +6,7 @@ concurrency:
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*-alpha.*
|
- ".*-alpha.*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -6,9 +6,9 @@ concurrency:
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
# Exclude all pre-releases
|
# Exclude all pre-releases
|
||||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
- "!*[0-9]+.[0-9]+.[0-9]+-*"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
|
@ -98,7 +98,7 @@ jobs:
|
||||||
git fetch
|
git fetch
|
||||||
mkdir sync
|
mkdir sync
|
||||||
echo "Packaging chart to sync dir"
|
echo "Packaging chart to sync dir"
|
||||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync
|
||||||
echo "Packaging successful"
|
echo "Packaging successful"
|
||||||
git checkout gh-pages
|
git checkout gh-pages
|
||||||
echo "Indexing helm repo"
|
echo "Indexing helm repo"
|
||||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
|
||||||
release_tag=v${{ env.RELEASE_VERSION }}
|
release_tag=${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
# Pull apps and worker images
|
# Pull apps and worker images
|
||||||
docker pull budibase/apps:$release_tag
|
docker pull budibase/apps:$release_tag
|
||||||
|
@ -108,8 +108,8 @@ jobs:
|
||||||
- name: Perform Github Release
|
- name: Perform Github Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
name: v${{ env.RELEASE_VERSION }}
|
name: ${{ env.RELEASE_VERSION }}
|
||||||
tag_name: v${{ env.RELEASE_VERSION }}
|
tag_name: ${{ env.RELEASE_VERSION }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
packages/cli/build/cli-win.exe
|
packages/cli/build/cli-win.exe
|
||||||
|
|
|
@ -71,7 +71,7 @@ jobs:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile
|
file: ./hosting/single/Dockerfile
|
||||||
- name: Tag and release Budibase Azure App Service docker image
|
- name: Tag and release Budibase Azure App Service docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
|
@ -80,5 +80,5 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
build-args: TARGETBUILD=aas
|
build-args: TARGETBUILD=aas
|
||||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile
|
file: ./hosting/single/Dockerfile
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Close stale issues and PRs # https://github.com/actions/stale
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 1 * * *' # 1:30 every morning
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v8
|
||||||
|
with:
|
||||||
|
# stale rules
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-pr-stale: 7
|
||||||
|
stale-issue-label: stale
|
||||||
|
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
|
||||||
|
|
||||||
|
# close rules
|
||||||
|
# days after being marked as stale to close
|
||||||
|
days-before-close: 30
|
||||||
|
close-issue-label: closed-stale
|
||||||
|
close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
|
||||||
|
days-before-pr-close: 7
|
||||||
|
|
||||||
|
# exemptions
|
||||||
|
exempt-pr-labels: pinned,security,roadmap
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
nodejs 14.20.1
|
nodejs 14.21.3
|
||||||
python 3.10.0
|
python 3.10.0
|
|
@ -209,7 +209,7 @@ services:
|
||||||
# Override values in couchDB subchart
|
# Override values in couchDB subchart
|
||||||
couchdb:
|
couchdb:
|
||||||
## clusterSize is the initial size of the CouchDB cluster.
|
## clusterSize is the initial size of the CouchDB cluster.
|
||||||
clusterSize: 3
|
clusterSize: 1
|
||||||
allowAdminParty: false
|
allowAdminParty: false
|
||||||
|
|
||||||
# Secret Management
|
# Secret Management
|
||||||
|
|
|
@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
|
||||||
|
|
||||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||||
PLUGINS_DIR=
|
PLUGINS_DIR=
|
||||||
|
ROLLING_LOG_MAX_SIZE=
|
|
@ -22,6 +22,16 @@ server {
|
||||||
proxy_pass http://127.0.0.1:4001;
|
proxy_pass http://127.0.0.1:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /embed {
|
||||||
|
rewrite /embed/(.*) /app/$1 break;
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header x-budibase-embed "true";
|
||||||
|
add_header x-budibase-embed "true";
|
||||||
|
add_header Content-Security-Policy "frame-ancestors *";
|
||||||
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
proxy_pass http://127.0.0.1:4001;
|
proxy_pass http://127.0.0.1:4001;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.8.2-alpha.5",
|
"version": "2.8.16-alpha.3",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
12
nx.json
12
nx.json
|
@ -1,9 +1,13 @@
|
||||||
{
|
{
|
||||||
"tasksRunnerOptions": {
|
"tasksRunnerOptions": {
|
||||||
"default": {
|
"default": {
|
||||||
"runner": "nx/tasks-runners/default",
|
"runner": "nx-cloud",
|
||||||
"options": {
|
"options": {
|
||||||
"cacheableOperations": ["build", "test"]
|
"cacheableOperations": [
|
||||||
|
"build",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11,7 +15,9 @@
|
||||||
"dev:builder": {
|
"dev:builder": {
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": ["@budibase/string-templates"],
|
"projects": [
|
||||||
|
"@budibase/string-templates"
|
||||||
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
19
package.json
19
package.json
|
@ -3,7 +3,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@nx/js": "16.2.1",
|
"@nx/js": "16.4.3",
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"esbuild": "^0.17.18",
|
"esbuild": "^0.17.18",
|
||||||
|
@ -13,9 +13,11 @@
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.0.2",
|
"lerna": "7.1.1",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
|
"nx": "16.4.3",
|
||||||
|
"nx-cloud": "16.0.5",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
@ -44,15 +46,15 @@
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
"nuke:docker": "lerna run --stream --parallel dev:stack:nuke",
|
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
"dev": "yarn run kill-all && lerna run --stream dev:builder --stream",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && yarn build --projects=@budibase/client && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
||||||
|
@ -106,6 +108,5 @@
|
||||||
"@budibase/string-templates": "0.0.0",
|
"@budibase/string-templates": "0.0.0",
|
||||||
"@budibase/types": "0.0.0"
|
"@budibase/types": "0.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-find": "7.2.2",
|
"pouchdb-find": "7.2.2",
|
||||||
"redlock": "4.2.0",
|
"redlock": "4.2.0",
|
||||||
|
"rotating-file-stream": "3.1.0",
|
||||||
"sanitize-s3-objectkey": "0.0.1",
|
"sanitize-s3-objectkey": "0.0.1",
|
||||||
"semver": "7.3.7",
|
"semver": "7.3.7",
|
||||||
"tar-fs": "2.1.1",
|
"tar-fs": "2.1.1",
|
||||||
|
|
|
@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const dbUser = await db.get(userId)
|
const dbUser = await db.get<any>(userId)
|
||||||
|
|
||||||
//Do not overwrite the refresh token if a valid one is not provided.
|
//Do not overwrite the refresh token if a valid one is not provided.
|
||||||
if (typeof details.refreshToken !== "string") {
|
if (typeof details.refreshToken !== "string") {
|
||||||
|
|
|
@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
|
||||||
*/
|
*/
|
||||||
async function populateFromDB(userId: string, tenantId: string) {
|
async function populateFromDB(userId: string, tenantId: string) {
|
||||||
const db = tenancy.getTenantDB(tenantId)
|
const db = tenancy.getTenantDB(tenantId)
|
||||||
const user = await db.get(userId)
|
const user = await db.get<any>(userId)
|
||||||
user.budibaseAccess = true
|
user.budibaseAccess = true
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const account = await accounts.getAccount(user.email)
|
const account = await accounts.getAccount(user.email)
|
||||||
|
|
|
@ -433,6 +433,9 @@ export class QueryBuilder<T> {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return `(*:* AND !${key}:${value})`
|
||||||
|
}
|
||||||
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ export async function createUserIndex() {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get<any>("_design/database")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.status === 404) {
|
if (err.status === 404) {
|
||||||
designDoc = { _id: "_design/database" }
|
designDoc = { _id: "_design/database" }
|
||||||
|
|
|
@ -47,7 +47,10 @@ function httpLogging() {
|
||||||
return process.env.HTTP_LOGGING
|
return process.env.HTTP_LOGGING
|
||||||
}
|
}
|
||||||
|
|
||||||
function findVersion() {
|
function getPackageJsonFields(): {
|
||||||
|
VERSION: string
|
||||||
|
SERVICE_NAME: string
|
||||||
|
} {
|
||||||
function findFileInAncestors(
|
function findFileInAncestors(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
currentDir: string
|
currentDir: string
|
||||||
|
@ -69,10 +72,14 @@ function findVersion() {
|
||||||
try {
|
try {
|
||||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||||
return JSON.parse(content).version
|
const parsedContent = JSON.parse(content)
|
||||||
|
return {
|
||||||
|
VERSION: parsedContent.version,
|
||||||
|
SERVICE_NAME: parsedContent.name,
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||||
return undefined
|
return { VERSION: "", SERVICE_NAME: "" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +161,7 @@ const environment = {
|
||||||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||||
: false,
|
: false,
|
||||||
VERSION: findVersion(),
|
...getPackageJsonFields(),
|
||||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
|
@ -162,6 +169,7 @@ const environment = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
environment[key] = value
|
environment[key] = value
|
||||||
},
|
},
|
||||||
|
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up any environment variable edge cases
|
// clean up any environment variable edge cases
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
export * as correlation from "./correlation/correlation"
|
export * as correlation from "./correlation/correlation"
|
||||||
export { logger } from "./pino/logger"
|
export { logger } from "./pino/logger"
|
||||||
export * from "./alerts"
|
export * from "./alerts"
|
||||||
|
export * as system from "./system"
|
||||||
// turn off or on context logging i.e. tenantId, appId etc
|
|
||||||
export let LOG_CONTEXT = true
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import env from "../../environment"
|
|
||||||
import pino, { LoggerOptions } from "pino"
|
import pino, { LoggerOptions } from "pino"
|
||||||
|
import pinoPretty from "pino-pretty"
|
||||||
|
|
||||||
|
import { IdentityType } from "@budibase/types"
|
||||||
|
import env from "../../environment"
|
||||||
import * as context from "../../context"
|
import * as context from "../../context"
|
||||||
import * as correlation from "../correlation"
|
import * as correlation from "../correlation"
|
||||||
import { IdentityType } from "@budibase/types"
|
|
||||||
import { LOG_CONTEXT } from "../index"
|
import { localFileDestination } from "../system"
|
||||||
|
|
||||||
// LOGGER
|
// LOGGER
|
||||||
|
|
||||||
|
@ -16,22 +19,27 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
return { level: label.toUpperCase() }
|
return { level: label.toUpperCase() }
|
||||||
},
|
},
|
||||||
bindings: () => {
|
bindings: () => {
|
||||||
return {}
|
return {
|
||||||
|
service: env.SERVICE_NAME,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const destinations: pino.DestinationStream[] = []
|
||||||
|
|
||||||
if (env.isDev()) {
|
if (env.isDev()) {
|
||||||
pinoOptions.transport = {
|
destinations.push(pinoPretty({ singleLine: true }))
|
||||||
target: "pino-pretty",
|
|
||||||
options: {
|
|
||||||
singleLine: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pinoInstance = pino(pinoOptions)
|
if (env.SELF_HOSTED) {
|
||||||
|
destinations.push(localFileDestination())
|
||||||
|
}
|
||||||
|
|
||||||
|
pinoInstance = destinations.length
|
||||||
|
? pino(pinoOptions, pino.multistream(destinations))
|
||||||
|
: pino(pinoOptions)
|
||||||
|
|
||||||
// CONSOLE OVERRIDES
|
// CONSOLE OVERRIDES
|
||||||
|
|
||||||
|
@ -83,15 +91,13 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
|
|
||||||
let contextObject = {}
|
let contextObject = {}
|
||||||
|
|
||||||
if (LOG_CONTEXT) {
|
contextObject = {
|
||||||
contextObject = {
|
tenantId: getTenantId(),
|
||||||
tenantId: getTenantId(),
|
appId: getAppId(),
|
||||||
appId: getAppId(),
|
automationId: getAutomationId(),
|
||||||
automationId: getAutomationId(),
|
identityId: identity?._id,
|
||||||
identityId: identity?._id,
|
identityType: identity?.type,
|
||||||
identityType: identity?.type,
|
correlationId: correlation.getId(),
|
||||||
correlationId: correlation.getId(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergingObject: any = {
|
const mergingObject: any = {
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import * as rfs from "rotating-file-stream"
|
||||||
|
|
||||||
|
import env from "../environment"
|
||||||
|
import { budibaseTempDir } from "../objectStore"
|
||||||
|
|
||||||
|
const logsFileName = `budibase.log`
|
||||||
|
const budibaseLogsHistoryFileName = "budibase-logs-history.txt"
|
||||||
|
|
||||||
|
const logsPath = path.join(budibaseTempDir(), "systemlogs")
|
||||||
|
|
||||||
|
function getFullPath(fileName: string) {
|
||||||
|
return path.join(logsPath, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSingleFileMaxSizeInfo(totalMaxSize: string) {
|
||||||
|
const regex = /(\d+)([A-Za-z])/
|
||||||
|
const match = totalMaxSize?.match(regex)
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`totalMaxSize does not have a valid value`, {
|
||||||
|
totalMaxSize,
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = +match[1]
|
||||||
|
const unit = match[2]
|
||||||
|
if (size === 1) {
|
||||||
|
switch (unit) {
|
||||||
|
case "B":
|
||||||
|
return { size: `${size}B`, totalHistoryFiles: 1 }
|
||||||
|
case "K":
|
||||||
|
return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 }
|
||||||
|
case "M":
|
||||||
|
return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 }
|
||||||
|
case "G":
|
||||||
|
return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 }
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size % 2 === 0) {
|
||||||
|
return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { size: `1${unit}`, totalHistoryFiles: size - 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localFileDestination() {
|
||||||
|
const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE)
|
||||||
|
const outFile = rfs.createStream(logsFileName, {
|
||||||
|
// As we have a rolling size, we want to half the max size
|
||||||
|
size: fileInfo?.size,
|
||||||
|
path: logsPath,
|
||||||
|
maxFiles: fileInfo?.totalHistoryFiles || 1,
|
||||||
|
immutable: true,
|
||||||
|
history: budibaseLogsHistoryFileName,
|
||||||
|
initialRotation: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return outFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogReadStream() {
|
||||||
|
const streams = []
|
||||||
|
const historyFile = getFullPath(budibaseLogsHistoryFileName)
|
||||||
|
if (fs.existsSync(historyFile)) {
|
||||||
|
const fileContent = fs.readFileSync(historyFile, "utf-8")
|
||||||
|
const historyFiles = fileContent.split("\n")
|
||||||
|
for (const historyFile of historyFiles.filter(x => x)) {
|
||||||
|
streams.push(fs.readFileSync(historyFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push(fs.readFileSync(getFullPath(logsFileName)))
|
||||||
|
|
||||||
|
const combinedContent = Buffer.concat(streams)
|
||||||
|
return combinedContent
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { getSingleFileMaxSizeInfo } from "../system"
|
||||||
|
|
||||||
|
describe("system", () => {
|
||||||
|
describe("getSingleFileMaxSizeInfo", () => {
|
||||||
|
it.each([
|
||||||
|
["100B", "50B"],
|
||||||
|
["200K", "100K"],
|
||||||
|
["20M", "10M"],
|
||||||
|
["4G", "2G"],
|
||||||
|
])(
|
||||||
|
"Halving even number (%s) returns halved size and 1 history file (%s)",
|
||||||
|
(totalValue, expectedMaxSize) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["5B", "1B", 4],
|
||||||
|
["17K", "1K", 16],
|
||||||
|
["21M", "1M", 20],
|
||||||
|
["3G", "1G", 2],
|
||||||
|
])(
|
||||||
|
"Halving an odd number (%s) returns as many files as size (-1) (%s)",
|
||||||
|
(totalValue, expectedMaxSize, totalHistoryFiles) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["1B", "1B"],
|
||||||
|
["1K", "500B"],
|
||||||
|
["1M", "500K"],
|
||||||
|
["1G", "500M"],
|
||||||
|
])(
|
||||||
|
"Halving '%s' returns halved unit (%s)",
|
||||||
|
(totalValue, expectedMaxSize) => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||||
|
expect(result).toEqual({
|
||||||
|
size: expectedMaxSize,
|
||||||
|
totalHistoryFiles: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each([[undefined], [""], ["50"], ["wrongvalue"]])(
|
||||||
|
"Halving wrongly formatted value ('%s') returns undefined",
|
||||||
|
totalValue => {
|
||||||
|
const result = getSingleFileMaxSizeInfo(totalValue!)
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
|
|
||||||
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
let user = await db.get(id)
|
let user = await db.get<User>(id)
|
||||||
if (opts?.cleanup) {
|
if (opts?.cleanup) {
|
||||||
user = removeUserPassword(user)
|
user = removeUserPassword(user) as User
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -16,48 +17,53 @@
|
||||||
export let tooltip = undefined
|
export let tooltip = undefined
|
||||||
export let newStyles = true
|
export let newStyles = true
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<AbsTooltip text={tooltip}>
|
||||||
{id}
|
<button
|
||||||
{type}
|
{id}
|
||||||
class:spectrum-Button--cta={cta}
|
{type}
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--warning={warning}
|
class:spectrum-Button--secondary={secondary}
|
||||||
class:spectrum-Button--overBackground={overBackground}
|
class:spectrum-Button--warning={warning}
|
||||||
class:spectrum-Button--quiet={quiet}
|
class:spectrum-Button--overBackground={overBackground}
|
||||||
class:new-styles={newStyles}
|
class:spectrum-Button--quiet={quiet}
|
||||||
class:active
|
class:new-styles={newStyles}
|
||||||
class:disabled
|
class:active
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class:is-disabled={disabled}
|
||||||
{disabled}
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
on:click|preventDefault
|
on:click|preventDefault={() => {
|
||||||
>
|
if (!disabled) {
|
||||||
{#if icon}
|
dispatch("click")
|
||||||
<svg
|
}
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
}}
|
||||||
focusable="false"
|
>
|
||||||
aria-hidden="true"
|
{#if icon}
|
||||||
aria-label={icon}
|
<svg
|
||||||
>
|
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
focusable="false"
|
||||||
</svg>
|
aria-hidden="true"
|
||||||
{/if}
|
aria-label={icon}
|
||||||
{#if $$slots}
|
>
|
||||||
<span class="spectrum-Button-label"><slot /></span>
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
{/if}
|
</svg>
|
||||||
{#if tooltip}
|
{/if}
|
||||||
<div class="tooltip">
|
{#if $$slots}
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
<span class="spectrum-Button-label"><slot /></span>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</button>
|
||||||
</button>
|
</AbsTooltip>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
button.is-disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -66,23 +72,6 @@
|
||||||
.active {
|
.active {
|
||||||
color: var(--spectrum-global-color-blue-600) !important;
|
color: var(--spectrum-global-color-blue-600) !important;
|
||||||
}
|
}
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
width: 160px;
|
|
||||||
text-align: center;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
left: 50%;
|
|
||||||
top: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
button:hover .tooltip {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.spectrum-Button--primary.new-styles {
|
.spectrum-Button--primary.new-styles {
|
||||||
background: var(--spectrum-global-color-gray-800);
|
background: var(--spectrum-global-color-gray-800);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
@ -96,10 +85,10 @@
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
.spectrum-Button--secondary.new-styles:not(.disabled):hover {
|
.spectrum-Button--secondary.new-styles:not(.is-disabled):hover {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.spectrum-Button--secondary.new-styles.disabled {
|
.spectrum-Button--secondary.new-styles.is-disabled {
|
||||||
color: var(--spectrum-global-color-gray-500);
|
color: var(--spectrum-global-color-gray-500);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="overlay" on:mousedown|self={() => (open = false)} />
|
<div class="overlay" on:mousedown|self={() => (open = false)} />
|
||||||
<div
|
<div
|
||||||
transition:fly={{ y: -20, duration: 200 }}
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
class="spectrum-Popover spectrum-Popover--bottom is-open"
|
class="spectrum-Popover spectrum-Popover--bottom is-open"
|
||||||
>
|
>
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
|
|
@ -90,6 +90,6 @@
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000);
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
overflow: visible;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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,6 +36,12 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
||||||
export { default as Page } from "./Layout/Page.svelte"
|
export { default as Page } from "./Layout/Page.svelte"
|
||||||
export { default as Link } from "./Link/Link.svelte"
|
export { default as Link } from "./Link/Link.svelte"
|
||||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||||
|
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
|
||||||
|
export {
|
||||||
|
default as AbsTooltip,
|
||||||
|
TooltipPosition,
|
||||||
|
TooltipType,
|
||||||
|
} from "./Tooltip/AbsTooltip.svelte"
|
||||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||||
export { default as Menu } from "./Menu/Menu.svelte"
|
export { default as Menu } from "./Menu/Menu.svelte"
|
||||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||||
|
|
|
@ -127,8 +127,12 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
let map = {}
|
let map = {}
|
||||||
$userStore.forEach(user => {
|
$userStore.forEach(user => {
|
||||||
if (user.builderMetadata?.selectedResourceId) {
|
const resource = user.builderMetadata?.selectedResourceId
|
||||||
map[user.builderMetadata?.selectedResourceId] = user
|
if (resource) {
|
||||||
|
if (!map[resource]) {
|
||||||
|
map[resource] = []
|
||||||
|
}
|
||||||
|
map[resource].push(user)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
|
|
|
@ -248,4 +248,36 @@ const automationActions = store => ({
|
||||||
}
|
}
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
|
replace: async (automationId, automation) => {
|
||||||
|
if (!automation) {
|
||||||
|
store.update(state => {
|
||||||
|
// Remove the automation
|
||||||
|
state.automations = state.automations.filter(
|
||||||
|
x => x._id !== automationId
|
||||||
|
)
|
||||||
|
// Select a new automation if required
|
||||||
|
if (automationId === state.selectedAutomationId) {
|
||||||
|
store.actions.select(state.automations[0]?._id)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const index = get(store).automations.findIndex(
|
||||||
|
x => x._id === automation._id
|
||||||
|
)
|
||||||
|
if (index === -1) {
|
||||||
|
// Automation addition
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
automations: [...state.automations, automation],
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Automation update
|
||||||
|
store.update(state => {
|
||||||
|
state.automations[index] = automation
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { createWebsocket } from "@budibase/frontend-core"
|
import { createWebsocket } from "@budibase/frontend-core"
|
||||||
import { userStore, store, deploymentStore } from "builderStore"
|
import {
|
||||||
|
userStore,
|
||||||
|
store,
|
||||||
|
deploymentStore,
|
||||||
|
automationStore,
|
||||||
|
} from "builderStore"
|
||||||
import { datasources, tables } from "stores/backend"
|
import { datasources, tables } from "stores/backend"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
@ -67,5 +72,10 @@ export const createBuilderWebsocket = appId => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Automations
|
||||||
|
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||||
|
automationStore.actions.replace(id, automation)
|
||||||
|
})
|
||||||
|
|
||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import {
|
||||||
|
automationStore,
|
||||||
|
selectedAutomation,
|
||||||
|
userSelectedResourceMap,
|
||||||
|
} from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -21,13 +25,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="automations-list">
|
<div class="automations-list">
|
||||||
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
|
{#each $automationStore.automations.sort(aut => aut.name) as automation}
|
||||||
<NavItem
|
<NavItem
|
||||||
border={idx > 0}
|
|
||||||
icon="ShareAndroid"
|
icon="ShareAndroid"
|
||||||
text={automation.name}
|
text={automation.name}
|
||||||
selected={automation._id === selectedAutomationId}
|
selected={automation._id === selectedAutomationId}
|
||||||
on:click={() => selectAutomation(automation._id)}
|
on:click={() => selectAutomation(automation._id)}
|
||||||
|
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||||
>
|
>
|
||||||
<EditAutomationPopover {automation} />
|
<EditAutomationPopover {automation} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -40,6 +44,5 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
<Panel title="Automations" borderRight>
|
<Panel title="Automations" borderRight>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Button cta on:click={modal.show}>Add automation</Button>
|
<Button cta on:click={modal.show}>Add automation</Button>
|
||||||
<AutomationList />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<AutomationList />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
Icon,
|
Icon,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
Detail,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import {
|
import {
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
jsAutocomplete,
|
hbAutocomplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
let drawer
|
let drawer
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
let inputData
|
let inputData
|
||||||
|
let codeBindingOpen = false
|
||||||
|
|
||||||
$: filters = lookForFilters(schemaProperties) || []
|
$: filters = lookForFilters(schemaProperties) || []
|
||||||
$: tempFilters = filters
|
$: tempFilters = filters
|
||||||
|
@ -70,6 +72,13 @@
|
||||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
$: codeMode =
|
||||||
|
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||||
|
|
||||||
|
$: stepCompletions =
|
||||||
|
codeMode === EditorModes.Handlebars
|
||||||
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
|
: []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO - Remove after November 2023
|
* TODO - Remove after November 2023
|
||||||
|
@ -489,6 +498,18 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "code"}
|
{:else if value.customType === "code"}
|
||||||
<CodeEditorModal>
|
<CodeEditorModal>
|
||||||
|
{#if codeMode == EditorModes.JS}
|
||||||
|
<ActionButton
|
||||||
|
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||||
|
quiet
|
||||||
|
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||||
|
>
|
||||||
|
<Detail size="S">Bindings</Detail>
|
||||||
|
</ActionButton>
|
||||||
|
{#if codeBindingOpen}
|
||||||
|
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
@ -496,19 +517,22 @@
|
||||||
onChange({ detail: e.detail }, key)
|
onChange({ detail: e.detail }, key)
|
||||||
inputData[key] = e.detail
|
inputData[key] = e.detail
|
||||||
}}
|
}}
|
||||||
completions={[
|
completions={stepCompletions}
|
||||||
jsAutocomplete([
|
mode={codeMode}
|
||||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
autocompleteEnabled={codeMode != EditorModes.JS}
|
||||||
]),
|
|
||||||
]}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
height={500}
|
height={500}
|
||||||
/>
|
/>
|
||||||
<div class="messaging">
|
<div class="messaging">
|
||||||
<Icon name="FlashOn" />
|
{#if codeMode == EditorModes.Handlebars}
|
||||||
<div class="messaging-wrap">
|
<Icon name="FlashOn" />
|
||||||
<div>Add available bindings by typing <strong>$</strong></div>
|
<div class="messaging-wrap">
|
||||||
</div>
|
<div>
|
||||||
|
Add available bindings by typing <strong>
|
||||||
|
}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
{:else if value.customType === "loopOption"}
|
{:else if value.customType === "loopOption"}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
$: text = getText(filters)
|
$: text = getText(filters)
|
||||||
|
|
||||||
const getText = filters => {
|
const getText = filters => {
|
||||||
const count = filters?.length
|
const count = filters?.filter(filter => filter.field)?.length
|
||||||
return count ? `Filter (${count})` : "Filter"
|
return count ? `Filter (${count})` : "Filter"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,9 +10,8 @@ export const createTableSelectionStore = (integration, datasource) => {
|
||||||
|
|
||||||
datasources.getTableNames(datasource).then(tableNames => {
|
datasources.getTableNames(datasource).then(tableNames => {
|
||||||
tableNamesStore.set(tableNames)
|
tableNamesStore.set(tableNames)
|
||||||
|
|
||||||
selectedTableNamesStore.set(
|
selectedTableNamesStore.set(
|
||||||
tableNames.filter(tableName => datasource.entities[tableName])
|
tableNames.filter(tableName => datasource.entities?.[tableName])
|
||||||
)
|
)
|
||||||
|
|
||||||
loadingStore.set(false)
|
loadingStore.set(false)
|
||||||
|
|
|
@ -2,21 +2,13 @@
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
ModalContent,
|
|
||||||
Toggle,
|
|
||||||
Divider,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import {
|
import {
|
||||||
BUDIBASE_INTERNAL_DB_ID,
|
BUDIBASE_INTERNAL_DB_ID,
|
||||||
BUDIBASE_DATASOURCE_TYPE,
|
BUDIBASE_DATASOURCE_TYPE,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
|
||||||
|
|
||||||
$: tableNames = $tables.list.map(table => table.name)
|
$: tableNames = $tables.list.map(table => table.name)
|
||||||
$: selectedSource = $datasources.list.find(
|
$: selectedSource = $datasources.list.find(
|
||||||
|
@ -43,28 +35,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = ""
|
let error = ""
|
||||||
let autoColumns = getAutoColumnInformation()
|
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let rows = []
|
let rows = []
|
||||||
let allValid = true
|
let allValid = true
|
||||||
let displayColumn = null
|
let displayColumn = null
|
||||||
|
|
||||||
function getAutoColumns() {
|
|
||||||
const selectedAutoColumns = {}
|
|
||||||
|
|
||||||
Object.entries(autoColumns).forEach(([subtype, column]) => {
|
|
||||||
if (column.enabled) {
|
|
||||||
selectedAutoColumns[column.name] = buildAutoColumn(
|
|
||||||
name,
|
|
||||||
column.name,
|
|
||||||
subtype
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return selectedAutoColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValid(evt) {
|
function checkValid(evt) {
|
||||||
const tableName = evt.target.value
|
const tableName = evt.target.value
|
||||||
if (tableNames.includes(tableName)) {
|
if (tableNames.includes(tableName)) {
|
||||||
|
@ -77,7 +53,7 @@
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
let newTable = {
|
let newTable = {
|
||||||
name,
|
name,
|
||||||
schema: { ...schema, ...getAutoColumns() },
|
schema: { ...schema },
|
||||||
rows,
|
rows,
|
||||||
type: "internal",
|
type: "internal",
|
||||||
sourceId: targetDatasourceId,
|
sourceId: targetDatasourceId,
|
||||||
|
@ -118,21 +94,6 @@
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
<div class="autocolumns">
|
|
||||||
<Label extraSmall grey>Auto Columns</Label>
|
|
||||||
<div class="toggles">
|
|
||||||
<div class="toggle-1">
|
|
||||||
<Toggle text="Created by" bind:value={autoColumns.createdBy.enabled} />
|
|
||||||
<Toggle text="Created at" bind:value={autoColumns.createdAt.enabled} />
|
|
||||||
<Toggle text="Auto ID" bind:value={autoColumns.autoID.enabled} />
|
|
||||||
</div>
|
|
||||||
<div class="toggle-2">
|
|
||||||
<Toggle text="Updated by" bind:value={autoColumns.updatedBy.enabled} />
|
|
||||||
<Toggle text="Updated at" bind:value={autoColumns.updatedAt.enabled} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Label grey extraSmall
|
<Label grey extraSmall
|
||||||
|
@ -148,24 +109,3 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
|
||||||
.autocolumns {
|
|
||||||
margin-bottom: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggles {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-1 :global(> *) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-2 :global(> *) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -35,9 +35,8 @@
|
||||||
try {
|
try {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||||
const name = view.name
|
|
||||||
const id = view.tableId
|
const id = view.tableId
|
||||||
await views.delete(name)
|
await views.delete(view)
|
||||||
notifications.success("View deleted")
|
notifications.success("View deleted")
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
$goto(`./table/${id}`)
|
$goto(`./table/${id}`)
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
closeBrackets,
|
closeBrackets,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
closeBracketsKeymap,
|
closeBracketsKeymap,
|
||||||
|
acceptCompletion,
|
||||||
|
completionStatus,
|
||||||
} from "@codemirror/autocomplete"
|
} from "@codemirror/autocomplete"
|
||||||
import {
|
import {
|
||||||
EditorView,
|
EditorView,
|
||||||
|
@ -34,7 +36,8 @@
|
||||||
defaultKeymap,
|
defaultKeymap,
|
||||||
historyKeymap,
|
historyKeymap,
|
||||||
history,
|
history,
|
||||||
indentWithTab,
|
indentMore,
|
||||||
|
indentLess,
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
import { Compartment } from "@codemirror/state"
|
import { Compartment } from "@codemirror/state"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
|
@ -48,6 +51,7 @@
|
||||||
export let mode = EditorModes.Handlebars
|
export let mode = EditorModes.Handlebars
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
|
export let autocompleteEnabled = true
|
||||||
|
|
||||||
// Export a function to expose caret position
|
// Export a function to expose caret position
|
||||||
export const getCaretPosition = () => {
|
export const getCaretPosition = () => {
|
||||||
|
@ -107,6 +111,22 @@
|
||||||
let isDark = !currentTheme.includes("light")
|
let isDark = !currentTheme.includes("light")
|
||||||
let themeConfig = new Compartment()
|
let themeConfig = new Compartment()
|
||||||
|
|
||||||
|
const indentWithTabCustom = {
|
||||||
|
key: "Tab",
|
||||||
|
run: view => {
|
||||||
|
if (completionStatus(view.state) == "active") {
|
||||||
|
acceptCompletion(view)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
indentMore(view)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
shift: view => {
|
||||||
|
indentLess(view)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const buildKeymap = () => {
|
const buildKeymap = () => {
|
||||||
const baseMap = [
|
const baseMap = [
|
||||||
...closeBracketsKeymap,
|
...closeBracketsKeymap,
|
||||||
|
@ -114,7 +134,7 @@
|
||||||
...historyKeymap,
|
...historyKeymap,
|
||||||
...foldKeymap,
|
...foldKeymap,
|
||||||
...completionKeymap,
|
...completionKeymap,
|
||||||
indentWithTab,
|
indentWithTabCustom,
|
||||||
]
|
]
|
||||||
return baseMap
|
return baseMap
|
||||||
}
|
}
|
||||||
|
@ -131,12 +151,6 @@
|
||||||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
autocompletion({
|
|
||||||
override: [...completions],
|
|
||||||
closeOnBlur: true,
|
|
||||||
icons: false,
|
|
||||||
optionClass: () => "autocomplete-option",
|
|
||||||
}),
|
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of(v => {
|
EditorView.updateListener.of(v => {
|
||||||
const docStr = v.state.doc?.toString()
|
const docStr = v.state.doc?.toString()
|
||||||
|
@ -159,11 +173,16 @@
|
||||||
|
|
||||||
const buildExtensions = base => {
|
const buildExtensions = base => {
|
||||||
const complete = [...base]
|
const complete = [...base]
|
||||||
if (mode.name == "javascript") {
|
|
||||||
complete.push(javascript())
|
if (autocompleteEnabled) {
|
||||||
complete.push(highlightWhitespace())
|
complete.push(
|
||||||
complete.push(lineNumbers())
|
autocompletion({
|
||||||
complete.push(foldGutter())
|
override: [...completions],
|
||||||
|
closeOnBlur: true,
|
||||||
|
icons: false,
|
||||||
|
optionClass: () => "autocomplete-option",
|
||||||
|
})
|
||||||
|
)
|
||||||
complete.push(
|
complete.push(
|
||||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||||
if (insert === "$") {
|
if (insert === "$") {
|
||||||
|
@ -193,6 +212,13 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode.name == "javascript") {
|
||||||
|
complete.push(javascript())
|
||||||
|
complete.push(highlightWhitespace())
|
||||||
|
complete.push(lineNumbers())
|
||||||
|
complete.push(foldGutter())
|
||||||
|
}
|
||||||
|
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
complete.push(placeholderFn(placeholder))
|
complete.push(placeholderFn(placeholder))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -2,6 +2,7 @@
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
|
@ -98,21 +99,25 @@
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
<div class="text" title={showTooltip ? text : null}>
|
||||||
|
{text}
|
||||||
|
{#if selectedBy}
|
||||||
|
<UserAvatars size="XS" users={selectedBy} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if withActions}
|
{#if withActions}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $$slots.right}
|
{#if $$slots.right}
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<slot name="right" />
|
<slot name="right" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if selectedBy}
|
|
||||||
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -136,13 +141,16 @@
|
||||||
}
|
}
|
||||||
.nav-item.highlighted {
|
.nav-item.highlighted {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||||
}
|
}
|
||||||
.nav-item.selected {
|
.nav-item.selected {
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background-color: var(--spectrum-global-color-gray-300);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.nav-item:hover .actions {
|
.nav-item:hover .actions {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
@ -159,37 +167,6 @@
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selected user styles */
|
|
||||||
.nav-item.selectedBy:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: calc(100% - 4px);
|
|
||||||
height: 28px;
|
|
||||||
border: 2px solid var(--selected-by-color);
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.selected-by-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background: var(--selected-by-color);
|
|
||||||
padding: 2px 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: white;
|
|
||||||
transform: translateY(calc(1px - 100%));
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
}
|
|
||||||
.nav-item.selectedBy:hover .selected-by-label {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Needed to fully display the actions icon */
|
/* Needed to fully display the actions icon */
|
||||||
.nav-item.scrollable .nav-item-content {
|
.nav-item.scrollable .nav-item-content {
|
||||||
padding-right: 1px;
|
padding-right: 1px;
|
||||||
|
@ -245,6 +222,9 @@
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
order: 2;
|
order: 2;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.scrollable .text {
|
.scrollable .text {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
@ -208,7 +208,9 @@
|
||||||
<div class="syntax-error">
|
<div class="syntax-error">
|
||||||
Current Handlebars syntax is invalid, please check the
|
Current Handlebars syntax is invalid, please check the
|
||||||
guide
|
guide
|
||||||
<a href="https://handlebarsjs.com/guide/">here</a>
|
<a href="https://handlebarsjs.com/guide/" target="_blank"
|
||||||
|
>here</a
|
||||||
|
>
|
||||||
for more details.
|
for more details.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
Link,
|
Link,
|
||||||
Modal,
|
Modal,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
@ -250,15 +251,20 @@
|
||||||
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||||
</span>
|
</span>
|
||||||
<span class="revert-link">
|
<span class="revert-link">
|
||||||
<Link
|
<AbsTooltip
|
||||||
disabled={!$isOnlyUser}
|
text={$isOnlyUser
|
||||||
quiet
|
? null
|
||||||
secondary
|
: "Unavailable - another user is editing this app"}
|
||||||
on:click={revertApp}
|
|
||||||
tooltip="Unavailable - another user is editing this app"
|
|
||||||
>
|
>
|
||||||
Revert
|
<Link
|
||||||
</Link>
|
disabled={!$isOnlyUser}
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={revertApp}
|
||||||
|
>
|
||||||
|
Revert
|
||||||
|
</Link>
|
||||||
|
</AbsTooltip>
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="status-text unpublished">Not published</span>
|
<span class="status-text unpublished">Not published</span>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
makeStateBinding,
|
makeStateBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||||
|
@ -29,6 +30,26 @@
|
||||||
let actionQuery
|
let actionQuery
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
|
||||||
|
const setUpdateActions = actions => {
|
||||||
|
return actions
|
||||||
|
? cloneDeep(actions)
|
||||||
|
.filter(action => {
|
||||||
|
return (
|
||||||
|
action[EVENT_TYPE_KEY] === "Update State" &&
|
||||||
|
action.parameters?.type === "set" &&
|
||||||
|
action.parameters.key
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.reduce((acc, action) => {
|
||||||
|
acc[action.id] = action
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot original action state
|
||||||
|
let updateStateActions = setUpdateActions(actions)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Ensure parameters object is never null
|
// Ensure parameters object is never null
|
||||||
if (selectedAction && !selectedAction.parameters) {
|
if (selectedAction && !selectedAction.parameters) {
|
||||||
|
@ -125,8 +146,9 @@
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||||
let allBindings = []
|
let allBindings = []
|
||||||
|
let cloneActionBindings = cloneDeep(actionBindings)
|
||||||
if (!actions) {
|
if (!actions) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -144,11 +166,19 @@
|
||||||
.forEach(action => {
|
.forEach(action => {
|
||||||
// Check we have a binding for this action, and generate one if not
|
// Check we have a binding for this action, and generate one if not
|
||||||
const stateBinding = makeStateBinding(action.parameters.key)
|
const stateBinding = makeStateBinding(action.parameters.key)
|
||||||
const hasKey = bindings.some(binding => {
|
const hasKey = actionBindings.some(binding => {
|
||||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||||
})
|
})
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
bindings.push(stateBinding)
|
let existing = updateStateActions[action.id]
|
||||||
|
if (existing) {
|
||||||
|
const existingBinding = makeStateBinding(existing.parameters.key)
|
||||||
|
cloneActionBindings = cloneActionBindings.filter(
|
||||||
|
binding =>
|
||||||
|
binding.runtimeBinding !== existingBinding.runtimeBinding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
allBindings.push(stateBinding)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||||
|
@ -164,15 +194,16 @@
|
||||||
.filter(index => index !== undefined)
|
.filter(index => index !== undefined)
|
||||||
|
|
||||||
// Based on the above, filter out the asynchronous automations from the bindings
|
// Based on the above, filter out the asynchronous automations from the bindings
|
||||||
if (asynchronousAutomationIndexes) {
|
let contextBindings = asynchronousAutomationIndexes
|
||||||
allBindings = eventContextBindings
|
? eventContextBindings.filter((binding, index) => {
|
||||||
.filter((binding, index) => {
|
|
||||||
return !asynchronousAutomationIndexes.includes(index)
|
return !asynchronousAutomationIndexes.includes(index)
|
||||||
})
|
})
|
||||||
.concat(bindings)
|
: eventContextBindings
|
||||||
} else {
|
|
||||||
allBindings = eventContextBindings.concat(bindings)
|
allBindings = contextBindings
|
||||||
}
|
.concat(cloneActionBindings)
|
||||||
|
.concat(allBindings)
|
||||||
|
|
||||||
return allBindings
|
return allBindings
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -70,8 +70,9 @@
|
||||||
} set`
|
} set`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="action-count">{actionText}</div>
|
<div class="action-editor">
|
||||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title={"Actions"}>
|
<Drawer bind:this={drawer} title={"Actions"}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
|
@ -89,9 +90,7 @@
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-count {
|
.action-editor :global(.spectrum-ActionButton) {
|
||||||
padding-top: 6px;
|
width: 100%;
|
||||||
padding-bottom: var(--spacing-s);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
let drawer
|
let drawer
|
||||||
let boundValue
|
let boundValue
|
||||||
|
|
||||||
|
$: text = getText(value)
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
$: schema = getSchema($currentAsset, datasource)
|
||||||
$: options = allowCellEditing
|
$: options = allowCellEditing
|
||||||
|
@ -31,6 +32,17 @@
|
||||||
allowLinks: true,
|
allowLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getText = value => {
|
||||||
|
if (!value?.length) {
|
||||||
|
return "All columns"
|
||||||
|
}
|
||||||
|
let text = `${value.length} column`
|
||||||
|
if (value.length !== 1) {
|
||||||
|
text += "s"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
const getSchema = (asset, datasource) => {
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||||
|
|
||||||
|
@ -76,7 +88,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column-editor">
|
<div class="column-editor">
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
<ActionButton on:click={open}>{text}</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Columns">
|
<Drawer bind:this={drawer} title="Columns">
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
|
|
|
@ -12,25 +12,36 @@
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value = []
|
export let value = []
|
||||||
|
|
||||||
const convertOldColumnFormat = oldColumns => {
|
|
||||||
if (typeof oldColumns?.[0] === "string") {
|
|
||||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: convertOldColumnFormat(value)
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let drawer
|
let drawer
|
||||||
let boundValue
|
let boundValue
|
||||||
|
|
||||||
|
$: text = getText(value)
|
||||||
|
$: convertOldColumnFormat(value)
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
$: schema = getSchema($currentAsset, datasource)
|
||||||
$: options = Object.keys(schema || {})
|
$: options = Object.keys(schema || {})
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
$: sanitisedValue = getValidColumns(value, options)
|
||||||
$: updateBoundValue(sanitisedValue)
|
$: updateBoundValue(sanitisedValue)
|
||||||
|
|
||||||
|
const getText = value => {
|
||||||
|
if (!value?.length) {
|
||||||
|
return "All fields"
|
||||||
|
}
|
||||||
|
let text = `${value.length} field`
|
||||||
|
if (value.length !== 1) {
|
||||||
|
text += "s"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertOldColumnFormat = oldColumns => {
|
||||||
|
if (typeof oldColumns?.[0] === "string") {
|
||||||
|
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
const getSchema = (asset, datasource) => {
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||||
|
|
||||||
|
@ -75,7 +86,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure fields</ActionButton>
|
<div class="field-configuration">
|
||||||
|
<ActionButton on:click={open}>{text}</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title="Form Fields">
|
<Drawer bind:this={drawer} title="Form Fields">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Configure the fields in your form.
|
Configure the fields in your form.
|
||||||
|
@ -83,3 +97,9 @@
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-configuration :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
$: text = getText(value)
|
$: text = getText(value?.filter(filter => filter.field))
|
||||||
|
|
||||||
async function saveFilter() {
|
async function saveFilter() {
|
||||||
dispatch("change", tempValue)
|
dispatch("change", tempValue)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { TOURS } from "./tours.js"
|
import { TOURS } from "./tours.js"
|
||||||
import { goto, layout, isActive } from "@roxi/routify"
|
import { goto, layout, isActive } from "@roxi/routify"
|
||||||
|
@ -10,17 +10,20 @@
|
||||||
let tourStep
|
let tourStep
|
||||||
let tourStepIdx
|
let tourStepIdx
|
||||||
let lastStep
|
let lastStep
|
||||||
|
let skipping = false
|
||||||
|
|
||||||
$: tourNodes = { ...$store.tourNodes }
|
$: tourNodes = { ...$store.tourNodes }
|
||||||
$: tourKey = $store.tourKey
|
$: tourKey = $store.tourKey
|
||||||
$: tourStepKey = $store.tourStepKey
|
$: tourStepKey = $store.tourStepKey
|
||||||
|
$: tour = TOURS[tourKey]
|
||||||
|
$: tourOnSkip = tour?.onSkip
|
||||||
|
|
||||||
const updateTourStep = (targetStepKey, tourKey) => {
|
const updateTourStep = (targetStepKey, tourKey) => {
|
||||||
if (!tourKey) {
|
if (!tourKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!tourSteps?.length) {
|
if (!tourSteps?.length) {
|
||||||
tourSteps = [...TOURS[tourKey]]
|
tourSteps = [...tour.steps]
|
||||||
}
|
}
|
||||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||||
|
@ -71,23 +74,8 @@
|
||||||
tourStep.onComplete()
|
tourStep.onComplete()
|
||||||
}
|
}
|
||||||
popover.hide()
|
popover.hide()
|
||||||
if (tourStep.endRoute) {
|
if (tour.endRoute) {
|
||||||
$goto(tourStep.endRoute)
|
$goto(tour.endRoute)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = async () => {
|
|
||||||
if (tourStepIdx > 0) {
|
|
||||||
let target = tourSteps[tourStepIdx - 1]
|
|
||||||
if (target) {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourStepKey: target.id,
|
|
||||||
}))
|
|
||||||
navigateStep(target)
|
|
||||||
} else {
|
|
||||||
console.log("Could not retrieve step")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,16 +120,23 @@
|
||||||
</Body>
|
</Body>
|
||||||
<div class="tour-footer">
|
<div class="tour-footer">
|
||||||
<div class="tour-navigation">
|
<div class="tour-navigation">
|
||||||
{#if tourStepIdx > 0}
|
{#if typeof tourOnSkip === "function"}
|
||||||
<Button
|
<Link
|
||||||
secondary
|
secondary
|
||||||
on:click={previousStep}
|
quiet
|
||||||
disabled={tourStepIdx == 0}
|
on:click={() => {
|
||||||
|
skipping = true
|
||||||
|
tourOnSkip()
|
||||||
|
if (tour.endRoute) {
|
||||||
|
$goto(tour.endRoute)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={skipping}
|
||||||
>
|
>
|
||||||
<div>Back</div>
|
Skip
|
||||||
</Button>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Button cta on:click={nextStep}>
|
<Button cta on:click={nextStep} disabled={skipping}>
|
||||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,9 +152,10 @@
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.tour-navigation {
|
.tour-navigation {
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
grid-gap: var(--spacing-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.tour-body :global(.feature-list) {
|
.tour-body :global(.feature-list) {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
const registerTourNode = (tourKey, stepKey) => {
|
const registerTourNode = (tourKey, stepKey) => {
|
||||||
if (ready && !registered && tourKey) {
|
if (ready && !registered && tourKey) {
|
||||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||||
if (!currentTourStep) {
|
if (!currentTourStep) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||||
|
|
||||||
export const TOUR_STEP_KEYS = {
|
export const TOUR_STEP_KEYS = {
|
||||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
||||||
FEATURE_ONBOARDING: "feature-onboarding",
|
FEATURE_ONBOARDING: "feature-onboarding",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endUserOnboarding = async ({ skipped = false } = {}) => {
|
||||||
|
// Mark the users onboarding as complete
|
||||||
|
// Clear all tour related state
|
||||||
|
if (get(auth).user) {
|
||||||
|
try {
|
||||||
|
await API.updateSelf({
|
||||||
|
onboardedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (skipped) {
|
||||||
|
tourEvent("skipped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
tourNodes: undefined,
|
||||||
|
tourKey: undefined,
|
||||||
|
tourKeyStep: undefined,
|
||||||
|
onboarding: false,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Onboarding failed", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tourEvent = eventKey => {
|
const tourEvent = eventKey => {
|
||||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||||
eventSource: EventSource.PORTAL,
|
eventSource: EventSource.PORTAL,
|
||||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
||||||
|
|
||||||
const getTours = () => {
|
const getTours = () => {
|
||||||
return {
|
return {
|
||||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
||||||
{
|
steps: [
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
{
|
||||||
title: "Data",
|
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
||||||
route: "/builder/app/:application/data",
|
title: "Data",
|
||||||
layout: OnboardingData,
|
route: "/builder/app/:application/data",
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
layout: OnboardingData,
|
||||||
onLoad: async () => {
|
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
onLoad: async () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
},
|
},
|
||||||
align: "left",
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
||||||
|
title: "Design",
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
layout: OnboardingDesign,
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||||
|
title: "Automations",
|
||||||
|
route: "/builder/app/:application/automation",
|
||||||
|
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||||
|
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
||||||
|
title: "Users",
|
||||||
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||||
|
title: "Publish",
|
||||||
|
layout: OnboardingPublish,
|
||||||
|
route: "/builder/app/:application/design",
|
||||||
|
query: ".toprightnav #builder-app-publish-button",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSkip: async () => {
|
||||||
|
await endUserOnboarding({ skipped: true })
|
||||||
},
|
},
|
||||||
{
|
endRoute: "/builder/app/:application/data",
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
},
|
||||||
title: "Design",
|
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
||||||
route: "/builder/app/:application/design",
|
steps: [
|
||||||
layout: OnboardingDesign,
|
{
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
||||||
onLoad: () => {
|
title: "Users",
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
query: ".toprightnav #builder-app-users-button",
|
||||||
|
body: "Add users to your app and control what level of access they have.",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
||||||
|
},
|
||||||
|
onComplete: endUserOnboarding,
|
||||||
},
|
},
|
||||||
align: "left",
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
|
||||||
title: "Automations",
|
|
||||||
route: "/builder/app/:application/automation",
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
|
||||||
title: "Publish",
|
|
||||||
layout: OnboardingPublish,
|
|
||||||
route: "/builder/app/:application/design",
|
|
||||||
endRoute: "/builder/app/:application/data",
|
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Mark the users onboarding as complete
|
|
||||||
// Clear all tour related state
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[TOUR_KEYS.FEATURE_ONBOARDING]: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
// Push the onboarding forward
|
|
||||||
if (get(auth).user) {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: undefined,
|
|
||||||
tourKey: undefined,
|
|
||||||
tourKeyStep: undefined,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Tooltip } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let text
|
export let text
|
||||||
export let url
|
export let url
|
||||||
export let active = false
|
export let active = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let tooltip = null
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="side-nav-item">
|
<div class="side-nav-item">
|
||||||
|
@ -18,11 +15,6 @@
|
||||||
{text || ""}
|
{text || ""}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tooltip}
|
|
||||||
<div class="tooltip">
|
|
||||||
<Tooltip textWrapping direction="right" text={tooltip} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -45,17 +37,4 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
}
|
}
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
left: 100%;
|
|
||||||
top: 50%;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
.side-nav-item:hover .tooltip {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { UserAvatar } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let lockedAction
|
export let lockedAction
|
||||||
|
|
||||||
$: editing = app?.lockedBy != null
|
$: editing = app.sessions?.length
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
<div class="updated">
|
<div class="updated">
|
||||||
{#if editing}
|
{#if editing}
|
||||||
Currently editing
|
Currently editing
|
||||||
<UserAvatar user={app.lockedBy} />
|
<UserAvatars users={app.sessions} />
|
||||||
{:else if app.updatedAt}
|
{:else if app.updatedAt}
|
||||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||||
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
|
||||||
import { Roles } from "constants/backend"
|
|
||||||
import { lowercase } from "helpers"
|
import { lowercase } from "helpers"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
@ -142,21 +140,6 @@
|
||||||
// Create user
|
// Create user
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
|
|
||||||
// Create a default home screen if no template was selected
|
|
||||||
if (template == null) {
|
|
||||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
|
||||||
defaultScreenTemplate.routing.route = "/home"
|
|
||||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
|
||||||
try {
|
|
||||||
await store.actions.screens.save(defaultScreenTemplate)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Could not create a default application screen", err)
|
|
||||||
notifications.warning(
|
|
||||||
"Encountered an issue creating the default screen."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
creating = false
|
creating = false
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
window.isBuilder = true
|
||||||
window.closePreview = () => {
|
window.closePreview = () => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<script>
|
|
||||||
import { UserAvatar } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
export let users = []
|
|
||||||
|
|
||||||
$: uniqueUsers = unique(users)
|
|
||||||
|
|
||||||
const unique = users => {
|
|
||||||
let uniqueUsers = {}
|
|
||||||
users?.forEach(user => {
|
|
||||||
uniqueUsers[user.email] = user
|
|
||||||
})
|
|
||||||
return Object.values(uniqueUsers)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="avatars">
|
|
||||||
{#each uniqueUsers as user}
|
|
||||||
<UserAvatar {user} tooltipDirection="bottom" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.avatars {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -15,6 +15,7 @@
|
||||||
Heading,
|
Heading,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
|
TooltipPosition,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AppActions from "components/deploy/AppActions.svelte"
|
import AppActions from "components/deploy/AppActions.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -25,8 +26,8 @@
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
import UserAvatars from "./_components/UserAvatars.svelte"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
@ -86,17 +87,10 @@
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||||
if (!$auth.user?.onboardedAt) {
|
if (!$auth.user?.onboardedAt) {
|
||||||
// Determine the correct step
|
|
||||||
const activeNav = $layout.children.find(c => $isActive(c.path))
|
|
||||||
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
|
|
||||||
const targetStep = activeNav
|
|
||||||
? onboardingTour.find(step => step.route === activeNav?.path)
|
|
||||||
: null
|
|
||||||
await store.update(state => ({
|
await store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
onboarding: true,
|
onboarding: true,
|
||||||
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
||||||
tourStepKey: targetStep?.id,
|
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Feature tour date
|
// Feature tour date
|
||||||
|
@ -172,7 +166,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
<span>
|
<span>
|
||||||
<UserAvatars users={$userStore} />
|
<UserAvatars
|
||||||
|
users={$userStore}
|
||||||
|
order="rtl"
|
||||||
|
tooltipPosition={TooltipPosition.Bottom}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<AppActions {application} {loaded} />
|
<AppActions {application} {loaded} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -228,7 +226,7 @@
|
||||||
.top-nav {
|
.top-nav {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
padding: 0 var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -8,6 +8,10 @@
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: automationId = $selectedAutomation?._id
|
||||||
|
$: store.actions.websocket.selectResource(automationId)
|
||||||
|
|
||||||
// Keep URL and state in sync for selected screen ID
|
// Keep URL and state in sync for selected screen ID
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
import { Button, Modal } from "@budibase/bbui"
|
import { Button, Modal } from "@budibase/bbui"
|
||||||
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
|
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
|
||||||
|
|
||||||
|
export let datasourceId
|
||||||
let importQueriesModal
|
let importQueriesModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={importQueriesModal}>
|
<Modal bind:this={importQueriesModal}>
|
||||||
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} />
|
<ImportQueriesModal createDatasource={false} {datasourceId} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class="button">
|
<div class="button">
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
|
|
||||||
import { hasData } from "stores/selectors"
|
import { hasData } from "stores/selectors"
|
||||||
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
|
import { notifications, Body, Icon, AbsTooltip } from "@budibase/bbui"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
||||||
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
||||||
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
|
import CreationPage from "components/common/CreationPage.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
|
||||||
|
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
@ -46,25 +46,16 @@
|
||||||
bind:this={externalDatasourceModal}
|
bind:this={externalDatasourceModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="page">
|
<CreationPage
|
||||||
<div class="closeButton">
|
showClose={hasData($datasources, $tables)}
|
||||||
{#if hasData($datasources, $tables)}
|
onClose={() => $goto("./table")}
|
||||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
heading="Add new data source"
|
||||||
{/if}
|
>
|
||||||
</div>
|
|
||||||
<div class="heading">
|
|
||||||
<Heading weight="light">Add new data source</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subHeading">
|
<div class="subHeading">
|
||||||
<Body>Get started with our Budibase DB</Body>
|
<Body>Get started with our Budibase DB</Body>
|
||||||
<div
|
<AbsTooltip text="Budibase DB is built with CouchDB">
|
||||||
role="tooltip"
|
<Icon name="Info" size="S" />
|
||||||
title="Budibase DB is built with CouchDB"
|
</AbsTooltip>
|
||||||
class="tooltip"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon name="fa-solid fa-circle-info" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
|
@ -113,37 +104,19 @@
|
||||||
</DatasourceOption>
|
</DatasourceOption>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CreationPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeButton {
|
|
||||||
height: 38px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subHeading {
|
.subHeading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-top: 12px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
.subHeading :global(p) {
|
||||||
.tooltip {
|
color: var(--spectrum-global-color-gray-600) !important;
|
||||||
margin-left: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
<script>
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
|
|
||||||
import blankScreenPreview from "./blankScreenPreview.png"
|
|
||||||
import listScreenPreview from "./listScreenPreview.png"
|
|
||||||
|
|
||||||
export let onConfirm
|
|
||||||
export let onCancel
|
|
||||||
|
|
||||||
let listScreenModeKey = "autoCreate"
|
|
||||||
let blankScreenModeKey = "blankScreen"
|
|
||||||
|
|
||||||
let selectedScreenMode
|
|
||||||
|
|
||||||
const confirmScreenSelection = async () => {
|
|
||||||
await onConfirm(selectedScreenMode)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<ModalContent
|
|
||||||
title="Add screens"
|
|
||||||
confirmText="Continue"
|
|
||||||
cancelText="Cancel"
|
|
||||||
onConfirm={confirmScreenSelection}
|
|
||||||
{onCancel}
|
|
||||||
disabled={!selectedScreenMode}
|
|
||||||
size="M"
|
|
||||||
>
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div
|
|
||||||
class="screen-type item blankView"
|
|
||||||
class:selected={selectedScreenMode == blankScreenModeKey}
|
|
||||||
on:click={() => {
|
|
||||||
selectedScreenMode = blankScreenModeKey
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="content screen-type-wrap">
|
|
||||||
<img
|
|
||||||
alt="blank screen preview"
|
|
||||||
class="preview"
|
|
||||||
src={blankScreenPreview}
|
|
||||||
/>
|
|
||||||
<div class="screen-type-text">
|
|
||||||
<Heading size="XS">Blank screen</Heading>
|
|
||||||
<Body size="S">Add an empty blank screen</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={`checkmark-spacing ${
|
|
||||||
selectedScreenMode == blankScreenModeKey ? "visible" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="listViewTitle">
|
|
||||||
<Heading size="XS">Quickly create a screen from your data</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="screen-type item"
|
|
||||||
class:selected={selectedScreenMode == listScreenModeKey}
|
|
||||||
on:click={() => {
|
|
||||||
selectedScreenMode = listScreenModeKey
|
|
||||||
}}
|
|
||||||
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
|
|
||||||
.length}
|
|
||||||
>
|
|
||||||
<div class="content screen-type-wrap">
|
|
||||||
<img
|
|
||||||
alt="list screen preview"
|
|
||||||
class="preview"
|
|
||||||
src={listScreenPreview}
|
|
||||||
/>
|
|
||||||
<div class="screen-type-text">
|
|
||||||
<Heading size="XS">List view</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Create, edit and view your data in a list view screen with side
|
|
||||||
panel
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={`checkmark-spacing ${
|
|
||||||
selectedScreenMode == listScreenModeKey ? "visible" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.screen-type-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.checkmark-spacing {
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
letter-spacing: 0px;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
cursor: pointer;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
|
||||||
transition: 0.3s all;
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
border-radius: 4px;
|
|
||||||
border-width: 1px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.item:hover,
|
|
||||||
.selected {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
.screen-type-wrap .screen-type-text {
|
|
||||||
padding-left: var(--spectrum-alias-item-padding-xl);
|
|
||||||
}
|
|
||||||
.screen-type-wrap .screen-type-text :global(h1) {
|
|
||||||
padding-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.screen-type-wrap :global(.spectrum-Icon) {
|
|
||||||
min-width: var(--spectrum-icon-size-m);
|
|
||||||
}
|
|
||||||
.screen-type-wrap :global(.spectrum-Heading) {
|
|
||||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
|
||||||
}
|
|
||||||
.preview {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listViewTitle {
|
|
||||||
margin-top: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blankView {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -9,7 +9,7 @@
|
||||||
Helpers,
|
Helpers,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { makeComponentUnique } from "builderStore/componentUtils"
|
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||||
import ScreenWizard from "./ScreenWizard.svelte"
|
|
||||||
import RoleIndicator from "./RoleIndicator.svelte"
|
import RoleIndicator from "./RoleIndicator.svelte"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
let searchString
|
let searchString
|
||||||
let accessRole = "all"
|
let accessRole = "all"
|
||||||
let showNewScreenModal
|
|
||||||
|
|
||||||
$: filteredScreens = getFilteredScreens(
|
$: filteredScreens = getFilteredScreens(
|
||||||
$sortedScreens,
|
$sortedScreens,
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
|
|
||||||
<Panel title="Screens" borderRight>
|
<Panel title="Screens" borderRight>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Button on:click={showNewScreenModal} cta>Add screen</Button>
|
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
||||||
<Search
|
<Search
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={searchString}
|
value={searchString}
|
||||||
|
@ -74,5 +73,3 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<ScreenWizard bind:showModal={showNewScreenModal} />
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||||
import NewScreenModal from "./NewScreenModal.svelte"
|
|
||||||
import DatasourceModal from "./DatasourceModal.svelte"
|
import DatasourceModal from "./DatasourceModal.svelte"
|
||||||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
@ -11,11 +10,11 @@
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
let pendingScreen
|
let pendingScreen
|
||||||
|
|
||||||
// Modal refs
|
// Modal refs
|
||||||
let newScreenModal
|
|
||||||
let screenDetailsModal
|
let screenDetailsModal
|
||||||
let datasourceModal
|
let datasourceModal
|
||||||
let screenAccessRoleModal
|
let screenAccessRoleModal
|
||||||
|
@ -26,16 +25,6 @@
|
||||||
let blankScreenUrl = null
|
let blankScreenUrl = null
|
||||||
let screenMode = null
|
let screenMode = null
|
||||||
|
|
||||||
// External handler to show the screen wizard
|
|
||||||
export const showModal = () => {
|
|
||||||
selectedTemplates = null
|
|
||||||
blankScreenUrl = null
|
|
||||||
screenMode = null
|
|
||||||
pendingScreen = null
|
|
||||||
screenAccessRole = Roles.BASIC
|
|
||||||
newScreenModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates an array of screens, checking and sanitising their URLs
|
// Creates an array of screens, checking and sanitising their URLs
|
||||||
const createScreens = async ({ screens, screenAccessRole }) => {
|
const createScreens = async ({ screens, screenAccessRole }) => {
|
||||||
if (!screens?.length) {
|
if (!screens?.length) {
|
||||||
|
@ -43,6 +32,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let screenId
|
||||||
|
|
||||||
for (let screen of screens) {
|
for (let screen of screens) {
|
||||||
// Check we aren't clashing with an existing URL
|
// Check we aren't clashing with an existing URL
|
||||||
if (hasExistingUrl(screen.routing.route)) {
|
if (hasExistingUrl(screen.routing.route)) {
|
||||||
|
@ -64,7 +55,8 @@
|
||||||
screen.routing.roleId = screenAccessRole
|
screen.routing.roleId = screenAccessRole
|
||||||
|
|
||||||
// Create the screen
|
// Create the screen
|
||||||
await store.actions.screens.save(screen)
|
const response = await store.actions.screens.save(screen)
|
||||||
|
screenId = response._id
|
||||||
|
|
||||||
// Add link in layout for list screens
|
// Add link in layout for list screens
|
||||||
if (screen.props._instanceName.endsWith("List")) {
|
if (screen.props._instanceName.endsWith("List")) {
|
||||||
|
@ -74,7 +66,10 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$goto(`./${screenId}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
notifications.error("Error creating screens")
|
notifications.error("Error creating screens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,18 +99,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for NewScreenModal
|
// Handler for NewScreenModal
|
||||||
const confirmScreenSelection = async mode => {
|
export const show = mode => {
|
||||||
|
selectedTemplates = null
|
||||||
|
blankScreenUrl = null
|
||||||
screenMode = mode
|
screenMode = mode
|
||||||
|
pendingScreen = null
|
||||||
|
screenAccessRole = Roles.BASIC
|
||||||
|
|
||||||
if (mode === "autoCreate") {
|
if (mode === "table") {
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
} else {
|
} else if (mode === "blank") {
|
||||||
let templates = getTemplates($store, $tables.list)
|
let templates = getTemplates($store, $tables.list)
|
||||||
const blankScreenTemplate = templates.find(
|
const blankScreenTemplate = templates.find(
|
||||||
t => t.id === "createFromScratch"
|
t => t.id === "createFromScratch"
|
||||||
)
|
)
|
||||||
pendingScreen = blankScreenTemplate.create()
|
pendingScreen = blankScreenTemplate.create()
|
||||||
screenDetailsModal.show()
|
screenDetailsModal.show()
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid mode provided")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +156,7 @@
|
||||||
|
|
||||||
// Submit screen config for creation.
|
// Submit screen config for creation.
|
||||||
const confirmScreenCreation = async () => {
|
const confirmScreenCreation = async () => {
|
||||||
if (screenMode === "blankScreen") {
|
if (screenMode === "blank") {
|
||||||
confirmBlankScreenCreation({
|
confirmBlankScreenCreation({
|
||||||
screenUrl: blankScreenUrl,
|
screenUrl: blankScreenUrl,
|
||||||
screenAccessRole,
|
screenAccessRole,
|
||||||
|
@ -166,7 +167,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleSelectBack = () => {
|
const roleSelectBack = () => {
|
||||||
if (screenMode === "blankScreen") {
|
if (screenMode === "blank") {
|
||||||
screenDetailsModal.show()
|
screenDetailsModal.show()
|
||||||
} else {
|
} else {
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
|
@ -174,14 +175,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={newScreenModal}>
|
|
||||||
<NewScreenModal onConfirm={confirmScreenSelection} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={datasourceModal}>
|
<Modal bind:this={datasourceModal}>
|
||||||
<DatasourceModal
|
<DatasourceModal
|
||||||
onConfirm={confirmScreenDatasources}
|
onConfirm={confirmScreenDatasources}
|
||||||
onCancel={() => newScreenModal.show()}
|
|
||||||
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -198,7 +194,6 @@
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal
|
||||||
onConfirm={confirmScreenBlank}
|
onConfirm={confirmScreenBlank}
|
||||||
onCancel={() => newScreenModal.show()}
|
|
||||||
initialUrl={blankScreenUrl}
|
initialUrl={blankScreenUrl}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
|
@ -40,14 +40,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Autogenerated screens"
|
title="Access"
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
{onConfirm}
|
{onConfirm}
|
||||||
{onCancel}
|
{onCancel}
|
||||||
disabled={!!error}
|
disabled={!!error}
|
||||||
>
|
>
|
||||||
Select which level of access you want your screens to have
|
Select the level of access required to see these screens
|
||||||
<Select
|
<Select
|
||||||
bind:value={screenAccessRole}
|
bind:value={screenAccessRole}
|
||||||
on:change={onChangeRole}
|
on:change={onChangeRole}
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -1,67 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, selectedScreen } from "builderStore"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { Layout, Button, Detail, notifications } from "@budibase/bbui"
|
import { store as frontendStore } from "builderStore"
|
||||||
import Logo from "assets/bb-space-man.svg"
|
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
|
||||||
import { Roles } from "constants/backend"
|
|
||||||
|
|
||||||
let loaded = false
|
$: {
|
||||||
|
if ($frontendStore.screens.length > 0) {
|
||||||
const createFirstScreen = async () => {
|
$redirect(`./${$frontendStore.screens[0]._id}`)
|
||||||
let screen = createFromScratchScreen.create()
|
|
||||||
screen.routing.route = "/home"
|
|
||||||
screen.routing.roldId = Roles.BASIC
|
|
||||||
screen.routing.homeScreen = true
|
|
||||||
try {
|
|
||||||
const savedScreen = await store.actions.screens.save(screen)
|
|
||||||
notifications.success("Screen created successfully")
|
|
||||||
$redirect(`./${savedScreen._id}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Could not create screen", err)
|
|
||||||
notifications.error("Error creating screen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if ($selectedScreen) {
|
|
||||||
$redirect(`./${$selectedScreen._id}`)
|
|
||||||
} else if ($store.screens?.length) {
|
|
||||||
$redirect(`./${$store.screens[0]._id}`)
|
|
||||||
} else {
|
} else {
|
||||||
loaded = true
|
$redirect("./new")
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
|
||||||
<div class="centered">
|
|
||||||
<Layout gap="S" justifyItems="center">
|
|
||||||
<img class="img-size" alt="logo" src={Logo} />
|
|
||||||
<div class="new-screen-text">
|
|
||||||
<Detail size="L">LET’S BRING THIS APP TO LIFE</Detail>
|
|
||||||
</div>
|
|
||||||
<Button on:click={createFirstScreen} size="M" cta icon="Add">
|
|
||||||
Create first screen
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.centered {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.new-screen-text {
|
|
||||||
width: 150px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.img-size {
|
|
||||||
width: 170px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
<script>
|
||||||
|
import { Body } from "@budibase/bbui"
|
||||||
|
import CreationPage from "components/common/CreationPage.svelte"
|
||||||
|
import blankImage from "./blank.png"
|
||||||
|
import tableImage from "./table.png"
|
||||||
|
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
let createScreenModal
|
||||||
|
|
||||||
|
$: hasScreens = $store.screens?.length
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<CreationPage
|
||||||
|
showClose={$store.screens.length > 0}
|
||||||
|
onClose={() => $goto(`./${$store.screens[0]._id}`)}
|
||||||
|
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||||
|
>
|
||||||
|
<div class="subHeading">
|
||||||
|
<Body>Start from scratch or create screens from your data</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||||
|
<div class="image">
|
||||||
|
<img alt="" src={blankImage} />
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<Body size="S">Blank screen</Body>
|
||||||
|
<Body size="XS">Add an empty blank screen</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||||
|
<div class="image">
|
||||||
|
<img alt="" src={tableImage} />
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<Body size="S">Table</Body>
|
||||||
|
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CreationPage>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateScreenModal bind:this={createScreenModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
padding: 28px 40px 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeading :global(p) {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 235px;
|
||||||
|
transition: filter 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 127px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
border: 1px solid var(--grey-4);
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
padding: 8px 16px 13px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text :global(p:nth-child(1)) {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text :global(p:nth-child(2)) {
|
||||||
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
</style>
|
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||||
import { Page, Layout } from "@budibase/bbui"
|
import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
import { isOnlyUser } from "builderStore"
|
import { isOnlyUser } from "builderStore"
|
||||||
|
@ -45,16 +45,20 @@
|
||||||
active={$isActive("./version")}
|
active={$isActive("./version")}
|
||||||
/>
|
/>
|
||||||
<div class="delete-action">
|
<div class="delete-action">
|
||||||
<SideNavItem
|
<AbsTooltip
|
||||||
text="Delete app"
|
position={TooltipPosition.Bottom}
|
||||||
on:click={() => {
|
text={$isOnlyUser
|
||||||
deleteModal.show()
|
|
||||||
}}
|
|
||||||
disabled={!$isOnlyUser}
|
|
||||||
tooltip={$isOnlyUser
|
|
||||||
? null
|
? null
|
||||||
: "Unavailable - another user is editing this app"}
|
: "Unavailable - another user is editing this app"}
|
||||||
/>
|
>
|
||||||
|
<SideNavItem
|
||||||
|
text="Delete app"
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
on:click={() => {
|
||||||
|
deleteModal.show()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AbsTooltip>
|
||||||
</div>
|
</div>
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
Heading,
|
Heading,
|
||||||
Body,
|
Body,
|
||||||
Modal,
|
Modal,
|
||||||
|
AbsTooltip,
|
||||||
|
TooltipPosition,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
||||||
|
@ -46,16 +48,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if row.type !== "restore"}
|
{#if row.type !== "restore"}
|
||||||
<MenuItem
|
<AbsTooltip
|
||||||
on:click={restoreDialog.show}
|
position={TooltipPosition.Left}
|
||||||
icon="Revert"
|
text="Unavailable - another user is editing this app"
|
||||||
disabled={!$isOnlyUser}
|
|
||||||
tooltip={$isOnlyUser
|
|
||||||
? null
|
|
||||||
: "Unavailable - another user is editing this app"}
|
|
||||||
>
|
>
|
||||||
Restore
|
<MenuItem
|
||||||
</MenuItem>
|
on:click={restoreDialog.show}
|
||||||
|
icon="Revert"
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</MenuItem>
|
||||||
|
</AbsTooltip>
|
||||||
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
||||||
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script>
|
||||||
|
import { Layout, Body, Button } from "@budibase/bbui"
|
||||||
|
import { downloadStream } from "@budibase/frontend-core"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
loading = true
|
||||||
|
try {
|
||||||
|
await downloadStream(await API.getSystemLogs())
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body>Download your latest logs to share with the Budibase team</Body>
|
||||||
|
<div class="download-button">
|
||||||
|
<Button cta on:click={download} disabled={loading}>
|
||||||
|
<div class="button-content">
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="10" />
|
||||||
|
{/if}
|
||||||
|
Download system logs
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,8 +7,6 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
|
||||||
import { Roles } from "constants/backend"
|
|
||||||
|
|
||||||
let name = "My first app"
|
let name = "My first app"
|
||||||
let url = "my-first-app"
|
let url = "my-first-app"
|
||||||
|
@ -38,11 +36,6 @@
|
||||||
// Create user
|
// Create user
|
||||||
await auth.setInitInfo({})
|
await auth.setInitInfo({})
|
||||||
|
|
||||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
|
||||||
defaultScreenTemplate.routing.route = "/home"
|
|
||||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
|
||||||
await store.actions.screens.save(defaultScreenTemplate)
|
|
||||||
|
|
||||||
appId = createdApp.instance._id
|
appId = createdApp.instance._id
|
||||||
return createdApp
|
return createdApp
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Helpers,
|
||||||
|
Divider,
|
||||||
|
notifications,
|
||||||
|
Icon,
|
||||||
|
TextArea,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { API } from "api"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let diagnosticInfo = ""
|
||||||
|
|
||||||
|
// Make sure page can't be visited directly in cloud
|
||||||
|
$: {
|
||||||
|
if ($admin.cloud) {
|
||||||
|
$redirect("../../portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSystemDebugInfo() {
|
||||||
|
const diagnostics = await API.fetchSystemDebugInfo()
|
||||||
|
diagnosticInfo = {
|
||||||
|
browser: {
|
||||||
|
language: navigator.language || navigator.userLanguage,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
platform: navigator.platform,
|
||||||
|
vendor: navigator.vendor,
|
||||||
|
},
|
||||||
|
server: diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
await Helpers.copyToClipboard(JSON.stringify(diagnosticInfo, undefined, 2))
|
||||||
|
notifications.success("Copied")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await fetchSystemDebugInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.isAdmin && diagnosticInfo}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS">
|
||||||
|
<Heading size="M">Diagnostics</Heading>
|
||||||
|
Please include this diagnostic information in support requests and github issues
|
||||||
|
by clicking the button on the top right to copy to clipboard.
|
||||||
|
<Divider />
|
||||||
|
<Body size="M">
|
||||||
|
<section>
|
||||||
|
<div on:click={copyToClipboard} class="copy-icon">
|
||||||
|
<Icon name="Copy" size="M" />
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
height="45vh"
|
||||||
|
disabled
|
||||||
|
value={JSON.stringify(diagnosticInfo, undefined, 2)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -26,14 +26,12 @@ export function createViewsStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteView = async view => {
|
const deleteView = async view => {
|
||||||
await API.deleteView(view)
|
await API.deleteView(view.name)
|
||||||
|
|
||||||
// Update tables
|
// Update tables
|
||||||
tables.update(state => {
|
tables.update(state => {
|
||||||
const table = state.list.find(table => table._id === view.tableId)
|
const table = state.list.find(table => table._id === view.tableId)
|
||||||
if (table) {
|
delete table.views[view.name]
|
||||||
delete table.views[view.name]
|
|
||||||
}
|
|
||||||
return { ...state }
|
return { ...state }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Version",
|
title: "Version",
|
||||||
href: "/builder/portal/settings/version",
|
href: "/builder/portal/settings/version",
|
||||||
})
|
})
|
||||||
|
settingsSubPages.push({
|
||||||
|
title: "Diagnostics",
|
||||||
|
href: "/builder/portal/settings/diagnostics",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
menu.push({
|
menu.push({
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
@ -85,6 +89,13 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Audit Logs",
|
title: "Audit Logs",
|
||||||
href: "/builder/portal/account/auditLogs",
|
href: "/builder/portal/account/auditLogs",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!$admin.cloud) {
|
||||||
|
accountSubPages.push({
|
||||||
|
title: "System Logs",
|
||||||
|
href: "/builder/portal/account/systemLogs",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
|
|
|
@ -3223,6 +3223,46 @@
|
||||||
"key": "allowManualEntry",
|
"key": "allowManualEntry",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Play sound on scan",
|
||||||
|
"key": "beepOnScan",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Sound pitch",
|
||||||
|
"key": "beepFrequency",
|
||||||
|
"dependsOn": "beepOnScan",
|
||||||
|
"defaultValue": 2637,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Low",
|
||||||
|
"value": 2096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Regular",
|
||||||
|
"value": 2637
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "High",
|
||||||
|
"value": 3136
|
||||||
|
},
|
||||||
|
{ "label": "Custom", "value": "custom" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Sound frequency (Hz)",
|
||||||
|
"key": "customFrequency",
|
||||||
|
"defaultValue": 1046,
|
||||||
|
"min": 20,
|
||||||
|
"max": 8000,
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "beepFrequency",
|
||||||
|
"value": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "validation/string",
|
"type": "validation/string",
|
||||||
"label": "Validation",
|
"label": "Validation",
|
||||||
|
@ -4541,6 +4581,16 @@
|
||||||
"setting": "clickBehaviour",
|
"setting": "clickBehaviour",
|
||||||
"value": "details"
|
"value": "details"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Hide notifications",
|
||||||
|
"key": "notificationOverride",
|
||||||
|
"defaultValue": false,
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "clickBehaviour",
|
||||||
|
"value": "details"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -5178,6 +5228,16 @@
|
||||||
"value": "View",
|
"value": "View",
|
||||||
"invert": true
|
"invert": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Hide notifications",
|
||||||
|
"key": "notificationOverride",
|
||||||
|
"defaultValue": false,
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "showSaveButton",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,27 +8,31 @@
|
||||||
|
|
||||||
let structureLookupMap = {}
|
let structureLookupMap = {}
|
||||||
|
|
||||||
const registerBlockComponent = (id, order, parentId, instance) => {
|
const registerBlockComponent = (id, parentId, order, instance) => {
|
||||||
// Ensure child map exists
|
// Ensure child map exists
|
||||||
if (!structureLookupMap[parentId]) {
|
if (!structureLookupMap[parentId]) {
|
||||||
structureLookupMap[parentId] = {}
|
structureLookupMap[parentId] = {}
|
||||||
}
|
}
|
||||||
// Add this instance in this order, overwriting any existing instance in
|
// Add this instance in this order, overwriting any existing instance in
|
||||||
// this order in case of repeaters
|
// this order in case of repeaters
|
||||||
structureLookupMap[parentId][order] = instance
|
structureLookupMap[parentId][id] = { order, instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterBlockComponent = (order, parentId) => {
|
const unregisterBlockComponent = (id, parentId) => {
|
||||||
// Ensure child map exists
|
// Ensure child map exists
|
||||||
if (!structureLookupMap[parentId]) {
|
if (!structureLookupMap[parentId]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete structureLookupMap[parentId][order]
|
delete structureLookupMap[parentId][id]
|
||||||
}
|
}
|
||||||
|
|
||||||
const eject = () => {
|
const eject = () => {
|
||||||
// Start the new structure with the root component
|
// Start the new structure with the root component
|
||||||
let definition = structureLookupMap[$component.id][0]
|
const rootMap = structureLookupMap[$component.id] || {}
|
||||||
|
let definition = Object.values(rootMap)[0]?.instance
|
||||||
|
if (!definition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Copy styles from block to root component
|
// Copy styles from block to root component
|
||||||
definition._styles = {
|
definition._styles = {
|
||||||
|
@ -49,10 +53,7 @@
|
||||||
const attachChildren = (rootComponent, map) => {
|
const attachChildren = (rootComponent, map) => {
|
||||||
// Transform map into children array
|
// Transform map into children array
|
||||||
let id = rootComponent._id
|
let id = rootComponent._id
|
||||||
const children = Object.entries(map[id] || {}).map(([order, instance]) => ({
|
const children = Object.values(map[id] || {})
|
||||||
order,
|
|
||||||
instance,
|
|
||||||
}))
|
|
||||||
if (!children.length) {
|
if (!children.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
// Create a fake component instance so that we can use the core Component
|
// Create a fake component instance so that we can use the core Component
|
||||||
// to render this part of the block, taking advantage of binding enrichment
|
// to render this part of the block, taking advantage of binding enrichment
|
||||||
$: id = `${block.id}-${context ?? rand}`
|
$: id = `${block.id}-${context ?? rand}`
|
||||||
|
$: parentId = $component?.id
|
||||||
|
$: inBuilder = $builderStore.inBuilder
|
||||||
$: instance = {
|
$: instance = {
|
||||||
_component: `@budibase/standard-components/${type}`,
|
_component: `@budibase/standard-components/${type}`,
|
||||||
_id: id,
|
_id: id,
|
||||||
|
@ -38,14 +40,14 @@
|
||||||
// Register this block component if we're inside the builder so it can be
|
// Register this block component if we're inside the builder so it can be
|
||||||
// ejected later
|
// ejected later
|
||||||
$: {
|
$: {
|
||||||
if ($builderStore.inBuilder) {
|
if (inBuilder) {
|
||||||
block.registerComponent(id, order ?? 0, $component?.id, instance)
|
block.registerComponent(id, parentId, order ?? 0, instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($builderStore.inBuilder) {
|
if (inBuilder) {
|
||||||
block.unregisterComponent(order ?? 0, $component?.id)
|
block.unregisterComponent(id, parentId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
{#if enrichedSearchColumns?.length}
|
{#if enrichedSearchColumns?.length}
|
||||||
{#each enrichedSearchColumns as column, idx}
|
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={column.componentType}
|
type={column.componentType}
|
||||||
props={{
|
props={{
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
export let sidePanelShowDelete
|
export let sidePanelShowDelete
|
||||||
export let sidePanelSaveLabel
|
export let sidePanelSaveLabel
|
||||||
export let sidePanelDeleteLabel
|
export let sidePanelDeleteLabel
|
||||||
|
export let notificationOverride
|
||||||
|
|
||||||
const { fetchDatasourceSchema, API } = getContext("sdk")
|
const { fetchDatasourceSchema, API } = getContext("sdk")
|
||||||
const stateKey = `ID_${generate()}`
|
const stateKey = `ID_${generate()}`
|
||||||
|
@ -169,7 +170,7 @@
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
{#if enrichedSearchColumns?.length}
|
{#if enrichedSearchColumns?.length}
|
||||||
{#each enrichedSearchColumns as column, idx}
|
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={column.componentType}
|
type={column.componentType}
|
||||||
props={{
|
props={{
|
||||||
|
@ -253,6 +254,7 @@
|
||||||
fields: sidePanelFields || normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: editTitle,
|
title: editTitle,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
|
notificationOverride,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
@ -277,6 +279,7 @@
|
||||||
fields: sidePanelFields || normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: "Create Row",
|
title: "Create Row",
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
|
notificationOverride,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
export let rowId
|
export let rowId
|
||||||
export let actionUrl
|
export let actionUrl
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
export let notificationOverride
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
const { fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@
|
||||||
showDeleteButton,
|
showDeleteButton,
|
||||||
schema,
|
schema,
|
||||||
repeaterId,
|
repeaterId,
|
||||||
|
notificationOverride,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let showDeleteButton
|
export let showDeleteButton
|
||||||
export let schema
|
export let schema
|
||||||
export let repeaterId
|
export let repeaterId
|
||||||
|
export let notificationOverride
|
||||||
|
|
||||||
const FieldTypeToComponentMap = {
|
const FieldTypeToComponentMap = {
|
||||||
string: "stringfield",
|
string: "stringfield",
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
parameters: {
|
parameters: {
|
||||||
providerId: formId,
|
providerId: formId,
|
||||||
tableId: dataSource?.tableId,
|
tableId: dataSource?.tableId,
|
||||||
|
notificationOverride,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,10 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let allowManualEntry = false
|
export let allowManualEntry = false
|
||||||
export let scanButtonText = "Scan code"
|
export let scanButtonText = "Scan code"
|
||||||
|
export let beepOnScan = false
|
||||||
|
export let beepFrequency = 2637
|
||||||
|
export let customFrequency = 1046
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let videoEle
|
let videoEle
|
||||||
|
@ -21,8 +25,13 @@
|
||||||
fps: 25,
|
fps: 25,
|
||||||
qrbox: { width: 250, height: 250 },
|
qrbox: { width: 250, height: 250 },
|
||||||
}
|
}
|
||||||
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
||||||
|
|
||||||
const onScanSuccess = decodedText => {
|
const onScanSuccess = decodedText => {
|
||||||
if (value != decodedText) {
|
if (value != decodedText) {
|
||||||
|
if (beepOnScan) {
|
||||||
|
beep()
|
||||||
|
}
|
||||||
dispatch("change", decodedText)
|
dispatch("change", decodedText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +93,27 @@
|
||||||
}
|
}
|
||||||
camModal.hide()
|
camModal.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const beep = () => {
|
||||||
|
const oscillator = audioCtx.createOscillator()
|
||||||
|
const gainNode = audioCtx.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioCtx.destination)
|
||||||
|
|
||||||
|
const frequency =
|
||||||
|
beepFrequency === "custom" ? customFrequency : beepFrequency
|
||||||
|
oscillator.frequency.value = frequency
|
||||||
|
oscillator.type = "square"
|
||||||
|
|
||||||
|
const duration = 420
|
||||||
|
const endTime = audioCtx.currentTime + duration / 1000
|
||||||
|
gainNode.gain.setValueAtTime(1, audioCtx.currentTime)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.001, endTime)
|
||||||
|
|
||||||
|
oscillator.start()
|
||||||
|
oscillator.stop(endTime)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="scanner-video-wrapper">
|
<div class="scanner-video-wrapper">
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let allowManualEntry
|
export let allowManualEntry
|
||||||
export let scanButtonText
|
export let scanButtonText
|
||||||
|
export let beepOnScan
|
||||||
|
export let beepFrequency
|
||||||
|
export let customFrequency
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -42,6 +45,9 @@
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
{allowManualEntry}
|
{allowManualEntry}
|
||||||
scanButtonText={scanText}
|
scanButtonText={scanText}
|
||||||
|
{beepOnScan}
|
||||||
|
{beepFrequency}
|
||||||
|
{customFrequency}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
||||||
import { devToolsStore } from "../../stores"
|
import { devToolsStore, appStore } from "../../stores"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -45,27 +45,41 @@
|
||||||
icon="Code"
|
icon="Code"
|
||||||
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
|
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
|
||||||
>
|
>
|
||||||
{$devToolsStore.visible ? "Close" : "Open"} DevTools
|
DevTools
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
{#if window.parent.isBuilder}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="LinkOut"
|
||||||
|
on:click={() => {
|
||||||
|
window.parent.closePreview?.()
|
||||||
|
window.open(`/${$appStore.appId}`, "_blank")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Fullscreen
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Close"
|
||||||
|
on:click={() => window.parent.closePreview?.()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Close"
|
|
||||||
on:click={() => window.parent.closePreview?.()}
|
|
||||||
>
|
|
||||||
Close preview
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dev-preview-header {
|
.dev-preview-header {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
background-color: black;
|
background-color: black;
|
||||||
padding: 0 var(--spacing-xl);
|
padding: 0 var(--spacing-xl);
|
||||||
grid-template-columns: 1fr auto auto auto;
|
display: flex;
|
||||||
grid-gap: var(--spacing-xl);
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.dev-preview-header :global(.spectrum-Heading) {
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.dev-preview-header.mobile {
|
.dev-preview-header.mobile {
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
|
|
|
@ -478,7 +478,7 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
actions.slice(i + 1),
|
actions.slice(i + 1),
|
||||||
newContext
|
newContext
|
||||||
)
|
)
|
||||||
resolve(await next())
|
resolve(typeof next === "function" ? await next() : true)
|
||||||
} else {
|
} else {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,15 @@ export const buildAppEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets budibase platform debug information.
|
||||||
|
*/
|
||||||
|
fetchSystemDebugInfo: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/debug/diagnostics`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs an app with the production database.
|
* Syncs an app with the production database.
|
||||||
* @param appId the ID of the app to sync
|
* @param appId the ID of the app to sync
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { buildBackupsEndpoints } from "./backups"
|
||||||
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
||||||
import { buildEventEndpoints } from "./events"
|
import { buildEventEndpoints } from "./events"
|
||||||
import { buildAuditLogsEndpoints } from "./auditLogs"
|
import { buildAuditLogsEndpoints } from "./auditLogs"
|
||||||
|
import { buildLogsEndpoints } from "./logs"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Random identifier to uniquely identify a session in a tab. This is
|
* Random identifier to uniquely identify a session in a tab. This is
|
||||||
|
@ -277,5 +278,6 @@ export const createAPIClient = config => {
|
||||||
...buildEnvironmentVariableEndpoints(API),
|
...buildEnvironmentVariableEndpoints(API),
|
||||||
...buildEventEndpoints(API),
|
...buildEventEndpoints(API),
|
||||||
...buildAuditLogsEndpoints(API),
|
...buildAuditLogsEndpoints(API),
|
||||||
|
...buildLogsEndpoints(API),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const buildLogsEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Gets a stream for the system logs.
|
||||||
|
*/
|
||||||
|
getSystemLogs: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: "/api/system/logs",
|
||||||
|
json: false,
|
||||||
|
parseResponse: async response => {
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,60 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { Avatar, Tooltip } from "@budibase/bbui"
|
import { Avatar, AbsTooltip, TooltipPosition } from "@budibase/bbui"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let user
|
export let user
|
||||||
export let size
|
export let size = "S"
|
||||||
export let tooltipDirection = "top"
|
export let tooltipPosition = TooltipPosition.Top
|
||||||
export let showTooltip = true
|
export let showTooltip = true
|
||||||
|
|
||||||
$: tooltipStyle = getTooltipStyle(tooltipDirection)
|
|
||||||
|
|
||||||
const getTooltipStyle = direction => {
|
|
||||||
if (!direction) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (direction === "top") {
|
|
||||||
return "transform: translateX(-50%) translateY(-100%);"
|
|
||||||
} else if (direction === "bottom") {
|
|
||||||
return "transform: translateX(-50%) translateY(100%);"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if user}
|
{#if user}
|
||||||
<div class="user-avatar">
|
<AbsTooltip
|
||||||
|
text={showTooltip ? helpers.getUserLabel(user) : null}
|
||||||
|
position={tooltipPosition}
|
||||||
|
color={helpers.getUserColor(user)}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
{size}
|
{size}
|
||||||
initials={helpers.getUserInitials(user)}
|
initials={helpers.getUserInitials(user)}
|
||||||
color={helpers.getUserColor(user)}
|
color={helpers.getUserColor(user)}
|
||||||
/>
|
/>
|
||||||
{#if showTooltip}
|
</AbsTooltip>
|
||||||
<div class="tooltip" style={tooltipStyle}>
|
|
||||||
<Tooltip
|
|
||||||
direction={tooltipDirection}
|
|
||||||
textWrapping
|
|
||||||
text={helpers.getUserLabel(user)}
|
|
||||||
size="S"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.user-avatar {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
}
|
|
||||||
.user-avatar:hover .tooltip {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script>
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
import { TooltipPosition, Avatar } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let users = []
|
||||||
|
export let order = "ltr"
|
||||||
|
export let size = "S"
|
||||||
|
export let tooltipPosition = TooltipPosition.Top
|
||||||
|
|
||||||
|
$: uniqueUsers = unique(users, order)
|
||||||
|
$: avatars = getAvatars(uniqueUsers, order)
|
||||||
|
|
||||||
|
const unique = users => {
|
||||||
|
let uniqueUsers = {}
|
||||||
|
users?.forEach(user => {
|
||||||
|
uniqueUsers[user.email] = user
|
||||||
|
})
|
||||||
|
return Object.values(uniqueUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAvatars = (users, order) => {
|
||||||
|
const avatars = users.slice(0, 3)
|
||||||
|
if (users.length > 3) {
|
||||||
|
const overflow = {
|
||||||
|
_id: "overflow",
|
||||||
|
label: `+${users.length - 3}`,
|
||||||
|
}
|
||||||
|
if (order === "ltr") {
|
||||||
|
avatars.push(overflow)
|
||||||
|
} else {
|
||||||
|
avatars.unshift(overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avatars.map((user, idx) => ({
|
||||||
|
...user,
|
||||||
|
zIndex: order === "ltr" ? idx : uniqueUsers.length - idx,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="avatars">
|
||||||
|
{#each avatars as user}
|
||||||
|
<span style="z-index:{user.zIndex};">
|
||||||
|
{#if user._id === "overflow"}
|
||||||
|
<Avatar
|
||||||
|
{size}
|
||||||
|
initials={user.label}
|
||||||
|
color="var(--spectrum-global-color-gray-500)"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<UserAvatar {size} {user} {tooltipPosition} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
span:not(:first-of-type) {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.avatars :global(.spectrum-Avatar) {
|
||||||
|
border: 2px solid var(--avatars-background, var(--background));
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ActionButton } from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { config, dispatch } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon="TableColumnAddRight"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => dispatch("add-column")}
|
|
||||||
disabled={!$config.allowSchemaChanges}
|
|
||||||
>
|
|
||||||
Add column
|
|
||||||
</ActionButton>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue