Merge branch 'develop' of github.com:Budibase/budibase into labday/sqs

This commit is contained in:
mike12345567 2023-07-21 18:41:48 +01:00
commit c185dd7dd0
277 changed files with 4759 additions and 2680 deletions

19
.github/stale.yml vendored
View File

@ -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

View File

@ -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

View File

@ -6,7 +6,7 @@ concurrency:
on: on:
push: push:
tags: tags:
- v*-alpha.* - "*-alpha.*"
workflow_dispatch: workflow_dispatch:
env: env:

View File

@ -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
@ -34,7 +34,6 @@ jobs:
exit 1 exit 1
fi fi
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
@ -58,9 +57,12 @@ jobs:
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release yarn release
- name: "Get Previous tag" - name: "Get Current tag"
id: previoustag id: currenttag
uses: "WyriHaximus/github-action-get-previous-tag@v1" run: |
version=v$(./scripts/getCurrentVersion.sh)
echo 'Using tag $version'
echo "::set-output name=tag::$resversionult"
- name: Build/release Docker images - name: Build/release Docker images
run: | run: |
@ -69,7 +71,7 @@ jobs:
env: env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
release-helm-chart: release-helm-chart:
needs: [release-images] needs: [release-images]
@ -96,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"

View File

@ -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

View File

@ -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

29
.github/workflows/stale_bot.yml vendored Normal file
View File

@ -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

View File

@ -1,2 +1,2 @@
nodejs 14.20.1 nodejs 14.21.3
python 3.10.0 python 3.10.0

View File

@ -40,6 +40,24 @@ spec:
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: proxy-service name: proxy-service
livenessProbe:
httpGet:
path: /health
port: {{ .Values.services.proxy.port }}
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /health
port: {{ .Values.services.proxy.port }}
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
ports: ports:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
env: env:

View File

@ -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

View File

@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
### Pro ### Pro
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. @budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro.
Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware.
From here - to develop a change in pro, you can follow the below flow:
``` ```
. # enter the pro submodule
|_ budibase cd packages/pro
|_ budibase-pro # get the base branch you are working from (same as monorepo)
git fetch
git checkout <develop | master>
# create a branch, named the same as the branch in your monorepo
git checkout -b <some branch>
... make changes
# commit the changes you've made, with a message for pro
git commit <something>
# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref"
cd ../..
git add packages/pro
git commit <add the new reference to main repo>
``` ```
From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline.
Note that only budibase maintainers will be able to access the pro repo. Note that only budibase maintainers will be able to access the pro repo.
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
### Troubleshooting ### Troubleshooting
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation.

View File

@ -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=

View File

@ -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;
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.2-alpha.2", "version": "2.8.22-alpha.0",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

12
nx.json
View File

@ -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"
} }
] ]

View File

@ -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": {}
}
} }

View File

@ -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",

View File

@ -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") {

View File

@ -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)

View File

@ -20,6 +20,8 @@ export enum Header {
TYPE = "x-budibase-type", TYPE = "x-budibase-type",
PREVIEW_ROLE = "x-budibase-role", PREVIEW_ROLE = "x-budibase-role",
TENANT_ID = "x-budibase-tenant-id", TENANT_ID = "x-budibase-tenant-id",
VERIFICATION_CODE = "x-budibase-verification-code",
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
TOKEN = "x-budibase-token", TOKEN = "x-budibase-token",
CSRF_TOKEN = "x-csrf-token", CSRF_TOKEN = "x-csrf-token",
CORRELATION_ID = "x-budibase-correlation-id", CORRELATION_ID = "x-budibase-correlation-id",

View File

@ -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)}`
}) })
} }

View File

@ -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" }

View File

@ -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: "" }
} }
} }
@ -155,13 +162,14 @@ 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,
_set(key: any, value: any) { _set(key: any, value: any) {
process.env[key] = value process.env[key] = value
// @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

View File

@ -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

View File

@ -1,37 +1,60 @@
import env from "../../environment"
import pino, { LoggerOptions } from "pino" import pino, { LoggerOptions } from "pino"
import pinoPretty from "pino-pretty"
import { IdentityType } from "@budibase/types"
import env from "../../environment"
import * as context from "../../context" import * as context from "../../context"
import * as correlation from "../correlation" import * as correlation from "../correlation"
import { IdentityType } from "@budibase/types"
import { LOG_CONTEXT } from "../index" import { localFileDestination } from "../system"
// LOGGER // LOGGER
let pinoInstance: pino.Logger | undefined let pinoInstance: pino.Logger | undefined
if (!env.DISABLE_PINO_LOGGER) { if (!env.DISABLE_PINO_LOGGER) {
const level = env.LOG_LEVEL
const pinoOptions: LoggerOptions = { const pinoOptions: LoggerOptions = {
level: env.LOG_LEVEL, level,
formatters: { formatters: {
level: label => { level: level => {
return { level: label.toUpperCase() } return { level: level.toUpperCase() }
}, },
bindings: () => { bindings: () => {
return {} if (env.SELF_HOSTED) {
// "service" is being injected in datadog using the pod names,
// so we should leave it blank to allow the default behaviour if it's not running self-hosted
return {
service: env.SERVICE_NAME,
}
} else {
return {}
}
}, },
}, },
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
} }
if (env.isDev()) { const destinations: pino.StreamEntry[] = []
pinoOptions.transport = {
target: "pino-pretty", destinations.push(
options: { env.isDev()
singleLine: true, ? {
}, stream: pinoPretty({ singleLine: true }),
} level: level as pino.Level,
}
: { stream: process.stdout, level: level as pino.Level }
)
if (env.SELF_HOSTED) {
destinations.push({
stream: localFileDestination(),
level: level as pino.Level,
})
} }
pinoInstance = pino(pinoOptions) pinoInstance = destinations.length
? pino(pinoOptions, pino.multistream(destinations))
: pino(pinoOptions)
// CONSOLE OVERRIDES // CONSOLE OVERRIDES
@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) {
let contextObject = {} let contextObject = {}
if (LOG_CONTEXT) { contextObject = {
contextObject = { tenantId: getTenantId(),
tenantId: getTenantId(), appId: getAppId(),
appId: getAppId(), automationId: getAutomationId(),
automationId: getAutomationId(), identityId: identity?._id,
identityId: identity?._id, identityType: identity?.type,
identityType: identity?.type, correlationId: correlation.getId(),
correlationId: correlation.getId(),
}
} }
const mergingObject: any = { const mergingObject: any = {

View File

@ -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
}

View File

@ -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()
}
)
})
})

View File

@ -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
} }

View File

@ -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>

View File

@ -30,6 +30,7 @@
setContext("drawer-actions", { setContext("drawer-actions", {
hide, hide,
show, show,
headless,
}) })
const easeInOutQuad = x => { const easeInOutQuad = x => {

View File

@ -12,23 +12,24 @@
export let getOptionValue = option => option export let getOptionValue = option => option
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
let tempValue = value const optionValue = e.target.value
let isChecked = e.target.checked if (e.target.checked && !value.includes(optionValue)) {
if (!tempValue.includes(e.target.value) && isChecked) { dispatch("change", [...value, optionValue])
tempValue.push(e.target.value) } else {
dispatch(
"change",
value.filter(x => x !== optionValue)
)
} }
value = tempValue
dispatch(
"change",
tempValue.filter(val => val !== e.target.value || isChecked)
)
} }
</script> </script>
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}> <div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
{#if options && Array.isArray(options)} {#if options && Array.isArray(options)}
{#each options as option} {#each options as option}
{@const optionValue = getOptionValue(option)}
<div <div
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
@ -39,11 +40,11 @@
> >
<input <input
on:change={onChange} on:change={onChange}
value={getOptionValue(option)}
type="checkbox" type="checkbox"
class="spectrum-Checkbox-input" class="spectrum-Checkbox-input"
value={optionValue}
checked={value.includes(optionValue)}
{disabled} {disabled}
checked={value.includes(getOptionValue(option))}
/> />
<span class="spectrum-Checkbox-box"> <span class="spectrum-Checkbox-box">
<svg <svg

View File

@ -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">

View File

@ -62,6 +62,13 @@
} }
} }
const getInputMode = type => {
if (type === "bigint") {
return "numeric"
}
return type === "number" ? "decimal" : "text"
}
onMount(() => { onMount(() => {
focus = autofocus focus = autofocus
if (focus) field.focus() if (focus) field.focus()
@ -103,7 +110,7 @@
{type} {type}
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align};` : ""} style={align ? `text-align: ${align};` : ""}
inputmode={type === "number" ? "decimal" : "text"} inputmode={getInputMode(type)}
{autocomplete} {autocomplete}
/> />
</div> </div>

View File

@ -47,7 +47,7 @@
</svg> </svg>
{#if tooltip && showTooltip} {#if tooltip && showTooltip}
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}> <div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
<Tooltip textWrapping direction="bottom" text={tooltip} /> <Tooltip textWrapping direction="top" text={tooltip} />
</div> </div>
{/if} {/if}
</div> </div>
@ -80,15 +80,14 @@
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
left: 50%; left: 50%;
top: calc(100% + 4px); bottom: calc(100% + 4px);
width: 100vw;
max-width: 150px;
transform: translateX(-50%); transform: translateX(-50%);
text-align: center; text-align: center;
z-index: 1;
} }
.spectrum-Icon--sizeXS { .spectrum-Icon--sizeXS {
width: 10px; width: var(--spectrum-global-dimension-size-150);
height: 10px; height: var(--spectrum-global-dimension-size-150);
} }
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -127,13 +127,17 @@ 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
}) })
export const isOnlyUser = derived(userStore, $userStore => { export const isOnlyUser = derived(userStore, $userStore => {
return $userStore.length === 1 return $userStore.length < 2
}) })

View File

@ -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
})
}
}
},
}) })

View File

@ -4,6 +4,7 @@ import { getSchemaForDatasource } from "../../../dataBinding"
const fieldTypeToComponentMap = { const fieldTypeToComponentMap = {
string: "stringfield", string: "stringfield",
number: "numberfield", number: "numberfield",
bigint: "bigintfield",
options: "optionsfield", options: "optionsfield",
array: "multifieldselect", array: "multifieldselect",
boolean: "booleanfield", boolean: "booleanfield",

View File

@ -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
} }

View File

@ -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>

View File

@ -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}>

View File

@ -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>
&#125;&#125;
</strong>
</div>
</div>
{/if}
</div> </div>
</CodeEditorModal> </CodeEditorModal>
{:else if value.customType === "loopOption"} {:else if value.customType === "loopOption"}

View File

@ -109,6 +109,7 @@
{disableSorting} {disableSorting}
{customPlaceholder} {customPlaceholder}
allowEditRows={allowEditing} allowEditRows={allowEditing}
allowEditColumns={allowEditing}
showAutoColumns={!hideAutocolumns} showAutoColumns={!hideAutocolumns}
{allowClickRows} {allowClickRows}
on:clickrelationship={e => selectRelationship(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)}

View File

@ -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>

View File

@ -18,7 +18,7 @@
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { import {
FIELDS, FIELDS,
RelationshipTypes, RelationshipType,
ALLOWABLE_STRING_OPTIONS, ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS, ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES, ALLOWABLE_STRING_TYPES,
@ -33,6 +33,7 @@
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import { getContext } from "svelte" import { getContext } from "svelte"
import JSONSchemaModal from "./JSONSchemaModal.svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
const AUTO_TYPE = "auto" const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
@ -183,7 +184,7 @@
dispatch("updatecolumns") dispatch("updatecolumns")
if ( if (
saveColumn.type === LINK_TYPE && saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
) { ) {
// Fetching the new tables // Fetching the new tables
tables.fetch() tables.fetch()
@ -237,7 +238,7 @@
// Default relationships many to many // Default relationships many to many
if (editableColumn.type === LINK_TYPE) { if (editableColumn.type === LINK_TYPE) {
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} }
if (editableColumn.type === FORMULA_TYPE) { if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic" editableColumn.formulaType = "dynamic"
@ -285,17 +286,17 @@
{ {
name: `Many ${thisName} rows → many ${linkName} rows`, name: `Many ${thisName} rows → many ${linkName} rows`,
alt: `Many ${table.name} rows → many ${linkTable.name} rows`, alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipTypes.MANY_TO_MANY, value: RelationshipType.MANY_TO_MANY,
}, },
{ {
name: `One ${linkName} row → many ${thisName} rows`, name: `One ${linkName} row → many ${thisName} rows`,
alt: `One ${linkTable.name} rows → many ${table.name} rows`, alt: `One ${linkTable.name} rows → many ${table.name} rows`,
value: RelationshipTypes.ONE_TO_MANY, value: RelationshipType.ONE_TO_MANY,
}, },
{ {
name: `One ${thisName} row → many ${linkName} rows`, name: `One ${thisName} row → many ${linkName} rows`,
alt: `One ${table.name} rows → many ${linkTable.name} rows`, alt: `One ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipTypes.MANY_TO_ONE, value: RelationshipType.MANY_TO_ONE,
}, },
] ]
} }
@ -326,6 +327,7 @@
FIELDS.NUMBER, FIELDS.NUMBER,
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.FORMULA, FIELDS.FORMULA,
FIELDS.BIGINT,
] ]
// no-sql or a spreadsheet // no-sql or a spreadsheet
if (!external || table.sql) { if (!external || table.sql) {
@ -374,7 +376,7 @@
const newError = {} const newError = {}
if (!external && fieldInfo.name?.startsWith("_")) { if (!external && fieldInfo.name?.startsWith("_")) {
newError.name = `Column name cannot start with an underscore.` newError.name = `Column name cannot start with an underscore.`
} else if (fieldInfo.name && !fieldInfo.name.match(/^[_a-zA-Z0-9\s]*$/g)) { } else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
newError.name = `Illegal character; must be alpha-numeric.` newError.name = `Illegal character; must be alpha-numeric.`
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) { } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
newError.name = `${PROHIBITED_COLUMN_NAMES.join( newError.name = `${PROHIBITED_COLUMN_NAMES.join(

View File

@ -95,9 +95,9 @@
{#if !creating} {#if !creating}
<div> <div>
A user's email, role, first and last names cannot be changed from within A user's email, role, first and last names cannot be changed from within
the app builder. Please go to the <Link the app builder. Please go to the
on:click={$goto("/builder/portal/manage/users")}>user portal</Link <Link on:click={$goto("/builder/portal/users/users")}>user portal</Link>
> to do this. to do this.
</div> </div>
{/if} {/if}
<RowFieldControl <RowFieldControl

View File

@ -1,5 +1,5 @@
<script> <script>
import { RelationshipTypes } from "constants/backend" import { RelationshipType } from "constants/backend"
import { import {
keepOpen, keepOpen,
Button, Button,
@ -25,11 +25,11 @@
const relationshipTypes = [ const relationshipTypes = [
{ {
label: "One to Many", label: "One to Many",
value: RelationshipTypes.MANY_TO_ONE, value: RelationshipType.MANY_TO_ONE,
}, },
{ {
label: "Many to Many", label: "Many to Many",
value: RelationshipTypes.MANY_TO_MANY, value: RelationshipType.MANY_TO_MANY,
}, },
] ]
@ -58,8 +58,8 @@
value: table._id, value: table._id,
})) }))
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet() $: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
function getTable(id) { function getTable(id) {
return plusTables.find(table => table._id === id) return plusTables.find(table => table._id === id)
@ -116,7 +116,7 @@
function allRequiredAttributesSet() { function allRequiredAttributesSet() {
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
if (relationshipType === RelationshipTypes.MANY_TO_ONE) { if (relationshipType === RelationshipType.MANY_TO_ONE) {
return base && fromPrimary && fromForeign return base && fromPrimary && fromForeign
} else { } else {
return base && getTable(throughId) && throughFromKey && throughToKey return base && getTable(throughId) && throughFromKey && throughToKey
@ -181,12 +181,12 @@
} }
function otherRelationshipType(type) { function otherRelationshipType(type) {
if (type === RelationshipTypes.MANY_TO_ONE) { if (type === RelationshipType.MANY_TO_ONE) {
return RelationshipTypes.ONE_TO_MANY return RelationshipType.ONE_TO_MANY
} else if (type === RelationshipTypes.ONE_TO_MANY) { } else if (type === RelationshipType.ONE_TO_MANY) {
return RelationshipTypes.MANY_TO_ONE return RelationshipType.MANY_TO_ONE
} else if (type === RelationshipTypes.MANY_TO_MANY) { } else if (type === RelationshipType.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY return RelationshipType.MANY_TO_MANY
} }
} }
@ -218,7 +218,7 @@
// if any to many only need to check from // if any to many only need to check from
const manyToMany = const manyToMany =
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
if (!manyToMany) { if (!manyToMany) {
delete relateFrom.through delete relateFrom.through
@ -253,7 +253,7 @@
} }
relateTo = { relateTo = {
...relateTo, ...relateTo,
relationshipType: RelationshipTypes.ONE_TO_MANY, relationshipType: RelationshipType.ONE_TO_MANY,
foreignKey: relateFrom.fieldName, foreignKey: relateFrom.fieldName,
fieldName: fromPrimary, fieldName: fromPrimary,
} }
@ -321,7 +321,7 @@
fromColumn = toRelationship.name fromColumn = toRelationship.name
} }
relationshipType = relationshipType =
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
if (selectedFromTable) { if (selectedFromTable) {
fromId = selectedFromTable._id fromId = selectedFromTable._id
fromColumn = selectedFromTable.name fromColumn = selectedFromTable.name

View File

@ -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)

View File

@ -1,4 +1,4 @@
import { RelationshipTypes } from "constants/backend" import { RelationshipType } from "constants/backend"
const typeMismatch = "Column type of the foreign key must match the primary key" const typeMismatch = "Column type of the foreign key must match the primary key"
const columnBeingUsed = "Column name cannot be an existing column" const columnBeingUsed = "Column name cannot be an existing column"
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
} }
isMany() { isMany() {
return this.type === RelationshipTypes.MANY_TO_MANY return this.type === RelationshipType.MANY_TO_MANY
} }
relationshipTypeSet(type) { relationshipTypeSet(type) {

View File

@ -1,17 +1,9 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Select, Icon } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { API } from "api" import { API } from "api"
import { parseFile } from "./utils" import { parseFile } from "./utils"
let fileInput
let error = null
let fileName = null
let loading = false
let validation = {}
let validateHash = ""
export let rows = [] export let rows = []
export let schema = {} export let schema = {}
export let allValid = true export let allValid = true
@ -49,6 +41,28 @@
}, },
] ]
let fileInput
let error = null
let fileName = null
let fileType = null
let loading = false
let validation = {}
let validateHash = ""
let errors = {}
$: displayColumnOptions = Object.keys(schema || {}).filter(column => {
return validation[column]
})
$: {
// binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
if (newValidateHash !== validateHash) {
validate(rows, schema)
}
validateHash = newValidateHash
}
$: openFileUpload(promptUpload, fileInput)
async function handleFile(e) { async function handleFile(e) {
loading = true loading = true
error = null error = null
@ -67,34 +81,23 @@
async function validate(rows, schema) { async function validate(rows, schema) {
loading = true loading = true
error = null
validation = {}
allValid = false
try { try {
if (rows.length > 0) { if (rows.length > 0) {
const response = await API.validateNewTableImport({ rows, schema }) const response = await API.validateNewTableImport({ rows, schema })
validation = response.schemaValidation validation = response.schemaValidation
allValid = response.allValid allValid = response.allValid
errors = response.errors
error = null
} }
} catch (e) { } catch (e) {
error = e.message error = e.message
validation = {}
allValid = false
errors = {}
} }
loading = false loading = false
} }
$: {
// binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
if (newValidateHash !== validateHash) {
validate(rows, schema)
}
validateHash = newValidateHash
}
const handleChange = (name, e) => { const handleChange = (name, e) => {
schema[name].type = e.detail schema[name].type = e.detail
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
@ -106,7 +109,13 @@
} }
} }
$: openFileUpload(promptUpload, fileInput) const deleteColumn = name => {
if (loading) {
return
}
delete schema[name]
schema = schema
}
</script> </script>
<div class="dropzone"> <div class="dropzone">
@ -119,10 +128,8 @@
on:change={handleFile} on:change={handleFile}
/> />
<label for="file-upload" class:uploaded={rows.length > 0}> <label for="file-upload" class:uploaded={rows.length > 0}>
{#if loading} {#if error}
loading... Error: {error}
{:else if error}
error: {error}
{:else if fileName} {:else if fileName}
{fileName} {fileName}
{:else} {:else}
@ -142,23 +149,26 @@
placeholder={null} placeholder={null}
getOptionLabel={option => option.label} getOptionLabel={option => option.label}
getOptionValue={option => option.value} getOptionValue={option => option.value}
disabled={loading}
/> />
<span <span
class={loading || validation[column.name] class={validation[column.name]
? "fieldStatusSuccess" ? "fieldStatusSuccess"
: "fieldStatusFailure"} : "fieldStatusFailure"}
> >
{validation[column.name] ? "Success" : "Failure"} {#if validation[column.name]}
Success
{:else}
Failure
{#if errors[column.name]}
<Icon name="Help" tooltip={errors[column.name]} />
{/if}
{/if}
</span> </span>
<i <Icon
class={`omit-button ri-close-circle-fill ${ size="S"
loading ? "omit-button-disabled" : "" name="Close"
}`} hoverable
on:click={() => { on:click={() => deleteColumn(column.name)}
delete schema[column.name]
schema = schema
}}
/> />
</div> </div>
{/each} {/each}
@ -167,7 +177,7 @@
<Select <Select
label="Display Column" label="Display Column"
bind:value={displayColumn} bind:value={displayColumn}
options={Object.keys(schema)} options={displayColumnOptions}
sort sort
/> />
</div> </div>
@ -235,23 +245,16 @@
justify-self: center; justify-self: center;
font-weight: 600; font-weight: 600;
} }
.fieldStatusFailure { .fieldStatusFailure {
color: var(--red); color: var(--red);
justify-self: center; justify-self: center;
font-weight: 600; font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
} }
.fieldStatusFailure :global(.spectrum-Icon) {
.omit-button { width: 12px;
font-size: 1.2em;
color: var(--grey-7);
cursor: pointer;
justify-self: flex-end;
}
.omit-button-disabled {
pointer-events: none;
opacity: 70%;
} }
.display-column { .display-column {

View File

@ -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>

View File

@ -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}`)

View File

@ -1,12 +1,15 @@
<script> <script>
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
import { import {
autocompletion, autocompletion,
closeBrackets, closeBrackets,
completionKeymap, completionKeymap,
closeBracketsKeymap, closeBracketsKeymap,
acceptCompletion,
completionStatus,
} from "@codemirror/autocomplete" } from "@codemirror/autocomplete"
import { import {
EditorView, EditorView,
@ -34,7 +37,8 @@
defaultKeymap, defaultKeymap,
historyKeymap, historyKeymap,
history, history,
indentWithTab, indentMore,
indentLess,
} from "@codemirror/commands" } from "@codemirror/commands"
import { Compartment } from "@codemirror/state" import { Compartment } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
@ -48,6 +52,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 = () => {
@ -77,7 +82,7 @@
// For handlebars only. // For handlebars only.
const bindStyle = new MatchDecorator({ const bindStyle = new MatchDecorator({
regexp: /{{[."#\-\w\s\][]*}}/g, regexp: FIND_ANY_HBS_REGEX,
decoration: () => { decoration: () => {
return Decoration.mark({ return Decoration.mark({
tag: "span", tag: "span",
@ -107,6 +112,22 @@
let isDark = !currentTheme.includes("light") let isDark = !currentTheme.includes("light")
let themeConfig = new Compartment() let themeConfig = new Compartment()
const indentWithTabCustom = {
key: "Tab",
run: view => {
if (completionStatus(view.state) == "active") {
acceptCompletion(view)
return true
}
indentMore(view)
return true
},
shift: view => {
indentLess(view)
return true
},
}
const buildKeymap = () => { const buildKeymap = () => {
const baseMap = [ const baseMap = [
...closeBracketsKeymap, ...closeBracketsKeymap,
@ -114,7 +135,7 @@
...historyKeymap, ...historyKeymap,
...foldKeymap, ...foldKeymap,
...completionKeymap, ...completionKeymap,
indentWithTab, indentWithTabCustom,
] ]
return baseMap return baseMap
} }
@ -131,12 +152,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 +174,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 +213,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))
} }

View File

@ -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>

View File

@ -4,7 +4,8 @@
import { licensing } from "stores/portal" import { licensing } from "stores/portal"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags" import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan $: isBusinessAndAbove =
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
let show let show
let hide let hide
@ -55,22 +56,22 @@
<div class="divider" /> <div class="divider" />
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)} {#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
<a <a
href={isPremiumUser href={isBusinessAndAbove
? "mailto:support@budibase.com" ? "mailto:support@budibase.com"
: "/builder/portal/account/usage"} : "/builder/portal/account/usage"}
> >
<div class="premiumLinkContent" class:disabled={!isPremiumUser}> <div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
<div class="icon"> <div class="icon">
<FontAwesomeIcon name="fa-solid fa-envelope" /> <FontAwesomeIcon name="fa-solid fa-envelope" />
</div> </div>
<Body size="S">Email support</Body> <Body size="S">Email support</Body>
</div> </div>
{#if !isPremiumUser} {#if !isBusinessAndAbove}
<div class="premiumBadge"> <div class="premiumBadge">
<div class="icon"> <div class="icon">
<FontAwesomeIcon name="fa-solid fa-lock" /> <FontAwesomeIcon name="fa-solid fa-lock" />
</div> </div>
<Body size="XS">Premium</Body> <Body size="XS">Business</Body>
</div> </div>
{/if} {/if}
</a> </a>

View File

@ -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>
@ -127,7 +132,6 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
position: relative;
} }
.nav-item.scrollable { .nav-item.scrollable {
flex-direction: column; flex-direction: column;
@ -136,13 +140,16 @@
} }
.nav-item.highlighted { .nav-item.highlighted {
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
--avatars-background: var(--spectrum-global-color-gray-200);
} }
.nav-item.selected { .nav-item.selected {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
color: var(--ink); color: var(--ink);
} }
.nav-item:hover { .nav-item:hover {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
} }
.nav-item:hover .actions { .nav-item:hover .actions {
visibility: visible; visibility: visible;
@ -159,37 +166,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 +221,9 @@
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
order: 2; order: 2;
width: 0; width: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.scrollable .text { .scrollable .text {
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -1,6 +1,7 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { isBuilderInputFocused } from "helpers"
export let store export let store
@ -8,9 +9,16 @@
if (!(e.ctrlKey || e.metaKey)) { if (!(e.ctrlKey || e.metaKey)) {
return return
} }
if (e.shiftKey && e.key === "Z") {
let keyLowerCase = e.key.toLowerCase()
// Ignore events when typing
if (isBuilderInputFocused(e)) {
return
}
if (e.shiftKey && keyLowerCase === "z") {
store.redo() store.redo()
} else if (e.key === "z") { } else if (keyLowerCase === "z") {
store.undo() store.undo()
} }
} }

View File

@ -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}
@ -339,7 +341,7 @@
</Tab> </Tab>
{/if} {/if}
<div class="drawer-actions"> <div class="drawer-actions">
{#if drawerActions?.hide} {#if typeof drawerActions.hide === "function" && drawerActions.headless}
<Button <Button
secondary secondary
quiet quiet
@ -350,7 +352,7 @@
Cancel Cancel
</Button> </Button>
{/if} {/if}
{#if bindingDrawerActions?.save} {#if typeof bindingDrawerActions?.save === "function" && drawerActions.headless}
<Button <Button
cta cta
disabled={!valid} disabled={!valid}

View File

@ -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>

View File

@ -52,6 +52,7 @@ const componentMap = {
"field/sortable": SortableFieldSelect, "field/sortable": SortableFieldSelect,
"field/string": FormFieldSelect, "field/string": FormFieldSelect,
"field/number": FormFieldSelect, "field/number": FormFieldSelect,
"field/bigint": FormFieldSelect,
"field/options": FormFieldSelect, "field/options": FormFieldSelect,
"field/boolean": FormFieldSelect, "field/boolean": FormFieldSelect,
"field/longform": FormFieldSelect, "field/longform": FormFieldSelect,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -228,7 +228,7 @@
on:change={event => (filter.value = event.detail)} on:change={event => (filter.value = event.detail)}
{fillWidth} {fillWidth}
/> />
{:else if ["string", "longform", "number", "formula"].includes(filter.type)} {:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
<Input disabled={filter.noValue} bind:value={filter.value} /> <Input disabled={filter.noValue} bind:value={filter.value} />
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
<Multiselect <Multiselect

View File

@ -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)

View File

@ -419,16 +419,22 @@
if (query && !query.fields.pagination) { if (query && !query.fields.pagination) {
query.fields.pagination = {} query.fields.pagination = {}
} }
dynamicVariables = getDynamicVariables( // if query doesn't have ID then its new - don't try to copy existing dynamic variables
datasource, if (!queryId) {
query._id, dynamicVariables = []
(variable, queryId) => variable.queryId === queryId globalDynamicBindings = getDynamicVariables(datasource)
) } else {
globalDynamicBindings = getDynamicVariables( dynamicVariables = getDynamicVariables(
datasource, datasource,
query._id, query._id,
(variable, queryId) => variable.queryId !== queryId (variable, queryId) => variable.queryId === queryId
) )
globalDynamicBindings = getDynamicVariables(
datasource,
query._id,
(variable, queryId) => variable.queryId !== queryId
)
}
prettifyQueryRequestBody( prettifyQueryRequestBody(
query, query,

View File

@ -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;

View File

@ -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
} }

View File

@ -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,
}))
}
},
},
],
} }
} }

View File

@ -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>

View File

@ -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(),

View File

@ -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

View File

@ -53,6 +53,10 @@ export const FIELDS = {
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" }, numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
}, },
}, },
BIGINT: {
name: "BigInt",
type: "bigint",
},
BOOLEAN: { BOOLEAN: {
name: "Boolean", name: "Boolean",
type: "boolean", type: "boolean",
@ -147,7 +151,7 @@ export function isAutoColumnUserRelationship(subtype) {
) )
} }
export const RelationshipTypes = { export const RelationshipType = {
MANY_TO_MANY: "many-to-many", MANY_TO_MANY: "many-to-many",
ONE_TO_MANY: "one-to-many", ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one", MANY_TO_ONE: "many-to-one",

View File

@ -29,3 +29,15 @@ export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
export const get_name = s => (!s ? "" : last(s.split("/"))) export const get_name = s => (!s ? "" : last(s.split("/")))
export const get_capitalised_name = name => pipe(name, [get_name, capitalise]) export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
export const isBuilderInputFocused = e => {
const activeTag = document.activeElement?.tagName.toLowerCase()
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
if (
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
e.key !== "Escape"
) {
return true
}
return false
}

View File

@ -7,4 +7,5 @@ export {
get_name, get_name,
get_capitalised_name, get_capitalised_name,
lowercase, lowercase,
isBuilderInputFocused,
} from "./helpers" } from "./helpers"

View File

@ -15,6 +15,7 @@
} }
onMount(() => { onMount(() => {
window.isBuilder = true
window.closePreview = () => { window.closePreview = () => {
store.update(state => ({ store.update(state => ({
...state, ...state,

View File

@ -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>

View File

@ -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;

View File

@ -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({

View File

@ -74,11 +74,12 @@
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 5px; border-radius: 5px;
width: 100%; width: 100%;
background-color: #00000047; background: var(--spectrum-global-color-gray-50);
color: white; color: white;
overflow: hidden; overflow: hidden;
padding: 12px 16px; padding: 12px 16px;
box-sizing: border-box; box-sizing: border-box;
transition: background 130ms ease-out;
} }
.left { .left {
flex: 1; flex: 1;
@ -94,7 +95,7 @@
} }
.button:hover { .button:hover {
cursor: pointer; cursor: pointer;
filter: brightness(1.2); background: var(--spectrum-global-color-gray-100);
} }
.connected { .connected {
display: flex; display: flex;

View File

@ -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">

View File

@ -18,7 +18,7 @@
const onClick = dynamicVariable => { const onClick = dynamicVariable => {
const queryId = dynamicVariable.queryId const queryId = dynamicVariable.queryId
queries.select({ _id: queryId }) queries.select({ _id: queryId })
$goto(`./${queryId}`) $goto(`../../query/${queryId}`)
} }
/** /**

View File

@ -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;

View File

@ -5,6 +5,7 @@
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { isBuilderInputFocused } from "helpers"
let confirmDeleteDialog let confirmDeleteDialog
let confirmEjectDialog let confirmEjectDialog
@ -100,13 +101,7 @@
return return
} }
// Ignore events when typing // Ignore events when typing
const activeTag = document.activeElement?.tagName.toLowerCase() if (isBuilderInputFocused(e)) {
const inCodeEditor =
document.activeElement?.classList?.contains("cm-content")
if (
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
e.key !== "Escape"
) {
return return
} }
// Key events are always for the selected component // Key events are always for the selected component

View File

@ -15,12 +15,7 @@
{ {
"name": "Layout", "name": "Layout",
"icon": "ClassicGridView", "icon": "ClassicGridView",
"children": [ "children": ["container", "section", "grid", "sidepanel"]
"container",
"section",
"grid",
"sidepanel"
]
}, },
{ {
"name": "Data", "name": "Data",
@ -63,6 +58,7 @@
"fieldgroup", "fieldgroup",
"stringfield", "stringfield",
"numberfield", "numberfield",
"bigintfield",
"passwordfield", "passwordfield",
"optionsfield", "optionsfield",
"booleanfield", "booleanfield",
@ -79,13 +75,6 @@
{ {
"name": "Chart", "name": "Chart",
"icon": "GraphBarVertical", "icon": "GraphBarVertical",
"children": [ "children": ["bar", "line", "area", "candlestick", "pie", "donut"]
"bar",
"line",
"area",
"candlestick",
"pie",
"donut"
]
} }
] ]

View File

@ -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>

View File

@ -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"

View File

@ -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} />

View File

@ -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>

View File

@ -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

View File

@ -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">LETS 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>

Some files were not shown because too many files have changed in this diff Show More