Merge branch 'master' into fix/security-deps
This commit is contained in:
commit
26aa47078b
229 changed files with 4342 additions and 2640 deletions
114
.github/workflows/budibase_ci.yml
vendored
114
.github/workflows/budibase_ci.yml
vendored
|
@ -18,6 +18,8 @@ env:
|
|||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
NX_BASE_BRANCH: origin/${{ github.base_ref }}
|
||||
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -25,20 +27,20 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != 'Budibase/budibase'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
|
@ -46,45 +48,66 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != 'Budibase/budibase'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
# Run build all the projects
|
||||
- run: yarn build
|
||||
- name: Build
|
||||
run: |
|
||||
yarn build
|
||||
# Check the types of the projects built via esbuild
|
||||
- run: yarn check:types
|
||||
- name: Check types
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn check:types --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn check:types
|
||||
fi
|
||||
|
||||
test-libraries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != 'Budibase/budibase'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Test
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||
fi
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
|
@ -96,21 +119,31 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != 'Budibase/budibase'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Test worker and server
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||
fi
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
||||
|
@ -119,42 +152,50 @@ jobs:
|
|||
|
||||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn test --scope=@budibase/pro
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Test
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn test --scope=@budibase/pro
|
||||
fi
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != 'Budibase/budibase'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn build --projects=@budibase/server,@budibase/worker,@budibase/client
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Build packages
|
||||
run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd qa-core
|
||||
|
@ -166,13 +207,12 @@ jobs:
|
|||
|
||||
check-pro-submodule:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Budibase/budibase'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Check pro commit
|
||||
|
|
29
.github/workflows/check_unreleased_changes.yml
vendored
Normal file
29
.github/workflows/check_unreleased_changes.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: check_unreleased_changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_unreleased:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for unreleased changes
|
||||
env:
|
||||
REPO: "Budibase/budibase"
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
RELEASE_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||
"https://api.github.com/repos/$REPO/releases/latest" | \
|
||||
jq -r .published_at)
|
||||
COMMIT_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||
"https://api.github.com/repos/$REPO/commits/master" | \
|
||||
jq -r .commit.committer.date)
|
||||
RELEASE_SECONDS=$(date --date="$RELEASE_TIMESTAMP" "+%s")
|
||||
COMMIT_SECONDS=$(date --date="$COMMIT_TIMESTAMP" "+%s")
|
||||
if (( COMMIT_SECONDS > RELEASE_SECONDS )); then
|
||||
echo "There are unreleased changes. Please release these changes before merging."
|
||||
exit 1
|
||||
fi
|
||||
echo "No unreleased changes detected."
|
2
.github/workflows/release-develop.yml
vendored
2
.github/workflows/release-develop.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
|||
v14.20.1
|
||||
v18.17.0
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
nodejs 14.21.3
|
||||
nodejs 18.17.0
|
||||
python 3.10.0
|
||||
yarn 1.22.19
|
||||
yarn 1.22.19
|
||||
|
|
71
.vscode/launch.json
vendored
71
.vscode/launch.json
vendored
|
@ -1,42 +1,31 @@
|
|||
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Budibase Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"-r",
|
||||
"ts-node/register/transpile-only"
|
||||
],
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/server/src/index.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/server"
|
||||
},
|
||||
{
|
||||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"-r",
|
||||
"ts-node/register/transpile-only"
|
||||
],
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/worker/src/index.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Start Budibase",
|
||||
"configurations": ["Budibase Server", "Budibase Worker"]
|
||||
}
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Budibase Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||
"args": ["${workspaceFolder}/packages/server/src/index.ts"],
|
||||
"cwd": "${workspaceFolder}/packages/server"
|
||||
},
|
||||
{
|
||||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Start Budibase",
|
||||
"configurations": ["Budibase Server", "Budibase Worker"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ Component libraries are collections of components as well as the definition of t
|
|||
|
||||
#### 1. Prerequisites
|
||||
|
||||
- NodeJS version `14.x.x`
|
||||
- NodeJS version `18.x.x`
|
||||
- Python version `3.x`
|
||||
|
||||
### Using asdf (recommended)
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
# optional ports are specified throughout for more advanced use cases.
|
||||
|
||||
services:
|
||||
minio-service:
|
||||
restart: on-failure
|
||||
# Last version that supports the "fs" backend
|
||||
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
|
||||
ports:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
couchdb-service:
|
||||
# platform: linux/amd64
|
||||
restart: on-failure
|
||||
image: budibase/couchdb
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
- COUCHDB_USER=${COUCH_DB_USER}
|
||||
ports:
|
||||
- "5984"
|
||||
- "4369"
|
||||
- "9100"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
redis-service:
|
||||
restart: on-failure
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- "6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
|
@ -1,7 +1,7 @@
|
|||
FROM node:14-slim as build
|
||||
FROM node:18-slim as build
|
||||
|
||||
# install node-gyp dependencies
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3
|
||||
|
||||
# add pin script
|
||||
WORKDIR /
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
module.exports = () => {
|
||||
return {
|
||||
dockerCompose: {
|
||||
composeFilePath: "../../hosting",
|
||||
composeFile: "docker-compose.test.yaml",
|
||||
startupTimeout: 10000,
|
||||
},
|
||||
couchdb: {
|
||||
image: "budibase/couchdb",
|
||||
ports: [5984],
|
||||
env: {
|
||||
COUCHDB_PASSWORD: "budibase",
|
||||
COUCHDB_USER: "budibase",
|
||||
},
|
||||
wait: {
|
||||
type: "ports",
|
||||
timeout: 20000,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.9.31",
|
||||
"version": "2.9.32",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||
"build": "yarn nx run-many -t=build",
|
||||
"build": "lerna run build --stream",
|
||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||
"check:types": "lerna run check:types",
|
||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||
|
@ -109,7 +109,7 @@
|
|||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <15.0.0"
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
14
packages/backend-core/src/cache/user.ts
vendored
14
packages/backend-core/src/cache/user.ts
vendored
|
@ -4,6 +4,8 @@ import * as context from "../context"
|
|||
import * as platform from "../platform"
|
||||
import env from "../environment"
|
||||
import * as accounts from "../accounts"
|
||||
import { UserDB } from "../users"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
const EXPIRY_SECONDS = 3600
|
||||
|
||||
|
@ -60,6 +62,18 @@ export async function getUser(
|
|||
// make sure the tenant ID is always correct/set
|
||||
user.tenantId = tenantId
|
||||
}
|
||||
// if has groups, could have builder permissions granted by a group
|
||||
if (user.userGroups && !sdk.users.isGlobalBuilder(user)) {
|
||||
await context.doInTenant(tenantId, async () => {
|
||||
const appIds = await UserDB.getGroupBuilderAppIds(user)
|
||||
if (appIds.length) {
|
||||
const existing = user.builder?.apps || []
|
||||
user.builder = {
|
||||
apps: [...new Set(existing.concat(appIds))],
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import fetch from "node-fetch"
|
||||
import { getCouchInfo } from "./couch"
|
||||
import { SearchFilters, Row } from "@budibase/types"
|
||||
import { createUserIndex } from "./searchIndexes/searchIndexes"
|
||||
import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
|
||||
|
||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||
|
||||
|
@ -65,6 +64,7 @@ export class QueryBuilder<T> {
|
|||
this.#index = index
|
||||
this.#query = {
|
||||
allOr: false,
|
||||
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||||
string: {},
|
||||
fuzzy: {},
|
||||
range: {},
|
||||
|
@ -218,6 +218,10 @@ export class QueryBuilder<T> {
|
|||
this.#query.allOr = true
|
||||
}
|
||||
|
||||
setOnEmptyFilter(value: EmptyFilterOption) {
|
||||
this.#query.onEmptyFilter = value
|
||||
}
|
||||
|
||||
handleSpaces(input: string) {
|
||||
if (this.#noEscaping) {
|
||||
return input
|
||||
|
@ -289,8 +293,9 @@ export class QueryBuilder<T> {
|
|||
const builder = this
|
||||
let allOr = this.#query && this.#query.allOr
|
||||
let query = allOr ? "" : "*:*"
|
||||
let allFiltersEmpty = true
|
||||
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
||||
let tableId
|
||||
let tableId: string = ""
|
||||
if (this.#query.equal!.tableId) {
|
||||
tableId = this.#query.equal!.tableId
|
||||
delete this.#query.equal!.tableId
|
||||
|
@ -305,7 +310,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const contains = (key: string, value: any, mode = "AND") => {
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
|
@ -384,6 +389,12 @@ export class QueryBuilder<T> {
|
|||
built += ` ${mode} `
|
||||
}
|
||||
built += expression
|
||||
if (
|
||||
(typeof value !== "string" && value != null) ||
|
||||
(typeof value === "string" && value !== tableId && value !== "")
|
||||
) {
|
||||
allFiltersEmpty = false
|
||||
}
|
||||
}
|
||||
if (opts?.returnBuilt) {
|
||||
return built
|
||||
|
@ -463,6 +474,13 @@ export class QueryBuilder<T> {
|
|||
allOr = false
|
||||
build({ tableId }, equal)
|
||||
}
|
||||
if (allFiltersEmpty) {
|
||||
if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
|
||||
return ""
|
||||
} else if (this.#query?.allOr) {
|
||||
return query.replace("()", "(*:*)")
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { newid } from "../../docIds/newid"
|
||||
import { getDB } from "../db"
|
||||
import { Database } from "@budibase/types"
|
||||
import { Database, EmptyFilterOption } from "@budibase/types"
|
||||
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
||||
|
||||
const INDEX_NAME = "main"
|
||||
|
@ -156,6 +156,76 @@ describe("lucene", () => {
|
|||
expect(resp.rows.length).toBe(2)
|
||||
})
|
||||
|
||||
describe("empty filters behaviour", () => {
|
||||
it("should return all rows by default", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.addEqual("property", "")
|
||||
builder.addEqual("number", null)
|
||||
builder.addString("property", "")
|
||||
builder.addFuzzy("property", "")
|
||||
builder.addNotEqual("number", undefined)
|
||||
builder.addOneOf("number", null)
|
||||
builder.addContains("array", undefined)
|
||||
builder.addNotContains("array", null)
|
||||
builder.addContainsAny("array", null)
|
||||
|
||||
const resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
})
|
||||
|
||||
it("should return all rows when onEmptyFilter is ALL", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_ALL)
|
||||
builder.setAllOr()
|
||||
builder.addEqual("property", "")
|
||||
builder.addEqual("number", null)
|
||||
builder.addString("property", "")
|
||||
builder.addFuzzy("property", "")
|
||||
builder.addNotEqual("number", undefined)
|
||||
builder.addOneOf("number", null)
|
||||
builder.addContains("array", undefined)
|
||||
builder.addNotContains("array", null)
|
||||
builder.addContainsAny("array", null)
|
||||
|
||||
const resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
})
|
||||
|
||||
it("should return no rows when onEmptyFilter is NONE", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
|
||||
builder.addEqual("property", "")
|
||||
builder.addEqual("number", null)
|
||||
builder.addString("property", "")
|
||||
builder.addFuzzy("property", "")
|
||||
builder.addNotEqual("number", undefined)
|
||||
builder.addOneOf("number", null)
|
||||
builder.addContains("array", undefined)
|
||||
builder.addNotContains("array", null)
|
||||
builder.addContainsAny("array", null)
|
||||
|
||||
const resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should return all matching rows when onEmptyFilter is NONE, but a filter value is provided", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
|
||||
builder.addEqual("property", "")
|
||||
builder.addEqual("number", 1)
|
||||
builder.addString("property", "")
|
||||
builder.addFuzzy("property", "")
|
||||
builder.addNotEqual("number", undefined)
|
||||
builder.addOneOf("number", null)
|
||||
builder.addContains("array", undefined)
|
||||
builder.addNotContains("array", null)
|
||||
builder.addContainsAny("array", null)
|
||||
|
||||
const resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("skip", () => {
|
||||
const skipDbName = `db-${newid()}`
|
||||
let docs: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import env from "../environment"
|
||||
import * as context from "../context"
|
||||
export * from "./installation"
|
||||
|
||||
/**
|
||||
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
17
packages/backend-core/src/features/installation.ts
Normal file
17
packages/backend-core/src/features/installation.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export function processFeatureEnvVar<T>(
|
||||
fullList: string[],
|
||||
featureList?: string
|
||||
) {
|
||||
let list
|
||||
if (!featureList) {
|
||||
list = fullList
|
||||
} else {
|
||||
list = featureList.split(",")
|
||||
}
|
||||
for (let feature of list) {
|
||||
if (!fullList.includes(feature)) {
|
||||
throw new Error(`Feature: ${feature} is not an allowed option`)
|
||||
}
|
||||
}
|
||||
return list as unknown as T[]
|
||||
}
|
|
@ -6,7 +6,8 @@ export * as roles from "./security/roles"
|
|||
export * as permissions from "./security/permissions"
|
||||
export * as accounts from "./accounts"
|
||||
export * as installation from "./installation"
|
||||
export * as featureFlags from "./featureFlags"
|
||||
export * as featureFlags from "./features"
|
||||
export * as features from "./features/installation"
|
||||
export * as sessions from "./security/sessions"
|
||||
export * as platform from "./platform"
|
||||
export * as auth from "./auth"
|
||||
|
|
|
@ -5,11 +5,12 @@ import env from "../environment"
|
|||
|
||||
export default async (ctx: UserCtx, next: any) => {
|
||||
const appId = getAppId()
|
||||
const builderFn = env.isWorker()
|
||||
? hasBuilderPermissions
|
||||
: env.isApps()
|
||||
? isBuilder
|
||||
: undefined
|
||||
const builderFn =
|
||||
env.isWorker() || !appId
|
||||
? hasBuilderPermissions
|
||||
: env.isApps()
|
||||
? isBuilder
|
||||
: undefined
|
||||
if (!builderFn) {
|
||||
throw new Error("Service name unknown - middleware inactive.")
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import env from "../environment"
|
|||
|
||||
export default async (ctx: UserCtx, next: any) => {
|
||||
const appId = getAppId()
|
||||
const builderFn = env.isWorker()
|
||||
? hasBuilderPermissions
|
||||
: env.isApps()
|
||||
? isBuilder
|
||||
: undefined
|
||||
const builderFn =
|
||||
env.isWorker() || !appId
|
||||
? hasBuilderPermissions
|
||||
: env.isApps()
|
||||
? isBuilder
|
||||
: undefined
|
||||
if (!builderFn) {
|
||||
throw new Error("Service name unknown - middleware inactive.")
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ export const BUILTIN_PERMISSIONS = {
|
|||
permissions: [
|
||||
new Permission(PermissionType.QUERY, PermissionLevel.READ),
|
||||
new Permission(PermissionType.TABLE, PermissionLevel.READ),
|
||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
WRITE: {
|
||||
|
@ -87,7 +86,6 @@ export const BUILTIN_PERMISSIONS = {
|
|||
permissions: [
|
||||
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
],
|
||||
},
|
||||
|
@ -98,7 +96,6 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.USER, PermissionLevel.READ),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
new Permission(PermissionType.VIEW, PermissionLevel.READ),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
|
@ -109,7 +106,6 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.TABLE, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.USER, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.VIEW, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
|
||||
],
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
import env from "../environment"
|
||||
import * as eventHelpers from "./events"
|
||||
import * as accounts from "../accounts"
|
||||
import * as accountSdk from "../accounts"
|
||||
import * as cache from "../cache"
|
||||
import { getIdentity, getTenantId, getGlobalDB } from "../context"
|
||||
import { getGlobalDB, getIdentity, getTenantId } from "../context"
|
||||
import * as dbUtils from "../db"
|
||||
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||
import * as platform from "../platform"
|
||||
import * as sessions from "../security/sessions"
|
||||
import * as usersCore from "./users"
|
||||
import {
|
||||
Account,
|
||||
AllDocsResponse,
|
||||
BulkUserCreated,
|
||||
BulkUserDeleted,
|
||||
isSSOAccount,
|
||||
isSSOUser,
|
||||
RowResponse,
|
||||
SaveUserOpts,
|
||||
User,
|
||||
Account,
|
||||
isSSOUser,
|
||||
isSSOAccount,
|
||||
UserStatus,
|
||||
UserGroup,
|
||||
ContextUser,
|
||||
} from "@budibase/types"
|
||||
import * as accountSdk from "../accounts"
|
||||
import {
|
||||
validateUniqueUser,
|
||||
getAccountHolderFromUserIds,
|
||||
isAdmin,
|
||||
validateUniqueUser,
|
||||
} from "./utils"
|
||||
import { searchExistingEmails } from "./lookup"
|
||||
import { hash } from "../utils"
|
||||
|
@ -32,8 +34,14 @@ import { hash } from "../utils"
|
|||
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
|
||||
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
|
||||
type FeatureFn = () => Promise<Boolean>
|
||||
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
|
||||
type GroupBuildersFn = (user: User) => Promise<string[]>
|
||||
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
|
||||
type GroupFns = { addUsers: GroupUpdateFn }
|
||||
type GroupFns = {
|
||||
addUsers: GroupUpdateFn
|
||||
getBulk: GroupGetFn
|
||||
getGroupBuilderAppIds: GroupBuildersFn
|
||||
}
|
||||
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
||||
|
||||
const bulkDeleteProcessing = async (dbUser: User) => {
|
||||
|
@ -179,6 +187,14 @@ export class UserDB {
|
|||
return user
|
||||
}
|
||||
|
||||
static async bulkGet(userIds: string[]) {
|
||||
return await usersCore.bulkGetGlobalUsersById(userIds)
|
||||
}
|
||||
|
||||
static async bulkUpdate(users: User[]) {
|
||||
return await usersCore.bulkUpdateGlobalUsers(users)
|
||||
}
|
||||
|
||||
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
|
||||
// default booleans to true
|
||||
if (opts.hashPassword == null) {
|
||||
|
@ -457,4 +473,12 @@ export class UserDB {
|
|||
await cache.user.invalidateUser(userId)
|
||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||
}
|
||||
|
||||
static async getGroups(groupIds: string[]) {
|
||||
return await this.groups.getBulk(groupIds)
|
||||
}
|
||||
|
||||
static async getGroupBuilderAppIds(user: User) {
|
||||
return await this.groups.getGroupBuilderAppIds(user)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,10 @@ export const useAuditLogs = () => {
|
|||
return useFeature(Feature.AUDIT_LOGS)
|
||||
}
|
||||
|
||||
export const usePublicApiUserRoles = () => {
|
||||
return useFeature(Feature.USER_ROLE_PUBLIC_API)
|
||||
}
|
||||
|
||||
export const useScimIntegration = () => {
|
||||
return useFeature(Feature.SCIM)
|
||||
}
|
||||
|
@ -98,6 +102,10 @@ export const useAppBuilders = () => {
|
|||
return useFeature(Feature.APP_BUILDERS)
|
||||
}
|
||||
|
||||
export const useViewPermissions = () => {
|
||||
return useFeature(Feature.VIEW_PERMISSIONS)
|
||||
}
|
||||
|
||||
// QUOTAS
|
||||
|
||||
export const setAutomationLogsQuota = (value: number) => {
|
||||
|
|
|
@ -32,8 +32,8 @@ function getTestContainerSettings(
|
|||
): string | null {
|
||||
const entry = Object.entries(global).find(
|
||||
([k]) =>
|
||||
k.includes(`_${serverName.toUpperCase()}`) &&
|
||||
k.includes(`_${key.toUpperCase()}__`)
|
||||
k.includes(`${serverName.toUpperCase()}`) &&
|
||||
k.includes(`${key.toUpperCase()}`)
|
||||
)
|
||||
if (!entry) {
|
||||
return null
|
||||
|
@ -67,27 +67,14 @@ function getContainerInfo(containerName: string, port: number) {
|
|||
}
|
||||
|
||||
function getCouchConfig() {
|
||||
return getContainerInfo("couchdb-service", 5984)
|
||||
}
|
||||
|
||||
function getMinioConfig() {
|
||||
return getContainerInfo("minio-service", 9000)
|
||||
}
|
||||
|
||||
function getRedisConfig() {
|
||||
return getContainerInfo("redis-service", 6379)
|
||||
return getContainerInfo("couchdb", 5984)
|
||||
}
|
||||
|
||||
export function setupEnv(...envs: any[]) {
|
||||
const couch = getCouchConfig(),
|
||||
minio = getCouchConfig(),
|
||||
redis = getRedisConfig()
|
||||
const couch = getCouchConfig()
|
||||
const configs = [
|
||||
{ key: "COUCH_DB_PORT", value: couch.port },
|
||||
{ key: "COUCH_DB_URL", value: couch.url },
|
||||
{ key: "MINIO_PORT", value: minio.port },
|
||||
{ key: "MINIO_URL", value: minio.url },
|
||||
{ key: "REDIS_URL", value: redis.url },
|
||||
]
|
||||
|
||||
for (const config of configs.filter(x => !!x.value)) {
|
||||
|
|
|
@ -32,11 +32,10 @@ export default function positionDropdown(element, opts) {
|
|||
left: null,
|
||||
top: null,
|
||||
}
|
||||
|
||||
// Determine vertical styles
|
||||
if (align === "right-outside") {
|
||||
styles.top = anchorBounds.top
|
||||
} else if (window.innerHeight - anchorBounds.bottom < 100) {
|
||||
} else if (window.innerHeight - anchorBounds.bottom < (maxHeight || 100)) {
|
||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||
styles.maxHeight = maxHeight || 240
|
||||
} else {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import Popover from "../Popover/Popover.svelte"
|
||||
import Layout from "../Layout/Layout.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import clickOutside from "../Actions/click_outside"
|
||||
import { fly } from "svelte/transition"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Input from "../Form/Input.svelte"
|
||||
import { capitalise } from "../helpers"
|
||||
|
@ -10,9 +10,11 @@
|
|||
export let value
|
||||
export let size = "M"
|
||||
export let spectrumTheme
|
||||
export let alignRight = false
|
||||
export let offset
|
||||
export let align
|
||||
|
||||
let open = false
|
||||
let dropdown
|
||||
let preview
|
||||
|
||||
$: customValue = getCustomValue(value)
|
||||
$: checkColor = getCheckColor(value)
|
||||
|
@ -82,7 +84,7 @@
|
|||
|
||||
const onChange = value => {
|
||||
dispatch("change", value)
|
||||
open = false
|
||||
dropdown.hide()
|
||||
}
|
||||
|
||||
const getCustomValue = value => {
|
||||
|
@ -119,30 +121,25 @@
|
|||
|
||||
return "var(--spectrum-global-color-static-gray-900)"
|
||||
}
|
||||
|
||||
const handleOutsideClick = event => {
|
||||
if (open) {
|
||||
event.stopPropagation()
|
||||
open = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
||||
<div
|
||||
class="fill {spectrumTheme || ''}"
|
||||
style={value ? `background: ${value};` : ""}
|
||||
class:placeholder={!value}
|
||||
/>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
use:clickOutside={handleOutsideClick}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:spectrum-Popover--align-right={alignRight}
|
||||
>
|
||||
<div
|
||||
bind:this={preview}
|
||||
class="preview size--{size || 'M'}"
|
||||
on:click={() => {
|
||||
dropdown.toggle()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="fill {spectrumTheme || ''}"
|
||||
style={value ? `background: ${value};` : ""}
|
||||
class:placeholder={!value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Popover bind:this={dropdown} anchor={preview} maxHeight={320} {offset} {align}>
|
||||
<Layout paddingX="XL" paddingY="L">
|
||||
<div class="container">
|
||||
{#each categories as category}
|
||||
<div class="category">
|
||||
<div class="heading">{category.label}</div>
|
||||
|
@ -187,8 +184,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
@ -248,20 +245,6 @@
|
|||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.spectrum-Popover {
|
||||
width: 210px;
|
||||
z-index: 999;
|
||||
top: 100%;
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.spectrum-Popover--align-right {
|
||||
right: 0;
|
||||
}
|
||||
.colors {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
|
@ -297,7 +280,11 @@
|
|||
.category--custom .heading {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.spectrum-wrapper {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,14 @@
|
|||
open = false
|
||||
}
|
||||
|
||||
export const toggle = () => {
|
||||
if (!open) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOutsideClick = e => {
|
||||
if (open) {
|
||||
// Stop propagation if the source is the anchor
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { getDeploymentStore } from "./store/deployments"
|
||||
import { derived } from "svelte/store"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createHistoryStore } from "builderStore/store/history"
|
||||
|
@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => {
|
|||
export const selectedComponent = derived(
|
||||
[store, selectedScreen],
|
||||
([$store, $selectedScreen]) => {
|
||||
if (
|
||||
$selectedScreen &&
|
||||
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||
) {
|
||||
return $selectedScreen?.props
|
||||
}
|
||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
|
@ -141,3 +147,5 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
|||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
||||
|
||||
export const screensHeight = writable("210px")
|
||||
|
|
|
@ -225,7 +225,6 @@ export const getFrontendStore = () => {
|
|||
// Select new screen
|
||||
store.update(state => {
|
||||
state.selectedScreenId = screen._id
|
||||
state.selectedComponentId = screen.props?._id
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -769,9 +768,13 @@ export const getFrontendStore = () => {
|
|||
else {
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Find the selected component
|
||||
let selectedComponentId = state.selectedComponentId
|
||||
if (selectedComponentId.startsWith(`${screen._id}-`)) {
|
||||
selectedComponentId = screen?.props._id
|
||||
}
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
state.selectedComponentId
|
||||
selectedComponentId
|
||||
)
|
||||
if (!currentComponent) {
|
||||
return false
|
||||
|
@ -994,12 +997,20 @@ export const getFrontendStore = () => {
|
|||
const componentId = state.selectedComponentId
|
||||
const screen = get(selectedScreen)
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
|
||||
// Check we aren't right at the top of the tree
|
||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||
if (!parent || componentId === screen.props._id) {
|
||||
|
||||
// Check for screen and navigation component edge cases
|
||||
const screenComponentId = `${screen._id}-screen`
|
||||
const navComponentId = `${screen._id}-navigation`
|
||||
if (componentId === screenComponentId) {
|
||||
return null
|
||||
}
|
||||
if (componentId === navComponentId) {
|
||||
return screenComponentId
|
||||
}
|
||||
if (parent._id === screen.props._id && index === 0) {
|
||||
return navComponentId
|
||||
}
|
||||
|
||||
// If we have siblings above us, choose the sibling or a descendant
|
||||
if (index > 0) {
|
||||
|
@ -1021,12 +1032,20 @@ export const getFrontendStore = () => {
|
|||
return parent._id
|
||||
},
|
||||
getNext: () => {
|
||||
const state = get(store)
|
||||
const component = get(selectedComponent)
|
||||
const componentId = component?._id
|
||||
const screen = get(selectedScreen)
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||
|
||||
// Check for screen and navigation component edge cases
|
||||
const screenComponentId = `${screen._id}-screen`
|
||||
const navComponentId = `${screen._id}-navigation`
|
||||
if (state.selectedComponentId === screenComponentId) {
|
||||
return navComponentId
|
||||
}
|
||||
|
||||
// If we have children, select first child
|
||||
if (component._children?.length) {
|
||||
return component._children[0]._id
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
Select,
|
||||
Toggle,
|
||||
RadioGroup,
|
||||
Icon,
|
||||
DatePicker,
|
||||
Modal,
|
||||
notifications,
|
||||
OptionSelectDnD,
|
||||
Layout,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
|
@ -47,6 +49,7 @@
|
|||
|
||||
export let field
|
||||
|
||||
let mounted = false
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
let originalName
|
||||
let linkEditDisabled
|
||||
|
@ -413,16 +416,22 @@
|
|||
}
|
||||
return newError
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<Input
|
||||
bind:value={editableColumn.name}
|
||||
disabled={uneditable ||
|
||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||
error={errors?.name}
|
||||
/>
|
||||
|
||||
{#if mounted}
|
||||
<Input
|
||||
autofocus
|
||||
bind:value={editableColumn.name}
|
||||
disabled={uneditable ||
|
||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||
error={errors?.name}
|
||||
/>
|
||||
{/if}
|
||||
<Select
|
||||
disabled={!typeEnabled}
|
||||
bind:value={editableColumn.type}
|
||||
|
@ -452,12 +461,17 @@
|
|||
/>
|
||||
{:else if editableColumn.type === "longform"}
|
||||
<div>
|
||||
<Label
|
||||
size="M"
|
||||
tooltip="Rich text includes support for images, links, tables, lists and more"
|
||||
>
|
||||
Formatting
|
||||
</Label>
|
||||
<div class="tooltip-alignment">
|
||||
<Label size="M">Formatting</Label>
|
||||
<AbsTooltip
|
||||
position="top"
|
||||
type="info"
|
||||
text={"Rich text includes support for images, link"}
|
||||
>
|
||||
<Icon size="XS" name="InfoOutline" />
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
bind:value={editableColumn.useRichText}
|
||||
text="Enable rich text support (markdown)"
|
||||
|
@ -488,13 +502,18 @@
|
|||
</div>
|
||||
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
|
||||
<div>
|
||||
<Label
|
||||
tooltip={isCreating
|
||||
? null
|
||||
: "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"}
|
||||
>
|
||||
Time zones
|
||||
</Label>
|
||||
<div>
|
||||
<Label>Time zones</Label>
|
||||
<AbsTooltip
|
||||
position="top"
|
||||
type="info"
|
||||
text={isCreating
|
||||
? null
|
||||
: "We recommend not changing how timezones are handled for existing columns, as existing data will not be updated"}
|
||||
>
|
||||
<Icon size="XS" name="InfoOutline" />
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
<Toggle
|
||||
bind:value={editableColumn.ignoreTimezones}
|
||||
text="Ignore time zones"
|
||||
|
@ -671,6 +690,12 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.tooltip-alignment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.label-length {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
|
|
|
@ -121,7 +121,9 @@
|
|||
type: "Screen",
|
||||
name: screen.routing.route,
|
||||
icon: "WebPage",
|
||||
action: () => $goto(`./design/${screen._id}/components`),
|
||||
action: () => {
|
||||
$goto(`./design/${screen._id}/${screen._id}-screen`)
|
||||
},
|
||||
})),
|
||||
...($automationStore?.automations?.map(automation => ({
|
||||
type: "Automation",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let id
|
||||
export let showTooltip = false
|
||||
export let selectedBy = null
|
||||
export let compact = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -80,8 +81,9 @@
|
|||
{#if withArrow}
|
||||
<div
|
||||
class:opened
|
||||
class:relative={indentLevel === 0}
|
||||
class:absolute={indentLevel > 0}
|
||||
class:relative={indentLevel === 0 && !compact}
|
||||
class:absolute={indentLevel > 0 && !compact}
|
||||
class:compact
|
||||
class="icon arrow"
|
||||
on:click={onIconClick}
|
||||
>
|
||||
|
@ -194,10 +196,21 @@
|
|||
padding: 8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.compact {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
padding: 8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.icon.arrow :global(svg) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.icon.arrow.compact :global(svg) {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
.icon.arrow.relative {
|
||||
position: relative;
|
||||
margin: 0 -6px 0 -4px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Icon, Heading } from "@budibase/bbui"
|
||||
import { Icon, Body } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let icon
|
||||
|
@ -25,7 +25,7 @@
|
|||
<Icon name={icon} />
|
||||
{/if}
|
||||
<div class="title">
|
||||
<Heading size="XXS">{title || ""}</Heading>
|
||||
<Body size="S">{title}</Body>
|
||||
</div>
|
||||
{#if showAddButton}
|
||||
<div class="add-button" on:click={onClickAddButton}>
|
||||
|
@ -78,15 +78,14 @@
|
|||
align-items: center;
|
||||
padding: 0 var(--spacing-l);
|
||||
border-bottom: var(--border-light);
|
||||
gap: var(--spacing-l);
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.title {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.title :global(h1) {
|
||||
.title :global(p) {
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import { Select, Label, Stepper } from "@budibase/bbui"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||
import { onMount } from "svelte"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset,
|
||||
|
@ -51,7 +53,11 @@
|
|||
<Select bind:value={parameters.type} options={typeOptions} />
|
||||
{#if parameters.type === "specific"}
|
||||
<Label small>Number</Label>
|
||||
<Stepper bind:value={parameters.number} />
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
value={parameters.number}
|
||||
on:change={e => (parameters.number = e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
<ColorPicker
|
||||
value={column.background}
|
||||
on:change={e => (column.background = e.detail)}
|
||||
alignRight
|
||||
spectrumTheme={$store.theme}
|
||||
/>
|
||||
</Layout>
|
||||
|
@ -51,7 +50,6 @@
|
|||
<ColorPicker
|
||||
value={column.color}
|
||||
on:change={e => (column.color = e.detail)}
|
||||
alignRight
|
||||
spectrumTheme={$store.theme}
|
||||
/>
|
||||
</Layout>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import { generate } from "shortid"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
|
@ -35,22 +35,28 @@
|
|||
{ value: "and", label: "Match all filters" },
|
||||
{ value: "or", label: "Match any filter" },
|
||||
]
|
||||
const onEmptyOptions = [
|
||||
{ value: "all", label: "Return all table rows" },
|
||||
{ value: "none", label: "Return no rows" },
|
||||
]
|
||||
|
||||
let rawFilters
|
||||
let matchAny = false
|
||||
let onEmptyFilter = "all"
|
||||
|
||||
$: parseFilters(filters)
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny))
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
// Remove field key prefixes and determine whether to use the "match all"
|
||||
// or "match any" behaviour
|
||||
// Remove field key prefixes and determine which behaviours to use
|
||||
const parseFilters = filters => {
|
||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||
onEmptyFilter =
|
||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||
rawFilters = (filters || [])
|
||||
.filter(filter => filter.operator !== "allOr")
|
||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
||||
.map(filter => {
|
||||
const { field } = filter
|
||||
let newFilter = { ...filter }
|
||||
|
@ -64,9 +70,18 @@
|
|||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
parseFilters(filters)
|
||||
rawFilters.forEach(filter => {
|
||||
filter.type =
|
||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||
filter.type
|
||||
})
|
||||
})
|
||||
|
||||
// Add field key prefixes and a special metadata filter object to indicate
|
||||
// whether to use the "match all" or "match any" behaviour
|
||||
const enrichFilters = (rawFilters, matchAny) => {
|
||||
// how to handle filter behaviour
|
||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
||||
let count = 1
|
||||
return rawFilters
|
||||
.filter(filter => filter.field)
|
||||
|
@ -75,6 +90,7 @@
|
|||
field: `${count++}:${filter.field}`,
|
||||
}))
|
||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
||||
.concat([{ onEmptyFilter }])
|
||||
}
|
||||
|
||||
const addFilter = () => {
|
||||
|
@ -186,6 +202,17 @@
|
|||
on:change={e => (matchAny = e.detail === "or")}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if datasource?.type === "table"}
|
||||
<Select
|
||||
label="When filter empty"
|
||||
value={onEmptyFilter}
|
||||
options={onEmptyOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (onEmptyFilter = e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="filter-label">
|
||||
|
|
|
@ -56,6 +56,11 @@ export const syncURLToState = options => {
|
|||
|
||||
// Navigate to a certain URL
|
||||
const gotoUrl = (url, params) => {
|
||||
// Clean URL
|
||||
if (url?.endsWith("/index")) {
|
||||
url = url.replace("/index", "")
|
||||
}
|
||||
// Allow custom URL handling
|
||||
if (beforeNavigate) {
|
||||
const res = beforeNavigate(url, params)
|
||||
if (res?.url) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Button, Drawer } from "@budibase/bbui"
|
||||
import NavigationLinksDrawer from "./NavigationLinksDrawer.svelte"
|
||||
import NavigationLinksDrawer from "./LinksDrawer.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { store } from "builderStore"
|
||||
|
||||
|
@ -20,12 +20,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Button cta on:click={openDrawer}>Configure links</Button>
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title={"Navigation Links"}
|
||||
width="calc(100% - 334px)"
|
||||
>
|
||||
<Button cta on:click={openDrawer}>Configure Links</Button>
|
||||
<Drawer bind:this={drawer} title={"Navigation Links"}>
|
||||
<svelte:fragment slot="description">
|
||||
Configure the links in your navigation bar.
|
||||
</svelte:fragment>
|
|
@ -0,0 +1,225 @@
|
|||
<script>
|
||||
import LinksEditor from "./LinksEditor.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import {
|
||||
Detail,
|
||||
Toggle,
|
||||
Body,
|
||||
Icon,
|
||||
ColorPicker,
|
||||
Input,
|
||||
Label,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
Checkbox,
|
||||
notifications,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
|
||||
const updateShowNavigation = async e => {
|
||||
await store.actions.screens.updateSetting(
|
||||
get(selectedScreen),
|
||||
"showNavigation",
|
||||
e.detail
|
||||
)
|
||||
}
|
||||
|
||||
const update = async (key, value) => {
|
||||
try {
|
||||
let navigation = $store.navigation
|
||||
navigation[key] = value
|
||||
await store.actions.navigation.save(navigation)
|
||||
} catch (error) {
|
||||
notifications.error("Error updating navigation settings")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
title="Navigation"
|
||||
icon={$selectedScreen.showNavigation ? "Visibility" : "VisibilityOff"}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<div class="generalSection">
|
||||
<div class="subheading">
|
||||
<Detail>General</Detail>
|
||||
</div>
|
||||
<div class="toggle">
|
||||
<Toggle
|
||||
on:change={updateShowNavigation}
|
||||
value={$selectedScreen.showNavigation}
|
||||
/>
|
||||
<Body size="S">Show nav on this screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $selectedScreen.showNavigation}
|
||||
<div class="divider" />
|
||||
<div class="customizeSection">
|
||||
<div class="subheading">
|
||||
<Detail>Customize</Detail>
|
||||
</div>
|
||||
<div class="info">
|
||||
<Icon name="InfoOutline" size="S" />
|
||||
<Body size="S">These settings apply to all screens</Body>
|
||||
</div>
|
||||
<div class="configureLinks">
|
||||
<LinksEditor />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="label">
|
||||
<Label size="M">Position</Label>
|
||||
</div>
|
||||
<ActionGroup quiet>
|
||||
<ActionButton
|
||||
selected={$store.navigation.navigation === "Top"}
|
||||
quiet={$store.navigation.navigation !== "Top"}
|
||||
icon="PaddingTop"
|
||||
on:click={() => update("navigation", "Top")}
|
||||
/>
|
||||
<ActionButton
|
||||
selected={$store.navigation.navigation === "Left"}
|
||||
quiet={$store.navigation.navigation !== "Left"}
|
||||
icon="PaddingLeft"
|
||||
on:click={() => update("navigation", "Left")}
|
||||
/>
|
||||
</ActionGroup>
|
||||
|
||||
{#if $store.navigation.navigation === "Top"}
|
||||
<div class="label">
|
||||
<Label size="M">Sticky header</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={$store.navigation.sticky}
|
||||
on:change={e => update("sticky", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label size="M">Width</Label>
|
||||
</div>
|
||||
<Select
|
||||
options={["Max", "Large", "Medium", "Small"]}
|
||||
plaveholder={null}
|
||||
value={$store.navigation.navWidth}
|
||||
on:change={e => update("navWidth", e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show logo</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideLogo}
|
||||
on:change={e => update("hideLogo", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideLogo}
|
||||
<div class="label">
|
||||
<Label size="M">Logo URL</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.logoUrl}
|
||||
on:change={e => update("logoUrl", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label size="M">Show title</Label>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={!$store.navigation.hideTitle}
|
||||
on:change={e => update("hideTitle", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideTitle}
|
||||
<div class="label">
|
||||
<Label size="M">Title</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={$store.navigation.title}
|
||||
on:change={e => update("title", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label>Background</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navBackground ||
|
||||
DefaultAppTheme.navBackground}
|
||||
on:change={e => update("navBackground", e.detail)}
|
||||
/>
|
||||
<div class="label">
|
||||
<Label>Text</Label>
|
||||
</div>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||
on:change={e => update("navTextColor", e.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
.generalSection {
|
||||
padding: 13px 13px 25px;
|
||||
}
|
||||
|
||||
.customizeSection {
|
||||
padding: 13px 13px 25px;
|
||||
}
|
||||
.subheading {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subheading :global(p) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--grey-3);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 90px 1fr;
|
||||
align-items: start;
|
||||
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||
border-left: 4px solid transparent;
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: 0 var(--spacing-xl) 0 calc(var(--spacing-xl) - 4px);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--background-alt);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.info :global(svg) {
|
||||
margin-right: 5px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.configureLinks :global(button) {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,12 +1,8 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import {
|
||||
Input,
|
||||
Layout,
|
||||
Button,
|
||||
Toggle,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Select,
|
||||
|
@ -16,7 +12,6 @@
|
|||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ButtonActionEditor from "components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
|
||||
|
@ -119,15 +114,6 @@
|
|||
label: "On screen load",
|
||||
control: ButtonActionEditor,
|
||||
},
|
||||
{
|
||||
key: "showNavigation",
|
||||
label: "Navigation",
|
||||
control: Toggle,
|
||||
props: {
|
||||
text: "Show nav",
|
||||
disabled: !!$selectedScreen.layoutId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "width",
|
||||
label: "Width",
|
||||
|
@ -145,36 +131,24 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
title={$selectedScreen.routing.route}
|
||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||
{#if $selectedScreen.layoutId}
|
||||
<Banner
|
||||
type="warning"
|
||||
extraButtonText="Detach custom layout"
|
||||
extraButtonAction={removeCustomLayout}
|
||||
showCloseButton={false}
|
||||
>
|
||||
This screen uses a custom layout, which is deprecated
|
||||
</Banner>
|
||||
{/if}
|
||||
{#each screenSettings as setting (setting.key)}
|
||||
<PropertyControl
|
||||
control={setting.control}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={Helpers.deepGet($selectedScreen, setting.key)}
|
||||
onChange={val => setScreenSetting(setting, val)}
|
||||
props={{ ...setting.props, error: errors[setting.key] }}
|
||||
{bindings}
|
||||
/>
|
||||
{/each}
|
||||
<Button secondary on:click={() => $goto("../components")}>
|
||||
View components
|
||||
</Button>
|
||||
</Layout>
|
||||
</Panel>
|
||||
{#if $selectedScreen.layoutId}
|
||||
<Banner
|
||||
type="warning"
|
||||
extraButtonText="Detach custom layout"
|
||||
extraButtonAction={removeCustomLayout}
|
||||
showCloseButton={false}
|
||||
>
|
||||
This screen uses a custom layout, which is deprecated
|
||||
</Banner>
|
||||
{/if}
|
||||
{#each screenSettings as setting (setting.key)}
|
||||
<PropertyControl
|
||||
control={setting.control}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={Helpers.deepGet($selectedScreen, setting.key)}
|
||||
onChange={val => setScreenSetting(setting, val)}
|
||||
props={{ ...setting.props, error: errors[setting.key] }}
|
||||
{bindings}
|
||||
/>
|
||||
{/each}
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Label,
|
||||
ColorPicker,
|
||||
notifications,
|
||||
Icon,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||
|
||||
$: customTheme = $store.customTheme || {}
|
||||
|
||||
const update = async (property, value) => {
|
||||
try {
|
||||
store.actions.customTheme.save({
|
||||
...get(store).customTheme,
|
||||
[property]: value,
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating custom theme")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="info">
|
||||
<Icon name="InfoOutline" size="S" />
|
||||
<Body size="S">These settings apply to all screens</Body>
|
||||
</div>
|
||||
<Layout noPadding gap="S">
|
||||
<Layout noPadding gap="XS">
|
||||
<AppThemeSelect />
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Button roundness</Label>
|
||||
<ButtonRoundnessSelect
|
||||
{customTheme}
|
||||
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
<PropertyControl
|
||||
label="Accent color"
|
||||
control={ColorPicker}
|
||||
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
|
||||
onChange={val => update("primaryColor", val)}
|
||||
props={{
|
||||
spectrumTheme: $store.theme,
|
||||
}}
|
||||
/>
|
||||
<PropertyControl
|
||||
label="Hover"
|
||||
control={ColorPicker}
|
||||
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
|
||||
onChange={val => update("primaryColorHover", val)}
|
||||
props={{
|
||||
spectrumTheme: $store.theme,
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.info {
|
||||
background-color: var(--background-alt);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
.info :global(svg) {
|
||||
margin-right: 5px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import GeneralPanel from "./GeneralPanel.svelte"
|
||||
import ThemePanel from "./ThemePanel.svelte"
|
||||
import { selectedScreen } from "builderStore"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { ActionButton, Layout } from "@budibase/bbui"
|
||||
|
||||
let activeTab = "general"
|
||||
const tabs = ["general", "theme"]
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
title={$selectedScreen.routing.route}
|
||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<div slot="panel-header-content">
|
||||
<div class="settings-tabs">
|
||||
{#each tabs as tab}
|
||||
<ActionButton
|
||||
size="M"
|
||||
quiet
|
||||
selected={activeTab === tab}
|
||||
on:click={() => {
|
||||
activeTab = tab
|
||||
}}
|
||||
>
|
||||
{capitalise(tab)}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||
{#if activeTab === "theme"}
|
||||
<ThemePanel />
|
||||
{:else}
|
||||
<GeneralPanel />
|
||||
{/if}
|
||||
</Layout>
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
.settings-tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
padding: 0 var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
import ComponentSettingsPanel from "./_components/Component/ComponentSettingsPanel.svelte"
|
||||
import NavigationPanel from "./_components/Navigation/index.svelte"
|
||||
import ScreenSettingsPanel from "./_components/Screen/index.svelte"
|
||||
|
||||
$: componentId = $store.selectedComponentId
|
||||
$: store.actions.websocket.selectResource(componentId)
|
||||
$: params = routify.params
|
||||
$: routeComponentId = $params.componentId
|
||||
|
||||
// Hide new component panel whenever component ID changes
|
||||
const closeNewComponentPanel = url => {
|
||||
if (url?.endsWith("/new")) {
|
||||
url = url.replace("/new", "")
|
||||
}
|
||||
return { url }
|
||||
}
|
||||
|
||||
const validate = id => {
|
||||
if (id === `${$store.selectedScreenId}-screen`) return true
|
||||
if (id === `${$store.selectedScreenId}-navigation`) return true
|
||||
|
||||
return !!findComponent($selectedScreen.props, id)
|
||||
}
|
||||
|
||||
// Keep URL and state in sync for selected component ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "componentId",
|
||||
stateKey: "selectedComponentId",
|
||||
validate,
|
||||
fallbackUrl: "../",
|
||||
store,
|
||||
routify,
|
||||
beforeNavigate: closeNewComponentPanel,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
{#if routeComponentId === `${$store.selectedScreenId}-screen`}
|
||||
<ScreenSettingsPanel />
|
||||
{:else if routeComponentId === `${$store.selectedScreenId}-navigation`}
|
||||
<NavigationPanel />
|
||||
{:else}
|
||||
<ComponentSettingsPanel />
|
||||
{/if}
|
||||
<slot />
|
|
@ -0,0 +1 @@
|
|||
<!-- Required to make Routify happy -->
|
|
@ -31,6 +31,10 @@
|
|||
$: orderMap = createComponentOrderMap(componentList)
|
||||
|
||||
const getAllowedComponents = (allComponents, screen, component) => {
|
||||
// Default to using the root screen container if no component specified
|
||||
if (!component) {
|
||||
component = screen.props
|
||||
}
|
||||
const path = findComponentPath(screen?.props, component?._id)
|
||||
if (!path?.length) {
|
||||
return []
|
|
@ -1,32 +1,16 @@
|
|||
<script>
|
||||
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
||||
import AppPreview from "./AppPreview.svelte"
|
||||
import { store, sortedScreens, screenHistoryStore } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { store, screenHistoryStore } from "builderStore"
|
||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||
import { isActive } from "@roxi/routify"
|
||||
</script>
|
||||
|
||||
<div class="app-panel">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<Select
|
||||
placeholder={null}
|
||||
options={$sortedScreens}
|
||||
getOptionLabel={x => x.routing.route}
|
||||
getOptionValue={x => x._id}
|
||||
getOptionColour={x => RoleUtils.getRoleColour(x.routing.roleId)}
|
||||
value={$store.selectedScreenId}
|
||||
on:change={e => store.actions.screens.select(e.detail)}
|
||||
quiet
|
||||
autoWidth
|
||||
/>
|
||||
<UndoRedoControl store={screenHistoryStore} />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if $isActive("./screens") || $isActive("./components")}
|
||||
<UndoRedoControl store={screenHistoryStore} />
|
||||
{/if}
|
||||
{#if $store.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{/if}
|
||||
|
@ -47,37 +31,24 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
padding: 9px var(--spacing-m);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-l);
|
||||
margin: 0 2px;
|
||||
z-index: 1;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.header-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
.header-left,
|
||||
.header-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.header-left {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.header-left :global(> *) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.header-left :global(.spectrum-Picker) {
|
||||
font-weight: 600;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import {
|
||||
store,
|
||||
selectedComponent,
|
||||
selectedScreen,
|
||||
selectedLayout,
|
||||
currentAsset,
|
||||
} from "builderStore"
|
||||
import { store, selectedScreen, currentAsset } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
ProgressCircle,
|
||||
|
@ -20,12 +14,10 @@
|
|||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "builderStore/componentUtils"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
let screen
|
||||
let selectedComponentId
|
||||
let confirmDeleteDialog
|
||||
let idToDelete
|
||||
let loading = true
|
||||
|
@ -39,36 +31,11 @@
|
|||
BUDIBASE: "type",
|
||||
}
|
||||
|
||||
const placeholderScreen = new Screen()
|
||||
.name("Screen Placeholder")
|
||||
.route("/")
|
||||
.component("@budibase/standard-components/screenslot")
|
||||
.instanceName("Content Placeholder")
|
||||
.normalStyle({ flex: "1 1 auto" })
|
||||
.json()
|
||||
|
||||
// Extract data to pass to the iframe
|
||||
$: {
|
||||
// If viewing legacy layouts, always show the custom layout
|
||||
if ($isActive("./layouts")) {
|
||||
screen = placeholderScreen
|
||||
layout = $selectedLayout
|
||||
} else {
|
||||
screen = $selectedScreen
|
||||
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
|
||||
}
|
||||
}
|
||||
$: screen = $selectedScreen
|
||||
|
||||
// Determine selected component ID
|
||||
$: {
|
||||
if ($isActive("./components")) {
|
||||
selectedComponentId = $store.selectedComponentId
|
||||
} else if ($isActive("./navigation")) {
|
||||
selectedComponentId = "navigation"
|
||||
} else {
|
||||
selectedComponentId = null
|
||||
}
|
||||
}
|
||||
$: selectedComponentId = $store.selectedComponentId
|
||||
|
||||
$: previewData = {
|
||||
appId: $store.appId,
|
||||
|
@ -98,9 +65,7 @@
|
|||
$: refreshContent(json)
|
||||
|
||||
// Determine if the add component menu is active
|
||||
$: isAddingComponent = $isActive(
|
||||
`./components/${$selectedComponent?._id}/new`
|
||||
)
|
||||
$: isAddingComponent = $isActive(`./${selectedComponentId}/new`)
|
||||
|
||||
// Register handler to send custom to the preview
|
||||
$: sendPreviewEvent = (name, payload) => {
|
||||
|
@ -152,9 +117,6 @@
|
|||
error = event.error || "An unknown error occurred"
|
||||
} else if (type === "select-component" && data.id) {
|
||||
$store.selectedComponentId = data.id
|
||||
if (!$isActive("./components")) {
|
||||
$goto("./components")
|
||||
}
|
||||
} else if (type === "update-prop") {
|
||||
await store.actions.components.updateSetting(data.prop, data.value)
|
||||
} else if (type === "update-styles") {
|
||||
|
@ -194,10 +156,6 @@
|
|||
store.actions.components.copy(source, true, false)
|
||||
await store.actions.components.paste(destination, data.mode)
|
||||
}
|
||||
} else if (type === "click-nav") {
|
||||
if (!$isActive("./navigation")) {
|
||||
$goto("./navigation")
|
||||
}
|
||||
} else if (type === "request-add-component") {
|
||||
toggleAddComponent()
|
||||
} else if (type === "highlight-setting") {
|
||||
|
@ -247,11 +205,10 @@
|
|||
}
|
||||
|
||||
const toggleAddComponent = () => {
|
||||
if (isAddingComponent) {
|
||||
$goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`)
|
||||
if ($isActive(`./:componentId/new`)) {
|
||||
$goto(`./:componentId`)
|
||||
} else {
|
||||
const id = $selectedComponent?._id || $selectedScreen?.props?._id
|
||||
$goto(`../${$selectedScreen._id}/components/${id}/new`)
|
||||
$goto(`./:componentId/new`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
|
@ -37,7 +36,7 @@
|
|||
confirmEjectDialog.show()
|
||||
},
|
||||
["Ctrl+Enter"]: () => {
|
||||
$goto("./new")
|
||||
$goto(`./:componentId/new`)
|
||||
},
|
||||
["Delete"]: component => {
|
||||
// Don't show confirmation for the screen itself
|
||||
|
@ -54,8 +53,8 @@
|
|||
store.actions.components.selectNext()
|
||||
},
|
||||
["Escape"]: () => {
|
||||
if ($isActive("./new")) {
|
||||
$goto("./")
|
||||
if ($isActive(`./:componentId/new`)) {
|
||||
$goto(`./${$store.selectedComponentId}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -85,10 +84,13 @@
|
|||
const handler = keyHandlers[key]
|
||||
if (!handler) {
|
||||
return false
|
||||
} else if (event) {
|
||||
}
|
||||
|
||||
if (event && key !== "Escape") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
return await handler(component)
|
||||
} catch (error) {
|
||||
notifications.error(error || "Error handling key press")
|
||||
|
@ -101,7 +103,13 @@
|
|||
return
|
||||
}
|
||||
// Ignore events when typing
|
||||
if (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
|
||||
}
|
||||
// Key events are always for the selected component
|
|
@ -9,14 +9,14 @@
|
|||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
const sidebarWidth = 259
|
||||
const sidebarWidth = 310
|
||||
const navItemHeight = 32
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||
let newOffsets = {}
|
||||
|
||||
// Calculate left offset
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft - 36
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft + 16
|
||||
if (offsetX > sidebarWidth) {
|
||||
newOffsets.left = offsetX - sidebarWidth
|
||||
} else {
|
||||
|
@ -64,6 +64,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
on:scroll
|
||||
bind:this={scrollRef}
|
||||
on:drop={onDrop}
|
||||
ondragover="return false"
|
||||
|
@ -74,7 +75,6 @@
|
|||
|
||||
<style>
|
||||
div {
|
||||
padding: var(--spacing-xl) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
|
@ -107,6 +107,7 @@
|
|||
id={`component-${component._id}`}
|
||||
>
|
||||
<NavItem
|
||||
compact
|
||||
scrollable
|
||||
draggable
|
||||
on:dragend={dndStore.actions.reset}
|
||||
|
@ -117,7 +118,7 @@
|
|||
text={getComponentText(component)}
|
||||
icon={getComponentIcon(component)}
|
||||
withArrow={componentHasChildren(component)}
|
||||
indentLevel={level + 1}
|
||||
indentLevel={level}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
{opened}
|
||||
highlighted={isChildOfSelectedComponent(component)}
|
|
@ -0,0 +1,163 @@
|
|||
<script>
|
||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore } from "./dndStore.js"
|
||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { DropPosition } from "./dndStore"
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
|
||||
let scrolling = false
|
||||
|
||||
const toNewComponentRoute = () => {
|
||||
if ($isActive(`./:componentId/new`)) {
|
||||
$goto(`./:componentId`)
|
||||
} else {
|
||||
$goto(`./:componentId/new`)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
await dndStore.actions.drop()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="components">
|
||||
<div class="header" class:scrolling>
|
||||
<Body size="S">Components</Body>
|
||||
<div on:click={toNewComponentRoute} class="addButton">
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-panel">
|
||||
<ComponentScrollWrapper on:scroll={handleScroll}>
|
||||
<ul>
|
||||
<li>
|
||||
<NavItem
|
||||
text="Screen"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId ===
|
||||
`${$store.selectedScreenId}-screen`}
|
||||
opened
|
||||
scrollable
|
||||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
|
||||
}}
|
||||
id={`component-screen`}
|
||||
selectedBy={$userSelectedResourceMap[
|
||||
`${$store.selectedScreenId}-screen`
|
||||
]}
|
||||
>
|
||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||
</NavItem>
|
||||
<NavItem
|
||||
text="Navigation"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId ===
|
||||
`${$store.selectedScreenId}-navigation`}
|
||||
opened
|
||||
scrollable
|
||||
icon={$selectedScreen.showNavigation
|
||||
? "Visibility"
|
||||
: "VisibilityOff"}
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
|
||||
}}
|
||||
id={`component-nav`}
|
||||
selectedBy={$userSelectedResourceMap[
|
||||
`${$store.selectedScreenId}-navigation`
|
||||
]}
|
||||
/>
|
||||
<ComponentTree
|
||||
level={0}
|
||||
components={$selectedScreen?.props._children}
|
||||
/>
|
||||
|
||||
<!-- Show drop indicators for the target and the parent -->
|
||||
{#if $dndStore.dragging && $dndStore.valid}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.target}
|
||||
position={$dndStore.dropPosition}
|
||||
/>
|
||||
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.targetParent}
|
||||
position={DropPosition.INSIDE}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</ComponentScrollWrapper>
|
||||
</div>
|
||||
<ComponentKeyHandler />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.components {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-bottom 130ms ease-out;
|
||||
}
|
||||
.header.scrolling {
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
|
||||
.components :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-left: auto;
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.addButton:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.list-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import ScreenList from "./ScreenList/index.svelte"
|
||||
import ComponentList from "./ComponentList/index.svelte"
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<ScreenList />
|
||||
<ComponentList />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
border-right: var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
|
@ -56,7 +56,7 @@
|
|||
const deleteScreen = async () => {
|
||||
try {
|
||||
await store.actions.screens.delete(screen)
|
||||
notifications.success("Deleted screen successfully.")
|
||||
notifications.success("Deleted screen successfully")
|
||||
} catch (err) {
|
||||
notifications.error("Error deleting screen")
|
||||
}
|
|
@ -26,7 +26,7 @@
|
|||
<StatusLight square {color} />
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping text={tooltip} direction="left" />
|
||||
<Tooltip textWrapping text={tooltip} direction="right" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -38,13 +38,11 @@
|
|||
.tooltip {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 8px);
|
||||
transform: translateX(-100%) translateY(-50%);
|
||||
bottom: -5px;
|
||||
left: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 200px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip :global(.spectrum-Tooltip) {
|
|
@ -0,0 +1,304 @@
|
|||
<script>
|
||||
import { Icon, Layout, Body } from "@budibase/bbui"
|
||||
import {
|
||||
store,
|
||||
sortedScreens,
|
||||
userSelectedResourceMap,
|
||||
screensHeight,
|
||||
} from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import DropdownMenu from "./DropdownMenu.svelte"
|
||||
import { onMount, tick } from "svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let search = false
|
||||
let resizing = false
|
||||
let searchValue = ""
|
||||
let searchInput
|
||||
let container
|
||||
let screensContainer
|
||||
let scrolling = false
|
||||
let previousHeight = null
|
||||
let dragOffset
|
||||
|
||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const openSearch = async () => {
|
||||
search = true
|
||||
await tick()
|
||||
searchInput.focus()
|
||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||
previousHeight = $screensHeight
|
||||
$screensHeight = "calc(100% + 1px)"
|
||||
}
|
||||
|
||||
const closeSearch = async () => {
|
||||
if (previousHeight) {
|
||||
// Restore previous height and wait for animation
|
||||
$screensHeight = previousHeight
|
||||
previousHeight = null
|
||||
await sleep(300)
|
||||
}
|
||||
search = false
|
||||
searchValue = ""
|
||||
}
|
||||
|
||||
const getFilteredScreens = (screens, search) => {
|
||||
return screens.filter(screen => {
|
||||
return !search || screen.routing.route.includes(search)
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddButton = () => {
|
||||
if (search) {
|
||||
closeSearch()
|
||||
} else {
|
||||
$goto("../new")
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (e.key === "Escape") {
|
||||
closeSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const startResizing = e => {
|
||||
// Reset the height store to match the true height
|
||||
$screensHeight = `${container.getBoundingClientRect().height}px`
|
||||
|
||||
// Store an offset to easily compute new height when moving the mouse
|
||||
dragOffset = parseInt($screensHeight) - e.clientY
|
||||
|
||||
// Add event listeners
|
||||
resizing = true
|
||||
document.addEventListener("mousemove", resize)
|
||||
document.addEventListener("mouseup", stopResizing)
|
||||
}
|
||||
|
||||
const resize = e => {
|
||||
// Prevent negative heights as this screws with layout
|
||||
const newHeight = Math.max(0, e.clientY + dragOffset)
|
||||
if (newHeight == null || isNaN(newHeight)) {
|
||||
return
|
||||
}
|
||||
$screensHeight = `${newHeight}px`
|
||||
}
|
||||
|
||||
const stopResizing = () => {
|
||||
resizing = false
|
||||
document.removeEventListener("mousemove", resize)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Ensure we aren't stuck at 100% height from leaving while searching
|
||||
if ($screensHeight == null || isNaN(parseInt($screensHeight))) {
|
||||
$screensHeight = "210px"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<div
|
||||
class="screens"
|
||||
class:search
|
||||
class:resizing
|
||||
style={`height:${$screensHeight};`}
|
||||
bind:this={container}
|
||||
>
|
||||
<div class="header" class:scrolling>
|
||||
<input
|
||||
readonly={!search}
|
||||
bind:value={searchValue}
|
||||
bind:this={searchInput}
|
||||
class="input"
|
||||
placeholder="Search for screens"
|
||||
/>
|
||||
<div class="title" class:hide={search}>
|
||||
<Body size="S">Screens</Body>
|
||||
</div>
|
||||
<div on:click={openSearch} class="searchButton" class:hide={search}>
|
||||
<Icon size="S" name="Search" />
|
||||
</div>
|
||||
<div
|
||||
on:click={handleAddButton}
|
||||
class="addButton"
|
||||
class:closeButton={search}
|
||||
>
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
|
||||
{#if filteredScreens?.length}
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<NavItem
|
||||
icon={screen.routing.homeScreen ? "Home" : null}
|
||||
indentLevel={0}
|
||||
selected={$store.selectedScreenId === screen._id}
|
||||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||
>
|
||||
<DropdownMenu screenId={screen._id} />
|
||||
<div slot="icon" class="icon">
|
||||
<RoleIndicator roleId={screen.routing.roleId} />
|
||||
</div>
|
||||
</NavItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<Layout paddingY="none" paddingX="L">
|
||||
<div class="no-results">
|
||||
There aren't any screens matching that route
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="divider"
|
||||
on:mousedown={startResizing}
|
||||
on:dblclick={() => screensHeight.set("210px")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screens {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 147px;
|
||||
max-height: calc(100% - 147px);
|
||||
position: relative;
|
||||
}
|
||||
.screens.search {
|
||||
transition: height 300ms ease-out;
|
||||
max-height: none;
|
||||
}
|
||||
.screens.resizing {
|
||||
user-select: none;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-bottom 130ms ease-out;
|
||||
}
|
||||
.header.scrolling {
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
|
||||
.input {
|
||||
font-family: var(--font-sans);
|
||||
position: absolute;
|
||||
color: var(--ink);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: var(--spectrum-alias-font-size-default);
|
||||
width: 260px;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
}
|
||||
.input::placeholder {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.screens.search input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.screens.resizing .content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screens :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
.searchButton:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
transition: transform 300ms ease-out;
|
||||
}
|
||||
.addButton:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transform: translateY(50%);
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.divider:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.divider:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../screens")
|
||||
</script>
|
|
@ -1,14 +1,10 @@
|
|||
<script>
|
||||
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
||||
import * as routify from "@roxi/routify"
|
||||
import AppPanel from "./_components/AppPanel.svelte"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import { onDestroy } from "svelte"
|
||||
const { isActive, goto } = routify
|
||||
|
||||
$: screenId = $store.selectedScreenId
|
||||
$: store.actions.websocket.selectResource(screenId)
|
||||
import LeftPanel from "./_components/LeftPanel.svelte"
|
||||
|
||||
// Keep URL and state in sync for selected screen ID
|
||||
const stopSyncing = syncURLToState({
|
||||
|
@ -23,51 +19,15 @@
|
|||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<div class="design">
|
||||
<div class="icon-nav">
|
||||
<IconSideNav>
|
||||
<IconSideNavItem
|
||||
icon="WebPage"
|
||||
tooltip="Screens"
|
||||
active={$isActive("./screens")}
|
||||
on:click={() => $goto("./screens")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="ViewList"
|
||||
tooltip="Components"
|
||||
active={$isActive("./components")}
|
||||
on:click={() => $goto("./components")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Brush"
|
||||
tooltip="Theme"
|
||||
active={$isActive("./theme")}
|
||||
on:click={() => $goto("./theme")}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Link"
|
||||
tooltip="Navigation"
|
||||
active={$isActive("./navigation")}
|
||||
on:click={() => $goto("./navigation")}
|
||||
/>
|
||||
{#if $store.layouts?.length}
|
||||
<IconSideNavItem
|
||||
icon="Experience"
|
||||
tooltip="Layouts"
|
||||
active={$isActive("./layouts")}
|
||||
on:click={() => $goto("./layouts")}
|
||||
/>
|
||||
{/if}
|
||||
</IconSideNav>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{#if $selectedScreen}
|
||||
<slot />
|
||||
{#if $selectedScreen}
|
||||
<div class="design">
|
||||
<div class="content">
|
||||
<LeftPanel />
|
||||
<AppPanel />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.design {
|
||||
|
@ -78,10 +38,7 @@
|
|||
align-items: stretch;
|
||||
height: 0;
|
||||
}
|
||||
.icon-nav {
|
||||
background: var(--background);
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -89,17 +46,4 @@
|
|||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
/*
|
||||
This is hacky, yes, but it's the only way to prevent routify from
|
||||
remounting the iframe on route changes.
|
||||
*/
|
||||
.content :global(> *:last-child) {
|
||||
order: 1;
|
||||
}
|
||||
.content :global(> *:first-child) {
|
||||
order: 0;
|
||||
}
|
||||
.content :global(> *:nth-child(2)) {
|
||||
order: 2;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore } from "./dndStore.js"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { DropPosition } from "./dndStore"
|
||||
import { notifications, Button } from "@budibase/bbui"
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
await dndStore.actions.drop()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Components" showExpandIcon borderRight>
|
||||
<div class="add-component">
|
||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
||||
</div>
|
||||
<ComponentScrollWrapper>
|
||||
<ul>
|
||||
<li>
|
||||
<NavItem
|
||||
text="Screen"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
||||
opened
|
||||
scrollable
|
||||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = $selectedScreen?.props._id
|
||||
}}
|
||||
id={`component-${$selectedScreen?.props._id}`}
|
||||
selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]}
|
||||
>
|
||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||
</NavItem>
|
||||
<ComponentTree
|
||||
level={0}
|
||||
components={$selectedScreen?.props._children}
|
||||
/>
|
||||
|
||||
<!-- Show drop indicators for the target and the parent -->
|
||||
{#if $dndStore.dragging && $dndStore.valid}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.target}
|
||||
position={$dndStore.dropPosition}
|
||||
/>
|
||||
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||
<DNDPositionIndicator
|
||||
component={$dndStore.targetParent}
|
||||
position={DropPosition.INSIDE}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</ComponentScrollWrapper>
|
||||
</Panel>
|
||||
<ComponentKeyHandler />
|
||||
|
||||
<style>
|
||||
.add-component {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
|
@ -1,41 +0,0 @@
|
|||
<script>
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
||||
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
||||
|
||||
$: componentId = $store.selectedComponentId
|
||||
$: store.actions.websocket.selectResource(componentId)
|
||||
|
||||
const cleanUrl = url => {
|
||||
// Strip trailing slashes
|
||||
if (url?.endsWith("/index")) {
|
||||
url = url.replace("/index", "")
|
||||
}
|
||||
// Hide new component panel whenever component ID changes
|
||||
if (url?.endsWith("/new")) {
|
||||
url = url.replace("/new", "")
|
||||
}
|
||||
return { url }
|
||||
}
|
||||
|
||||
// Keep URL and state in sync for selected component ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "componentId",
|
||||
stateKey: "selectedComponentId",
|
||||
validate: id => !!findComponent($selectedScreen.props, id),
|
||||
fallbackUrl: "../",
|
||||
store,
|
||||
routify,
|
||||
beforeNavigate: cleanUrl,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<ComponentListPanel />
|
||||
<ComponentSettingsPanel />
|
||||
<slot />
|
|
@ -1,4 +0,0 @@
|
|||
<!--
|
||||
Placeholder file so that routify works.
|
||||
No unique content is needed in this index page.
|
||||
-->
|
|
@ -1,18 +0,0 @@
|
|||
<script>
|
||||
import { selectedScreen, selectedComponent } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(() => {
|
||||
if ($selectedComponent) {
|
||||
// Navigate to the selected component if one exists
|
||||
$redirect(`./${$selectedComponent._id}`)
|
||||
} else if ($selectedScreen) {
|
||||
// Otherwise the screen slot if a screen exists
|
||||
$redirect(`./${$selectedScreen.props._id}`)
|
||||
} else {
|
||||
// Otherwise go up so we can select a new valid screen
|
||||
$redirect("../")
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$redirect("./screens")
|
||||
$redirect(`./${$store.selectedScreenId}-screen`)
|
||||
</script>
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||
|
||||
export let layout
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
const deleteLayout = async () => {
|
||||
try {
|
||||
await store.actions.layouts.delete(layout)
|
||||
notifications.success("Layout deleted successfully")
|
||||
} catch (err) {
|
||||
notifications.error(err?.message || "Error deleting layout")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this layout?"}
|
||||
okText="Delete layout"
|
||||
onOk={deleteLayout}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,29 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { store } from "builderStore"
|
||||
import LayoutDropdownMenu from "./LayoutDropdownMenu.svelte"
|
||||
</script>
|
||||
|
||||
<Panel title="Layouts" borderRight>
|
||||
<div class="layouts">
|
||||
{#each $store.layouts as layout (layout._id)}
|
||||
<NavItem
|
||||
icon="Experience"
|
||||
indentLevel={0}
|
||||
selected={$store.selectedLayoutId === layout._id}
|
||||
text={layout.name}
|
||||
on:click={() => store.actions.layouts.select(layout._id)}
|
||||
>
|
||||
<LayoutDropdownMenu {layout} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
.layouts {
|
||||
margin-top: var(--spacing-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { store, selectedLayout } from "builderStore"
|
||||
import { Layout, Body, Button, Banner, notifications } from "@budibase/bbui"
|
||||
import { Component } from "builderStore/store/screenTemplates/utils/Component"
|
||||
|
||||
const copyLayout = () => {
|
||||
// Build an outer container component to put layout contents inside
|
||||
let container = new Component("@budibase/standard-components/container")
|
||||
.instanceName($selectedLayout.name)
|
||||
.customProps({
|
||||
gap: "M",
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "shrink",
|
||||
})
|
||||
.json()
|
||||
|
||||
// Attach layout components
|
||||
container._children = $selectedLayout.props._children
|
||||
|
||||
// Replace the screenslot component with a container. This is better than
|
||||
// simply removing it as it still shows its position.
|
||||
container = JSON.parse(
|
||||
JSON.stringify(container).replace(
|
||||
"@budibase/standard-components/screenslot",
|
||||
"@budibase/standard-components/container"
|
||||
)
|
||||
)
|
||||
|
||||
// Copy new component structure
|
||||
store.actions.components.copy(container)
|
||||
notifications.success("Components copied successfully")
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Banner type="warning" showCloseButton={false}>
|
||||
Custom layouts are being deprecated. They will be removed in a future
|
||||
release.
|
||||
</Banner>
|
||||
<Body size="S">
|
||||
You can save the content of this layout by pressing the button below.
|
||||
</Body>
|
||||
<Body size="S">
|
||||
This will copy all components inside your layout, which you can then paste
|
||||
into a screen.
|
||||
</Body>
|
||||
<Button cta on:click={copyLayout}>Copy components</Button>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,20 +0,0 @@
|
|||
<script>
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { store } from "builderStore"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
// Keep URL and state in sync for selected component ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "layoutId",
|
||||
stateKey: "selectedLayoutId",
|
||||
validate: id => $store.layouts?.some(layout => layout._id === id),
|
||||
fallbackUrl: "../",
|
||||
store,
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import LayoutListPanel from "./_components/LayoutListPanel.svelte"
|
||||
import LayoutSettingsPanel from "./_components/LayoutSettingsPanel.svelte"
|
||||
</script>
|
||||
|
||||
<LayoutListPanel />
|
||||
<LayoutSettingsPanel />
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$: {
|
||||
if (!$store.layouts?.length) {
|
||||
$redirect("../")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(() => {
|
||||
if ($store.layouts?.length) {
|
||||
$redirect(`./${$store.layouts[0]._id}`)
|
||||
}
|
||||
// The redirection when no layouts exist is handled by the routify layout
|
||||
})
|
||||
</script>
|
|
@ -1,33 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Body, Layout, Banner } from "@budibase/bbui"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const removeCustomLayout = async () => {
|
||||
return store.actions.screens.removeCustomLayout(get(selectedScreen))
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Navigation" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
{#if $selectedScreen.layoutId}
|
||||
<Banner
|
||||
type="warning"
|
||||
extraButtonText="Detach custom layout"
|
||||
extraButtonAction={removeCustomLayout}
|
||||
showCloseButton={false}
|
||||
>
|
||||
You can't preview your navigation settings using this screen as it uses
|
||||
a custom layout, which is deprecated
|
||||
</Banner>
|
||||
{/if}
|
||||
<Body size="S">
|
||||
Your navigation is configured for all the screens within your app.
|
||||
</Body>
|
||||
<Body size="S">
|
||||
You can hide and show your navigation for each screen in the screen
|
||||
settings.
|
||||
</Body>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,110 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import {
|
||||
Layout,
|
||||
Label,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
Checkbox,
|
||||
Select,
|
||||
ColorPicker,
|
||||
Input,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import NavigationLinksEditor from "./NavigationLinksEditor.svelte"
|
||||
import { store } from "builderStore"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
|
||||
const update = async (key, value) => {
|
||||
try {
|
||||
let navigation = $store.navigation
|
||||
navigation[key] = value
|
||||
await store.actions.navigation.save(navigation)
|
||||
} catch (error) {
|
||||
notifications.error("Error updating navigation settings")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Navigation" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<NavigationLinksEditor />
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Position</Label>
|
||||
<ActionGroup quiet>
|
||||
<ActionButton
|
||||
selected={$store.navigation.navigation === "Top"}
|
||||
quiet={$store.navigation.navigation !== "Top"}
|
||||
icon="PaddingTop"
|
||||
on:click={() => update("navigation", "Top")}
|
||||
/>
|
||||
<ActionButton
|
||||
selected={$store.navigation.navigation === "Left"}
|
||||
quiet={$store.navigation.navigation !== "Left"}
|
||||
icon="PaddingLeft"
|
||||
on:click={() => update("navigation", "Left")}
|
||||
/>
|
||||
</ActionGroup>
|
||||
</Layout>
|
||||
{#if $store.navigation.navigation === "Top"}
|
||||
<Checkbox
|
||||
text="Sticky header"
|
||||
value={$store.navigation.sticky}
|
||||
on:change={e => update("sticky", e.detail)}
|
||||
/>
|
||||
<Select
|
||||
label="Width"
|
||||
options={["Max", "Large", "Medium", "Small"]}
|
||||
plaveholder={null}
|
||||
value={$store.navigation.navWidth}
|
||||
on:change={e => update("navWidth", e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
<Layout noPadding gap="XS">
|
||||
<Checkbox
|
||||
text="Logo"
|
||||
value={!$store.navigation.hideLogo}
|
||||
on:change={e => update("hideLogo", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideLogo}
|
||||
<Input
|
||||
value={$store.navigation.logoUrl}
|
||||
on:change={e => update("logoUrl", e.detail)}
|
||||
placeholder="Add logo URL"
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Checkbox
|
||||
text="Title"
|
||||
value={!$store.navigation.hideTitle}
|
||||
on:change={e => update("hideTitle", !e.detail)}
|
||||
/>
|
||||
{#if !$store.navigation.hideTitle}
|
||||
<Input
|
||||
value={$store.navigation.title}
|
||||
on:change={e => update("title", e.detail)}
|
||||
placeholder="Add title"
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Background color</Label>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navBackground || DefaultAppTheme.navBackground}
|
||||
on:change={e => update("navBackground", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Text color</Label>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={$store.navigation.navTextColor || DefaultAppTheme.navTextColor}
|
||||
on:change={e => update("navTextColor", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import NavigationSettingsPanel from "./_components/NavigationSettingsPanel.svelte"
|
||||
import NavigationInfoPanel from "./_components/NavigationInfoPanel.svelte"
|
||||
</script>
|
||||
|
||||
<NavigationSettingsPanel />
|
||||
<NavigationInfoPanel />
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { roles } from "stores/backend"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
let searchString
|
||||
let accessRole = "all"
|
||||
|
||||
$: filteredScreens = getFilteredScreens(
|
||||
$sortedScreens,
|
||||
searchString,
|
||||
accessRole
|
||||
)
|
||||
|
||||
const getFilteredScreens = (screens, search, role) => {
|
||||
return screens.filter(screen => {
|
||||
const searchMatch = !search || screen.routing.route.includes(search)
|
||||
const roleMatch =
|
||||
!role || role === "all" || screen.routing.roleId === role
|
||||
return searchMatch && roleMatch
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Screens" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
||||
<Search
|
||||
placeholder="Search"
|
||||
value={searchString}
|
||||
on:change={e => (searchString = e.detail)}
|
||||
/>
|
||||
<Select
|
||||
bind:value={accessRole}
|
||||
placeholder={null}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => {
|
||||
if (role?._id === "all") {
|
||||
return null
|
||||
}
|
||||
return RoleUtils.getRoleColour(role._id)
|
||||
}}
|
||||
options={[{ name: "All screens", _id: "all" }, ...$roles]}
|
||||
/>
|
||||
</Layout>
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<NavItem
|
||||
icon={screen.routing.homeScreen ? "Home" : null}
|
||||
indentLevel={0}
|
||||
selected={$store.selectedScreenId === screen._id}
|
||||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||
>
|
||||
<ScreenDropdownMenu screenId={screen._id} />
|
||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{#if !filteredScreens?.length}
|
||||
<Layout paddingY="" paddingX="L">
|
||||
<Body size="S">
|
||||
There aren't any screens matching the current filters
|
||||
</Body>
|
||||
</Layout>
|
||||
{/if}
|
||||
</Panel>
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import { selectedScreen } from "builderStore"
|
||||
import ScreenListPanel from "./_components/ScreenListPanel.svelte"
|
||||
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
|
||||
</script>
|
||||
|
||||
<ScreenListPanel />
|
||||
{#if $selectedScreen}
|
||||
{#key $selectedScreen._id}
|
||||
<ScreenSettingsPanel />
|
||||
{/key}
|
||||
{/if}
|
|
@ -1,12 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Body, Layout } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL">
|
||||
<Body size="S">
|
||||
Your theme is set across all the screens within your app.
|
||||
</Body>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,55 +0,0 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { Layout, Label, ColorPicker, notifications } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
import { DefaultAppTheme } from "constants"
|
||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||
|
||||
$: customTheme = $store.customTheme || {}
|
||||
|
||||
const update = async (property, value) => {
|
||||
try {
|
||||
store.actions.customTheme.save({
|
||||
...get(store).customTheme,
|
||||
[property]: value,
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating custom theme")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel title="Theme" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Theme</Label>
|
||||
<AppThemeSelect />
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Button roundness</Label>
|
||||
<ButtonRoundnessSelect
|
||||
{customTheme}
|
||||
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Accent color</Label>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
|
||||
on:change={e => update("primaryColor", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Accent color (hover)</Label>
|
||||
<ColorPicker
|
||||
spectrumTheme={$store.theme}
|
||||
value={customTheme.primaryColorHover ||
|
||||
DefaultAppTheme.primaryColorHover}
|
||||
on:change={e => update("primaryColorHover", e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import ThemeSettingsPanel from "./_components/ThemeSettingsPanel.svelte"
|
||||
import ThemeInfoPanel from "./_components/ThemeInfoPanel.svelte"
|
||||
</script>
|
||||
|
||||
<ThemeSettingsPanel />
|
||||
<ThemeInfoPanel />
|
|
@ -58,16 +58,17 @@
|
|||
const response = await store.actions.screens.save(screen)
|
||||
screenId = response._id
|
||||
|
||||
// Add link in layout for list screens
|
||||
if (screen.props._instanceName.endsWith("List")) {
|
||||
await store.actions.links.save(
|
||||
screen.routing.route,
|
||||
capitalise(screen.routing.route.split("/")[1])
|
||||
)
|
||||
}
|
||||
// Add link in layout. We only ever actually create 1 screen now, even
|
||||
// for autoscreens, so it's always safe to do this.
|
||||
await store.actions.links.save(
|
||||
screen.routing.route,
|
||||
capitalise(screen.routing.route.split("/")[1])
|
||||
)
|
||||
}
|
||||
|
||||
// Go to new screen
|
||||
$goto(`./${screenId}`)
|
||||
store.actions.screens.select(screenId)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
notifications.error("Error creating screens")
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue