1
0
Fork 0
mirror of synced 2024-06-26 18:10:51 +12:00

Merge remote-tracking branch 'origin/develop' into feat/user-groups-tab

This commit is contained in:
Peter Clement 2022-07-14 13:32:51 +01:00
commit 6663fc25fc
302 changed files with 5487 additions and 4131 deletions

View file

@ -7,7 +7,6 @@ on:
branches:
- master
- develop
- new-design-ui
pull_request:
branches:
- master
@ -60,19 +59,3 @@ jobs:
with:
install: false
command: yarn test:e2e:ci
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Upload to S3
if: github.ref == 'refs/heads/new-design-ui'
run: |
tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html
aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/
aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js
aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*"

View file

@ -0,0 +1,62 @@
name: Deploy Budibase Single Container Image to DockerHub
on:
push:
branches:
- "omnibus-action"
- "develop"
- "master"
- "main"
env:
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
BRANCH: ${{ github.event.pull_request.head.ref }}
CI: true
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
REGISTRY_URL: registry.hub.docker.com
jobs:
build:
name: "build"
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- name: Run Yarn
run: yarn
- name: Run Yarn Bootstrap
run: yarn bootstrap
- name: Runt Yarn Lint
run: yarn lint
- name: Run Yarn Build
run: yarn build
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_API_KEY }}
- name: Get the latest release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo $release_version
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Budibase service docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile

View file

@ -16,6 +16,16 @@ on:
- 'package.json'
- 'yarn.lock'
workflow_dispatch:
inputs:
versioning:
type: choice
description: "Versioning type: patch, minor, major"
default: patch
options:
- patch
- minor
- major
required: true
env:
# Posthog token used by ui at build time
@ -58,6 +68,7 @@ jobs:
- name: Publish budibase packages to NPM
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_VERSION_TYPE: ${{ github.event.inputs.version }}
run: |
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
git config --global user.name "Budibase Release Bot"

1
.yarnrc Normal file
View file

@ -0,0 +1 @@
network-timeout 100000

View file

@ -122,6 +122,14 @@ spec:
value: {{ .Values.globals.automationMaxIterations | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
{{ if .Values.globals.bbAdminUserEmail }}
- name: BB_ADMIN_USER_EMAIL
value: { { .Values.globals.bbAdminUserEmail | quote } }
{{ end }}
{{ if .Values.globals.bbAdminUserPassword }}
- name: BB_ADMIN_USER_PASSWORD
value: { { .Values.globals.bbAdminUserPassword | quote } }
{{ end }}
image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always

View file

@ -18,4 +18,8 @@ MINIO_PORT=4004
COUCH_DB_PORT=4005
REDIS_PORT=6379
WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION
BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD=

View file

@ -23,6 +23,8 @@ services:
ENABLE_ANALYTICS: "true"
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
depends_on:
- worker-service
- redis-service

View file

@ -19,3 +19,7 @@ COUCH_DB_PORT=4005
REDIS_PORT=6379
WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD=

View file

@ -34,27 +34,32 @@ ENV \
ARCHITECTURE=amd \
BUDIBASE_ENVIRONMENT=PRODUCTION \
CLUSTER_PORT=80 \
COUCHDB_PASSWORD=budibase \
COUCHDB_USER=budibase \
COUCH_DB_URL=http://budibase:budibase@localhost:5984 \
# CUSTOM_DOMAIN=budi001.custom.com \
DEPLOYMENT_ENVIRONMENT=docker \
INTERNAL_API_KEY=budibase \
JWT_SECRET=testsecret \
MINIO_ACCESS_KEY=budibase \
MINIO_SECRET_KEY=budibase \
MINIO_URL=http://localhost:9000 \
POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS \
REDIS_PASSWORD=budibase \
REDIS_URL=localhost:6379 \
SELF_HOSTED=1 \
TARGETBUILD=$TARGETBUILD \
WORKER_PORT=4002 \
WORKER_URL=http://localhost:4002
WORKER_URL=http://localhost:4002 \
APPS_URL=http://localhost:4001
# These secret env variables are generated by the runner at startup
# their values can be overriden by the user, they will be written
# to the .env file in the /data directory for use later on
# REDIS_PASSWORD=budibase \
# COUCHDB_PASSWORD=budibase \
# COUCHDB_USER=budibase \
# COUCH_DB_URL=http://budibase:budibase@localhost:5984 \
# INTERNAL_API_KEY=budibase \
# JWT_SECRET=testsecret \
# MINIO_ACCESS_KEY=budibase \
# MINIO_SECRET_KEY=budibase \
# install base dependencies
RUN apt-get update && \
apt-get install -y software-properties-common wget nginx && \
apt-get install -y software-properties-common wget nginx uuid-runtime && \
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
apt-get update
@ -66,8 +71,8 @@ RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh &
npm install --global yarn pm2
# setup nginx
ADD hosting/single/nginx.conf /etc/nginx
ADD hosting/single/nginx-default-site.conf /etc/nginx/sites-enabled/default
ADD hosting/single/nginx/nginx.conf /etc/nginx
ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid
@ -86,13 +91,13 @@ RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clou
WORKDIR /opt/clouseau
RUN mkdir ./bin
ADD hosting/single/clouseau ./bin/
ADD hosting/single/log4j.properties hosting/single/clouseau.ini ./
ADD hosting/single/clouseau/clouseau ./bin/
ADD hosting/single/clouseau/log4j.properties hosting/single/clouseau/clouseau.ini ./
RUN chmod +x ./bin/clouseau
# setup CouchDB
WORKDIR /opt/couchdb
ADD hosting/single/vm.args ./etc/
ADD hosting/single/couch/vm.args hosting/single/couch/local.ini ./etc/
# setup minio
WORKDIR /minio

View file

@ -7,7 +7,7 @@ name=clouseau@127.0.0.1
cookie=monster
; the path where you would like to store the search index files
dir=/opt/couchdb/data/search
dir=/data/search
; the number of search indexes that can be open simultaneously
max_indexes_open=500

View file

@ -0,0 +1,5 @@
; CouchDB Configuration Settings
[couchdb]
database_dir = /data/couch/dbs
view_index_dir = /data/couch/views

View file

@ -88,7 +88,4 @@ server {
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}

View file

@ -1,6 +1,34 @@
#!/bin/bash
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
if [ -f "/data/.env" ]; then
export $(cat /data/.env | xargs)
fi
# first randomise any unset environment variables
for ENV_VAR in "${ENV_VARS[@]}"
do
temp=$(eval "echo \$$ENV_VAR")
if [[ -z "${temp}" ]]; then
eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')"
fi
done
if [[ -z "${COUCH_DB_URL}" ]]; then
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
fi
if [ ! -f "/data/.env" ]; then
touch /data/.env
for ENV_VAR in "${ENV_VARS[@]}"
do
temp=$(eval "echo \$$ENV_VAR")
echo "$ENV_VAR=$temp" >> /data/.env
done
fi
# make these directories in runner, incase of mount
mkdir -p /data/couch/dbs /data/couch/views
chown couchdb:couchdb /data/couch /data/couch/dbs /data/couch/views
redis-server --requirepass $REDIS_PASSWORD &
/opt/clouseau/bin/clouseau &
/minio/minio server /minio &
/minio/minio server /data/minio &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
/etc/init.d/nginx restart
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then

View file

@ -1,4 +1,4 @@
#!/bin/bash
id=$(docker run -t -d -p 80:80 budibase:latest)
id=$(docker run -t -d -p 8080:80 budibase:latest)
docker exec -it $id bash
docker kill $id

View file

@ -1,5 +1,5 @@
{
"version": "1.0.219-alpha.4",
"version": "1.0.220-alpha.4",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -25,7 +25,7 @@
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run build",
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
"release:pro": "bash scripts/pro/release.sh",
"release:pro:develop": "bash scripts/pro/release.sh develop",
@ -40,7 +40,8 @@
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test",
"test": "lerna run test && yarn test:pro",
"test:pro": "bash scripts/pro/test.sh",
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
@ -83,4 +84,4 @@
"install:pro": "bash scripts/pro/install.sh",
"dep:clean": "yarn clean && yarn bootstrap"
}
}
}

View file

@ -5,4 +5,5 @@ module.exports = {
app: require("./src/cache/appMetadata"),
writethrough: require("./src/cache/writethrough"),
...generic,
cache: generic,
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.219-alpha.4",
"version": "1.0.220-alpha.4",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/types": "^1.0.219-alpha.4",
"@budibase/types": "^1.0.220-alpha.4",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
"bcrypt": "5.0.1",
@ -59,6 +59,7 @@
]
},
"devDependencies": {
"@budibase/types": "^1.0.219",
"@shopify/jest-koa-mocks": "3.1.5",
"@types/jest": "27.5.1",
"@types/koa": "2.0.52",

View file

@ -40,7 +40,7 @@ const env = {
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PLATFORM_URL: process.env.PLATFORM_URL,
PLATFORM_URL: process.env.PLATFORM_URL || "",
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.219-alpha.4",
"version": "1.0.220-alpha.4",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.219-alpha.4",
"@budibase/string-templates": "^1.0.220-alpha.4",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",
@ -66,11 +66,12 @@
"@spectrum-css/radio": "^3.0.2",
"@spectrum-css/search": "^3.0.2",
"@spectrum-css/sidenav": "^3.0.2",
"@spectrum-css/slider": "3.0.1",
"@spectrum-css/statuslight": "^3.0.2",
"@spectrum-css/stepper": "^3.0.3",
"@spectrum-css/switch": "^1.0.2",
"@spectrum-css/table": "^3.0.1",
"@spectrum-css/tabs": "^3.0.1",
"@spectrum-css/tabs": "^3.2.12",
"@spectrum-css/tags": "^3.0.2",
"@spectrum-css/textfield": "^3.0.1",
"@spectrum-css/toast": "^3.0.1",

View file

@ -82,6 +82,12 @@
.active svg {
color: var(--spectrum-global-color-blue-600);
}
:global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) {
margin-left: 0;
}
.is-selected:not(.spectrum-ActionButton--emphasized) {
background: var(--spectrum-global-color-gray-300);
}
.noPadding {
padding: 0;
min-width: 0;

View file

@ -8,6 +8,7 @@
export let size = "S"
export let extraButtonText
export let extraButtonAction
export let showCloseButton = true
let show = true
@ -39,22 +40,24 @@
</button>
{/if}
</div>
<div class="spectrum-Toast-buttons">
<button
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}"
on:click={clear}
>
<div class="spectrum-ClearButton-fill">
<svg
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Cross100" />
</svg>
</div>
</button>
</div>
{#if showCloseButton}
<div class="spectrum-Toast-buttons">
<button
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}"
on:click={clear}
>
<div class="spectrum-ClearButton-fill">
<svg
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Cross100" />
</svg>
</div>
</button>
</div>
{/if}
</div>
{/if}
@ -63,4 +66,7 @@
pointer-events: all;
width: 100%;
}
.spectrum-Button {
border: 1px solid rgba(255, 255, 255, 0.2);
}
</style>

View file

@ -16,6 +16,9 @@
/>
<style>
hr {
background: var(--spectrum-global-color-gray-200);
}
hr.noMargin {
margin: 0;
}

View file

@ -6,6 +6,8 @@
export let title
export let fillWidth
export let left = "314px"
export let width = "calc(100% - 576px)"
let visible = false
@ -42,7 +44,12 @@
{#if visible}
<Portal>
<section class:fillWidth class="drawer" transition:slide|local>
<section
class:fillWidth
class="drawer"
transition:slide|local
style={`width: ${width}; left: ${left};`}
>
<header>
<div class="text">
<Heading size="XS">{title}</Heading>
@ -69,8 +76,6 @@
.drawer {
position: absolute;
bottom: 0;
left: 260px;
width: calc(100% - 520px);
background: var(--background);
border-top: var(--border-light);
z-index: 2;

View file

@ -47,7 +47,7 @@
return
}
searchTerm = null
open = true
open = !open
}
const getSortedOptions = (options, getLabel, sort) => {
@ -75,112 +75,77 @@
}
</script>
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled}
class:is-invalid={!!error}
class:is-open={open}
aria-haspopup="listbox"
on:mousedown={onClick}
>
{#if fieldIcon}
<span class="option-left">
<Icon name={fieldIcon} />
</span>
{:else if fieldColour}
<span class="option-left">
<StatusLight color={fieldColour} />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
<div use:clickOutside={() => (open = false)}>
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled}
class:is-invalid={!!error}
class:is-open={open}
aria-haspopup="listbox"
on:mousedown={onClick}
>
{fieldText}
</span>
{#if error}
{#if fieldIcon}
<span class="option-icon">
<Icon name={fieldIcon} />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
>
{fieldText}
</span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
{#if fieldColour}
<span class="option-colour">
<StatusLight size="L" color={fieldColour} />
</span>
{/if}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
{/if}
{#if fieldIcon && fieldColour}
<span class="option-right">
<StatusLight color={fieldColour} />
</span>
{/if}
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
{#if open}
<div
use:clickOutside={() => (open = false)}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:auto-width={autoWidth}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}
>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
</button>
{#if open}
<div
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:auto-width={autoWidth}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
{#if filteredOptions.length}
{#each filteredOptions as option, idx}
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
on:click={() => onSelectOption(null)}
>
{#if getOptionIcon(option, idx)}
<span class="option-left">
<Icon name={getOptionIcon(option, idx)} />
</span>
{:else if getOptionColour(option, idx)}
<span class="option-left">
<StatusLight color={getOptionColour(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
@ -188,17 +153,45 @@
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
{#if getOptionIcon(option, idx) && getOptionColour(option, idx)}
<span class="option-right">
<StatusLight color={getOptionColour(option, idx)} />
</span>
{/if}
</li>
{/each}
{/if}
</ul>
</div>
{/if}
{/if}
{#if filteredOptions.length}
{#each filteredOptions as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
>
{#if getOptionIcon(option, idx)}
<span class="option-icon">
<Icon name={getOptionIcon(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
{#if getOptionColour(option, idx)}
<span class="option-colour">
<StatusLight size="L" color={getOptionColour(option, idx)} />
</span>
{/if}
</li>
{/each}
{/if}
</ul>
</div>
{/if}
</div>
<style>
.spectrum-Popover {
@ -233,14 +226,13 @@
.spectrum-Menu-checkmark {
align-self: center;
margin-top: 0;
margin-left: 12px;
}
.option-left {
padding-right: 8px;
}
.option-right {
.option-colour {
padding-left: 8px;
}
.option-icon {
padding-right: 8px;
}
.spectrum-Popover :global(.spectrum-Search) {
margin-top: -1px;

View file

@ -8,6 +8,8 @@
export let id = null
export let updateOnChange = true
export let quiet = false
export let inputRef
const dispatch = createEventDispatcher()
let focus = false
@ -68,6 +70,7 @@
type="search"
class="spectrum-Textfield-input spectrum-Search-input"
autocomplete="off"
bind:this={inputRef}
/>
</div>
<button

View file

@ -0,0 +1,86 @@
<script>
import "@spectrum-css/slider/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let value = false
export let id = null
export let disabled = false
export let min = 0
export let max = 100
export let step = 1
const dispatch = createEventDispatcher()
const onChange = event => {
dispatch("change", event.target.value)
}
</script>
<div>
<input
type="range"
{min}
{max}
{step}
{value}
{disabled}
{id}
on:change={onChange}
/>
</div>
<style>
div {
display: grid;
place-items: center;
}
input {
width: 100%;
padding: 0;
margin: 0;
-webkit-appearance: none;
background: transparent;
}
input::-webkit-slider-thumb {
-webkit-appearance: none;
}
input:focus {
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
border: 2px solid var(--spectrum-global-color-gray-700);
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--background);
cursor: pointer;
transition: background 130ms ease-out;
margin-top: -7px;
}
input[type="range"]::-moz-range-thumb {
border: 2px solid var(--spectrum-global-color-gray-700);
height: 12px;
width: 12px;
border-radius: 50%;
background: var(--background);
cursor: pointer;
transition: background 130ms ease-out;
}
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: var(--spectrum-global-color-gray-300);
border-radius: 2px;
}
input[type="range"]::-moz-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: var(--spectrum-global-color-gray-300);
border-radius: 2px;
}
</style>

View file

@ -12,3 +12,4 @@ export { default as CoreDatePicker } from "./DatePicker.svelte"
export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreStepper } from "./Stepper.svelte"
export { default as CoreRichTextField } from "./RichTextField.svelte"
export { default as CoreSlider } from "./Slider.svelte"

View file

@ -10,6 +10,7 @@
export let disabled = false
export let updateOnChange = true
export let quiet = false
export let inputRef
const dispatch = createEventDispatcher()
const onChange = e => {
@ -25,6 +26,7 @@
{value}
{placeholder}
{quiet}
bind:inputRef
on:change={onChange}
on:click
on:input

View file

@ -0,0 +1,24 @@
<script>
import Field from "./Field.svelte"
import Slider from "./Core/Slider.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = null
export let labelPosition = "above"
export let min = 0
export let max = 100
export let step = 1
export let disabled = false
export let error = null
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Field {label} {labelPosition} {error}>
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} />
</Field>

View file

@ -47,7 +47,7 @@
</svg>
{#if tooltip && showTooltip}
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
<Tooltip textWrapping direction={"bottom"} text={tooltip} />
<Tooltip textWrapping direction="bottom" text={tooltip} />
</div>
{/if}
</div>

View file

@ -0,0 +1,14 @@
<div class="icon-side-nav">
<slot />
</div>
<style>
.icon-side-nav {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: var(--spacing-s);
gap: var(--spacing-xs);
}
</style>

View file

@ -0,0 +1,56 @@
<script>
import Icon from "../Icon/Icon.svelte"
import Tooltip from "../Tooltip/Tooltip.svelte"
import { fade } from "svelte/transition"
export let icon
export let active = false
export let tooltip
let showTooltip = false
</script>
<div
class="icon-side-nav-item"
class:active
on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:click
>
<Icon name={icon} hoverable />
{#if tooltip && showTooltip}
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
<Tooltip textWrapping direction="right" text={tooltip} />
</div>
{/if}
</div>
<style>
.icon-side-nav-item {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 4px;
position: relative;
cursor: pointer;
transition: background 130ms ease-out;
}
.icon-side-nav-item:hover :global(svg),
.active :global(svg) {
color: var(--spectrum-global-color-gray-900);
}
.active {
background: var(--spectrum-global-color-gray-300);
}
.tooltip {
position: absolute;
pointer-events: none;
left: calc(100% - 4px);
top: 50%;
white-space: nowrap;
transform: translateY(-50%);
z-index: 1;
}
</style>

View file

@ -9,7 +9,7 @@
<Portal target=".modal-container">
<div class="notifications">
{#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
<div transition:fly={{ y: -30 }}>
<div transition:fly={{ y: 30 }}>
<Notification
{type}
{icon}
@ -27,7 +27,7 @@
<style>
.notifications {
position: fixed;
top: 20px;
bottom: 40px;
left: 0;
right: 0;
margin: 0 auto;

View file

@ -79,4 +79,10 @@
.emphasized {
color: var(--spectrum-global-color-blue-600);
}
.spectrum-Tabs-item {
color: var(--spectrum-global-color-gray-600);
}
.spectrum-Tabs-item.is-selected {
color: var(--spectrum-global-color-gray-900);
}
</style>

View file

@ -10,8 +10,7 @@
export let noHorizPadding = false
export let quiet = false
export let emphasized = false
// overlay content from the tab bar onto tabs e.g. for a dropdown
export let onTop = false
export let size = "M"
let thisSelected = undefined
@ -74,20 +73,18 @@
<div
bind:this={container}
class:quiet
class:spectrum-Tabs--quiet={quiet}
class:noHorizPadding
class="selected-border spectrum-Tabs {quiet &&
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
? 'vertical'
: 'horizontal'}"
class:onTop
class:spectrum-Tabs--vertical={vertical}
class:spectrum-Tabs--horizontal={!vertical}
class="spectrum-Tabs spectrum-Tabs--size{size}"
>
<slot />
{#if $tab.info}
<div
class="spectrum-Tabs-selectionIndicator indicator-transition"
style="{emphasized &&
'background-color: var(--spectrum-global-color-blue-400)'}; width: {width}; height: {height}; left: {left}; top: {top};"
class="spectrum-Tabs-selectionIndicator"
class:emphasized
style="width: {width}; height: {height}; left: {left}; top: {top};"
/>
{/if}
</div>
@ -98,26 +95,26 @@
/>
<style>
.quiet {
.spectrum-Tabs--quiet {
border-bottom: none !important;
}
.onTop {
z-index: 20;
}
.spectrum-Tabs {
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
position: relative;
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
border-bottom-color: var(--spectrum-global-color-gray-200);
}
.spectrum-Tabs-content {
margin-top: var(--spectrum-global-dimension-static-size-150);
}
.indicator-transition {
.spectrum-Tabs-selectionIndicator {
transition: all 200ms;
background-color: var(--spectrum-global-color-gray-900);
}
.spectrum-Tabs-selectionIndicator.emphasized {
background-color: var(--spectrum-global-color-blue-400);
}
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
bottom: 0 !important;
}
.noHorizPadding {
padding: 0;

View file

@ -8,7 +8,7 @@
<!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
{#if textWrapping}
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open tooltip">
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" />
</span>
@ -22,3 +22,9 @@
</div>
</span>
{/if}
<style>
.tooltip {
pointer-events: none;
}
</style>

View file

@ -69,6 +69,9 @@ export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
export { default as RichTextField } from "./Form/RichTextField.svelte"
export { default as List } from "./List/List.svelte"
export { default as ListItem } from "./List/ListItem.svelte"
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
export { default as Slider } from "./Form/Slider.svelte"
// Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"

View file

@ -206,6 +206,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713"
integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g==
"@spectrum-css/slider@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/slider/-/slider-3.0.1.tgz#5281e6f47eb5a4fd3d1816c138bf66d01d7f2e49"
integrity sha512-DI2dtMRnQuDM1miVzl3SGyR1khUEKnwdXfO5EHDFwkC3yav43F5QogkfjmjFmWWobMVovdJlAuiaaJ/IHejD0Q==
"@spectrum-css/statuslight@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
@ -226,10 +231,10 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/table/-/table-3.0.2.tgz#c666743d569fef81ddc8810fac8cda53b315f8d7"
integrity sha512-nt/QNC7NmUank0wozd4FySEX1UIYXuvuOKDyN1II3sxfwFSpJfp/Df9KVMhrYs4EsmB4XMGcoxp8ND/CrvH3ow==
"@spectrum-css/tabs@^3.0.1":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.0.2.tgz#822316672e7b0dfba66faa988e638ddae18c700e"
integrity sha512-4RNcmwf0wxLpB7M54H02owlj0mKE8neL1+lytQpxOOhlwTO5zdsD82zjvx9tIc8tRnRKuhCCCwTuBxHYstnBmw==
"@spectrum-css/tabs@^3.2.12":
version "3.2.12"
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.2.12.tgz#9b08f23d5aa881b3441af7757800c7173e5685ff"
integrity sha512-rPFUW9SSW4+3/UJ3UrtY2/l3sQvlqB1fqxHLPDjgykvbfrnMejcCTNV4ZrFNHXpE/6+kGnk+yVViSPtWGwJzkA==
"@spectrum-css/tags@^3.0.2":
version "3.0.2"

View file

@ -16,4 +16,4 @@
"runMode": 1,
"openMode": 0
}
}
}

View file

@ -10,6 +10,7 @@ filterTests(['all'], () => {
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
cy.navigateToFrontend()
cy.wait(500)
cy.addComponent("Form", "Form")
cy.addComponent("Form", "Options Picker").then((componentId) => {
// Provide field setting
@ -36,5 +37,9 @@ filterTests(['all'], () => {
})
cy.addCustomSourceOptions(totalRadioButtons)
}
after(() => {
cy.deleteAllApps()
})
})
})

View file

@ -9,10 +9,11 @@ filterTests(["smoke", "all"], () => {
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests")
cy.createApp("Cypress Tests", false)
// Create new user
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000})
cy.wait(500)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.createUser(bbUserEmail)
cy.contains("bbuser").click()
cy.wait(500)

View file

@ -6,11 +6,11 @@ filterTests(["smoke", "all"], () => {
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests")
cy.createApp("Cypress Tests", false)
})
it("should create a user via basic onboarding", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000})
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.createUser("bbuser@test.com")
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
})
@ -43,19 +43,20 @@ filterTests(["smoke", "all"], () => {
const uuid = () => Cypress._.random(0, 1e6)
const name = uuid()
if(i < 1){
cy.createApp(name)
cy.createApp(name, false)
} else {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.wait(1000)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true })
cy.createAppFromScratch(name)
}
}
}
})
// Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 500})
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).contains("bbuser").click()
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
for (let i = 0; i < 3; i++) {
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
.eq(1)
@ -64,24 +65,24 @@ filterTests(["smoke", "all"], () => {
.find(interact.SPECTRUM_TABLE_CELL)
.eq(0)
.click()
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 500 })
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 })
.contains("Choose an option")
.click()
.then(() => {
if (i == 0) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Admin").click({ force: true })
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true })
}
else if (i == 1) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Power").click({ force: true })
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true })
}
else if (i == 2) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Basic").click({ force: true })
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true })
}
cy.get(interact.SPECTRUM_BUTTON, { timeout: 1000 })
cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 })
.contains("Update role")
.click({ force: true })
})
cy.reload()
cy.reload({ timeout: 5000 })
cy.wait(1000)
}
// Confirm roles exist within Configure roles table
@ -173,14 +174,16 @@ filterTests(["smoke", "all"], () => {
it("Should edit user details within user details page", () => {
// Add First name
cy.get(interact.FIELD, { timeout: 500 }).eq(2).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).type("bb")
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
})
// Add Last name
cy.get(interact.FIELD).eq(3).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("test")
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
})
cy.get(interact.FIELD).eq(0).click()
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click()
// Reload page
cy.reload()
@ -188,8 +191,8 @@ filterTests(["smoke", "all"], () => {
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
})
cy.get(interact.FIELD).eq(3).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).should('have.value', "test")
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
})
})

View file

@ -103,6 +103,8 @@ filterTests(["smoke", "all"], () => {
}
cy.get("button").contains("Update password").click({ force: true })
})
// Remove users name
cy.updateUserInformation()
})
})
})

View file

@ -5,7 +5,8 @@ filterTests(["all"], () => {
context("Application Overview screen", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.deleteAllApps()
cy.createApp("Cypress Tests")
})
it("Should be accessible from the applications list", () => {
@ -81,13 +82,14 @@ filterTests(["all"], () => {
})
it("Should reflect the app deployment state", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button")
.contains("Edit")
.eq(0)
.click({ force: true })
cy.get(".toprightnav button.spectrum-Button")
cy.wait(500)
cy.get(".toprightnav button.spectrum-Button", { timeout: 2000 })
.contains("Publish")
.click({ force: true })
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']")
@ -300,7 +302,7 @@ filterTests(["all"], () => {
})
it("Should allow editing of the app details.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button")
.contains("Manage")
.eq(0)
@ -315,7 +317,8 @@ filterTests(["all"], () => {
cy.updateAppName("sample name")
//publish and check its disabled
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.wait(500)
cy.get(".appTable .app-row-actions button")
.contains("Edit")
.eq(0)
@ -331,8 +334,8 @@ filterTests(["all"], () => {
cy.wait(1000)
})
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button", { timeout: 1000 })
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
cy.get(".appTable .app-row-actions button", { timeout: 5000 })
.contains("Manage")
.eq(0)
.click({ force: true })

View file

@ -6,11 +6,12 @@ filterTests(['all'], () => {
context("Publish Application Workflow", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.deleteAllApps()
cy.createApp("Cypress Tests", false)
})
it("Should reflect the unpublished status correctly", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0)
.within(() => {
@ -29,6 +30,7 @@ filterTests(['all'], () => {
it("Should publish an application and correctly reflect that", () => {
//Assuming the previous test was run and the unpublished app is open in edit mode.
cy.closeModal()
cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true })
cy.get(interact.DEPLOY_APP_MODAL).should("be.visible")
@ -72,7 +74,7 @@ filterTests(['all'], () => {
it("Should unpublish an application using the link and reflect the status change", () => {
//Assuming the previous test app exists and is published
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.APP_TABLE_STATUS).eq(0)
.within(() => {
@ -85,6 +87,7 @@ filterTests(['all'], () => {
cy.get(interact.APP_TABLE_APP_NAME).click({ force: true })
})
cy.closeModal()
cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
cy.get("[data-cy='publish-popover-menu']")
@ -97,7 +100,8 @@ filterTests(['all'], () => {
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }
)})
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 })
cy.wait(500)
cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished")
})

View file

@ -51,7 +51,8 @@ filterTests(['smoke', 'all'], () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
// Start create app process. If apps already exist, click second button
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
cy.wait(1000)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 3000 }).click({ force: true })
const appName = "Cypress Tests"
cy.get(interact.SPECTRUM_MODAL).within(() => {
@ -86,7 +87,7 @@ filterTests(['smoke', 'all'], () => {
const appName = "Cypress Tests"
cy.createApp(appName, false)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.applicationInAppTable(appName)
cy.deleteApp(appName)

View file

@ -5,7 +5,6 @@ filterTests(['smoke', 'all'], () => {
context("Create a View", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.createTable("data")
cy.addColumn("data", "group", "Text")

View file

@ -111,6 +111,7 @@ filterTests(["all"], () => {
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
cy.wait(1000)
})
// Confirm table length & relationship name
cy.get(".spectrum-Table", { timeout: 1000 })

View file

@ -151,7 +151,7 @@ filterTests(["all"], () => {
cy.get("@query").its("response.body").should("not.be.empty")
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".hierarchy-items-container").should("contain", queryName)
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName)
})
it("should switch to schema with no tables", () => {
@ -217,24 +217,24 @@ filterTests(["all"], () => {
it("should edit a query name", () => {
// Access query
cy.get(".hierarchy-items-container")
cy.get(".hierarchy-items-container", { timeout: 2000 })
.contains(queryName + " (1)")
.click()
// Rename query
cy.get(".spectrum-Form-item")
cy.wait(1000)
cy.get(".spectrum-Form-item", { timeout: 2000 })
.eq(0)
.within(() => {
cy.get("input").clear().type(queryRename)
})
// Run and Save query
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.get(".spectrum-Button", { timeout: 500 }).contains("Save Query").click({ force: true })
//cy.reload()
//cy.wait(500)
cy.get(".nav-item").should("contain", queryRename)
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true })
cy.wait(1000)
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true })
cy.reload({ timeout: 5000 })
cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename)
})
it("should delete a query", () => {
@ -251,6 +251,7 @@ filterTests(["all"], () => {
.contains("Delete Query")
.click({ force: true })
// Confirm deletion
cy.reload({ timeout: 5000 })
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
})

View file

@ -12,7 +12,7 @@ filterTests(["all"], () => {
const appName = "Cypress Tests"
const appRename = "Cypress Renamed"
// Rename app, Search for app, Confirm name was changed
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, appRename)
cy.reload()
cy.searchForApplication(appRename)
@ -39,7 +39,7 @@ filterTests(["all"], () => {
.click({ force: true })
})
// Rename app, Search for app, Confirm name was changed
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, appRename, true)
cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1)
cy.applicationInAppTable(appRename)
@ -47,7 +47,7 @@ filterTests(["all"], () => {
it("Should try to rename an application to have no name", () => {
const appName = "Cypress Tests"
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, " ", false, true)
// Close modal and confirm name has not been changed
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click()
@ -57,7 +57,7 @@ filterTests(["all"], () => {
xit("Should create two applications with the same name", () => {
// It is not possible to have applications with the same name
const appName = "Cypress Tests"
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 }
.contains("Create app")
.click({ force: true })
@ -80,18 +80,15 @@ filterTests(["all"], () => {
const appName = "Cypress Tests"
const numberName = 12345
const specialCharName = "£$%^"
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, numberName)
cy.reload()
cy.applicationInAppTable(numberName)
cy.reload()
renameApp(numberName, specialCharName)
cy.get(interact.ERROR).should(
"have.text",
"App name must be letters, numbers and spaces only"
)
// Set app name back to Cypress Tests
cy.reload()
renameApp(numberName, appName)
})

View file

@ -134,15 +134,18 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
const shouldCreateDefaultTable =
typeof addDefaultTable != "boolean" ? true : addDefaultTable
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(`[data-cy="create-app-btn"]`, { timeout: 2000 }).click({ force: true })
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
cy.wait(1000)
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
// If apps already exist
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({
force: true,
})
}
})
@ -400,17 +403,19 @@ Cypress.Commands.add("createAppFromScratch", appName => {
Cypress.Commands.add("createTable", (tableName, initialTable) => {
if (!initialTable) {
cy.navigateToDataSection()
cy.get(`[data-cy="new-table"]`).click()
cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click()
}
cy.wait(2000)
cy.get(".item")
cy.get(".item", { timeout: 2000 })
.contains("Budibase DB")
.click({ force: true })
.then(() => {
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.get(".spectrum-Button", { timeout: 2000 })
.contains("Continue")
.click({ force: true })
})
cy.get(".spectrum-Modal").within(() => {
cy.get("input", { timeout: 1000 }).first().type(tableName).blur()
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
cy.contains(tableName).should("be.visible")
@ -504,12 +509,13 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
// DESIGN AREA
Cypress.Commands.add("addComponent", (category, component) => {
if (category) {
cy.get(`[data-cy="category-${category}"]`, { timeout: 1000 }).click({
cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({
force: true,
})
}
cy.wait(500)
if (component) {
cy.get(`[data-cy="component-${component}"]`, { timeout: 1000 }).click({
cy.get(`[data-cy="component-${component}"]`, { timeout: 3000 }).click({
force: true,
})
}
@ -517,7 +523,7 @@ Cypress.Commands.add("addComponent", (category, component) => {
cy.location().then(loc => {
const params = loc.pathname.split("/")
const componentId = params[params.length - 1]
cy.getComponent(componentId).should("exist")
cy.getComponent(componentId, { timeout: 3000 }).should("exist")
return cy.wrap(componentId)
})
})
@ -621,8 +627,8 @@ Cypress.Commands.add("navigateToFrontend", () => {
// Clicks on Design tab and then the Home nav item
cy.wait(500)
cy.contains("Design").click()
cy.get(".spectrum-Search").type("/")
cy.get(".nav-item").contains("home").click()
cy.get(".spectrum-Search", { timeout: 2000 }).type("/")
cy.get(".nav-item", { timeout: 2000 }).contains("home").click()
})
Cypress.Commands.add("navigateToDataSection", () => {
@ -782,7 +788,7 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
// MISC
Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal").within(() => {
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get(".close-icon").click()
cy.wait(1000) // Wait for modal to close
})

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.219-alpha.4",
"version": "1.0.220-alpha.4",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -69,10 +69,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.219-alpha.4",
"@budibase/client": "^1.0.219-alpha.4",
"@budibase/frontend-core": "^1.0.219-alpha.4",
"@budibase/string-templates": "^1.0.219-alpha.4",
"@budibase/bbui": "^1.0.220-alpha.4",
"@budibase/client": "^1.0.220-alpha.4",
"@budibase/frontend-core": "^1.0.220-alpha.4",
"@budibase/string-templates": "^1.0.220-alpha.4",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -95,7 +95,7 @@
"@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.0",
"@roxi/routify": "2.18.5",
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0",
@ -113,7 +113,7 @@
"rollup": "^2.44.0",
"rollup-plugin-copy": "^3.4.0",
"start-server-and-test": "^1.12.1",
"svelte": "^3.38.2",
"svelte": "^3.48.0",
"svelte-jester": "^1.3.2",
"ts-node": "^10.4.0",
"tsconfig-paths": "4.0.0",

View file

@ -20,7 +20,7 @@ import {
} from "@budibase/string-templates"
import { TableNames } from "../constants"
import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -478,11 +478,17 @@ const getUrlBindings = asset => {
}
})
const safeURL = makePropSafe("url")
return params.map(param => ({
const urlParamBindings = params.map(param => ({
type: "context",
runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
readableBinding: `URL.${param}`,
}))
const queryParamsBinding = {
type: "context",
runtimeBinding: makePropSafe("query"),
readableBinding: "Query params",
}
return urlParamBindings.concat([queryParamsBinding])
}
const getRoleBindings = () => {
@ -782,6 +788,13 @@ export const getAllStateVariables = () => {
})
})
// Add on load settings from screens
get(store).screens.forEach(screen => {
if (screen.onLoad) {
eventSettings.push(screen.onLoad)
}
})
// Extract all state keys from any "update state" actions in each setting
let bindingSet = new Set()
eventSettings.forEach(setting => {

View file

@ -1,65 +1,77 @@
import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { derived } from "svelte/store"
import { LAYOUT_NAMES } from "../constants"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const currentAsset = derived(store, $store => {
const type = $store.currentFrontEndType
if (type === FrontendTypes.SCREEN) {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
} else if (type === FrontendTypes.LAYOUT) {
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId)
}
return null
export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
})
export const selectedLayout = derived(store, $store => {
return $store.layouts?.find(layout => layout._id === $store.selectedLayoutId)
})
export const selectedComponent = derived(
[store, currentAsset],
([$store, $currentAsset]) => {
if (!$currentAsset || !$store.selectedComponentId) {
[store, selectedScreen],
([$store, $selectedScreen]) => {
if (!$selectedScreen || !$store.selectedComponentId) {
return null
}
return findComponent($currentAsset?.props, $store.selectedComponentId)
return findComponent($selectedScreen?.props, $store.selectedComponentId)
}
)
export const sortedScreens = derived(store, $store => {
return $store.screens.slice().sort((a, b) => {
// Sort by role first
const roleA = RoleUtils.getRolePriority(a.routing.roleId)
const roleB = RoleUtils.getRolePriority(b.routing.roleId)
if (roleA !== roleB) {
return roleA > roleB ? -1 : 1
}
// Then put home screens first
const homeA = !!a.routing.homeScreen
const homeB = !!b.routing.homeScreen
if (homeA !== homeB) {
return homeA ? -1 : 1
}
// Then sort alphabetically by each URL param
const aParams = a.routing.route.split("/")
const bParams = b.routing.route.split("/")
let minParams = Math.min(aParams.length, bParams.length)
for (let i = 0; i < minParams; i++) {
if (aParams[i] === bParams[i]) {
continue
}
return aParams[i] < bParams[i] ? -1 : 1
}
// Then sort by the fewest amount of URL params
return aParams.length < bParams.length ? -1 : 1
})
})
export const selectedComponentPath = derived(
[store, currentAsset],
([$store, $currentAsset]) => {
[store, selectedScreen],
([$store, $selectedScreen]) => {
return findComponentPath(
$currentAsset?.props,
$selectedScreen?.props,
$store.selectedComponentId
).map(component => component._id)
}
)
export const currentAssetId = derived(store, $store => {
return $store.currentFrontEndType === FrontendTypes.SCREEN
? $store.selectedScreenId
: $store.selectedLayoutId
})
export const currentAssetName = derived(currentAsset, $currentAsset => {
return $currentAsset?.name
})
// leave this as before for consistency
export const allScreens = derived(store, $store => {
return $store.screens
})
export const mainLayout = derived(store, $store => {
return $store.layouts?.find(
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
)
})
export const selectedAccessRole = writable("BASIC")
export const screenSearchString = writable(null)
// For compatibility
export const currentAsset = selectedScreen

View file

@ -68,7 +68,19 @@ const automationActions = store => ({
return state
})
},
duplicate: async automation => {
const response = await API.createAutomation({
...automation,
name: `${automation.name} - copy`,
_id: undefined,
_ref: undefined,
})
store.update(state => {
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
},
save: async automation => {
const response = await API.updateAutomation(automation)
store.update(state => {

View file

@ -1,12 +1,6 @@
import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
allScreens,
currentAsset,
mainLayout,
selectedComponent,
selectedAccessRole,
} from "builderStore"
import { currentAsset, mainLayout, selectedComponent } from "builderStore"
import {
datasources,
integrations,
@ -15,7 +9,6 @@ import {
tables,
} from "stores/backend"
import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
findComponentType,
@ -27,6 +20,7 @@ import {
makeComponentUnique,
} from "../componentUtils"
import { Helpers } from "@budibase/bbui"
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -47,10 +41,6 @@ const INITIAL_FRONTEND_STATE = {
messagePassing: false,
continueIfAction: false,
},
currentFrontEndType: "none",
selectedScreenId: "",
selectedLayoutId: "",
selectedComponentId: "",
errors: [],
hasAppPackage: false,
libraries: null,
@ -61,6 +51,11 @@ const INITIAL_FRONTEND_STATE = {
customTheme: {},
previewDevice: "desktop",
highlightedSettingKey: null,
// URL params
selectedScreenId: null,
selectedComponentId: null,
selectedLayoutId: null,
}
export const getFrontendStore = () => {
@ -100,6 +95,7 @@ export const getFrontendStore = () => {
previousTopNavPath: {},
version: application.version,
revertableVersion: application.revertableVersion,
navigation: application.navigation || {},
}))
// Initialise backend stores
@ -108,6 +104,35 @@ export const getFrontendStore = () => {
await integrations.init()
await queries.init()
await tables.init()
// Add navigation settings to old apps
if (!application.navigation) {
const layout = layouts.find(x => x._id === LAYOUT_NAMES.MASTER.PRIVATE)
const customTheme = application.customTheme
let navigationSettings = {
navigation: "Top",
title: application.name,
navWidth: "Large",
navBackground:
customTheme?.navBackground || DefaultAppTheme.navBackground,
navTextColor:
customTheme?.navTextColor || DefaultAppTheme.navTextColor,
}
if (layout) {
navigationSettings.hideLogo = layout.props.hideLogo
navigationSettings.hideTitle = layout.props.hideTitle
navigationSettings.title = layout.props.title || application.name
navigationSettings.logoUrl = layout.props.logoUrl
navigationSettings.links = layout.props.links
navigationSettings.navigation = layout.props.navigation || "Top"
navigationSettings.sticky = layout.props.sticky
navigationSettings.navWidth = layout.props.width || "Large"
if (navigationSettings.navigation === "None") {
navigationSettings.navigation = "Top"
}
}
await store.actions.navigation.save(navigationSettings)
}
},
theme: {
save: async theme => {
@ -135,6 +160,19 @@ export const getFrontendStore = () => {
})
},
},
navigation: {
save: async navigation => {
const appId = get(store).appId
await API.saveAppMetadata({
appId,
metadata: { navigation },
})
store.update(state => {
state.navigation = navigation
return state
})
},
},
routing: {
fetch: async () => {
const response = await API.fetchAppRoutes()
@ -147,18 +185,12 @@ export const getFrontendStore = () => {
screens: {
select: screenId => {
store.update(state => {
let screens = get(allScreens)
let screens = state.screens
let screen =
screens.find(screen => screen._id === screenId) || screens[0]
if (!screen) return state
// Update role to the screen's role setting so that it will always
// be visible
selectedAccessRole.set(screen.routing.roleId)
state.currentFrontEndType = FrontendTypes.SCREEN
state.selectedScreenId = screen._id
state.currentView = "detail"
state.selectedComponentId = screen.props?._id
return state
})
@ -221,16 +253,44 @@ export const getFrontendStore = () => {
// Refresh routes
await store.actions.routing.fetch()
},
updateHomeScreen: async (screen, makeHomeScreen = true) => {
let promises = []
// Find any existing home screen for this role so we can remove it,
// if we are setting this to be the new home screen
if (makeHomeScreen) {
const roleId = screen.routing.roleId
let existingHomeScreen = get(store).screens.find(s => {
return (
s.routing.roleId === roleId &&
s.routing.homeScreen &&
s._id !== screen._id
)
})
if (existingHomeScreen) {
existingHomeScreen.routing.homeScreen = false
promises.push(store.actions.screens.save(existingHomeScreen))
}
}
// Update the passed in screen
screen.routing.homeScreen = makeHomeScreen
promises.push(store.actions.screens.save(screen))
return await Promise.all(promises)
},
removeCustomLayout: async screen => {
// Pull relevant settings from old layout, if required
const layout = get(store).layouts.find(x => x._id === screen.layoutId)
screen.layoutId = null
screen.showNavigation = layout?.props.navigation !== "None"
screen.width = layout?.props.width || "Large"
await store.actions.screens.save(screen)
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
return await store.actions.screens.save(selectedAsset)
} else {
return await store.actions.layouts.save(selectedAsset)
}
return await store.actions.screens.save(selectedAsset)
},
setDevice: device => {
store.update(state => {
@ -245,8 +305,6 @@ export const getFrontendStore = () => {
const layout =
store.actions.layouts.find(layoutId) || get(store).layouts[0]
if (!layout) return
state.currentFrontEndType = FrontendTypes.LAYOUT
state.currentView = "detail"
state.selectedLayoutId = layout._id
state.selectedComponentId = layout.props?._id
return state
@ -297,32 +355,6 @@ export const getFrontendStore = () => {
},
},
components: {
select: component => {
const asset = get(currentAsset)
if (!asset || !component) {
return
}
// If this is the root component, select the asset instead
const parent = findComponentParent(asset.props, component._id)
if (parent == null) {
const state = get(store)
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
if (isLayout) {
store.actions.layouts.select(asset._id)
} else {
store.actions.screens.select(asset._id)
}
return
}
// Otherwise select the component
store.update(state => {
state.selectedComponentId = component._id
state.currentView = "component"
return state
})
},
getDefinition: componentName => {
if (!componentName) {
return null
@ -418,7 +450,6 @@ export const getFrontendStore = () => {
// Save components and update UI
await store.actions.preview.saveSelected()
store.update(state => {
state.currentView = "component"
state.selectedComponentId = componentInstance._id
return state
})
@ -461,11 +492,14 @@ export const getFrontendStore = () => {
parent._children = parent._children.filter(
child => child._id !== component._id
)
store.actions.components.select(parent)
store.update(state => {
state.selectedComponentId = parent._id
return state
})
}
await store.actions.preview.saveSelected()
},
copy: (component, cut = false) => {
copy: (component, cut = false, selectParent = true) => {
const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return null
@ -485,7 +519,12 @@ export const getFrontendStore = () => {
parent._children = parent._children.filter(
child => child._id !== component._id
)
store.actions.components.select(parent)
if (selectParent) {
store.update(state => {
state.selectedComponentId = parent._id
return state
})
}
}
}
},
@ -536,7 +575,7 @@ export const getFrontendStore = () => {
// Save and select the new component
promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste)
state.selectedComponentId = componentToPaste._id
return state
})
await Promise.all(promises)
@ -578,35 +617,38 @@ export const getFrontendStore = () => {
},
links: {
save: async (url, title) => {
const layout = get(mainLayout)
if (!layout) {
const navigation = get(store).navigation
let links = [...navigation?.links]
// Skip if we have an identical link
if (links.find(link => link.url === url && link.text === title)) {
return
}
// Add link setting to main layout
if (!layout.props.links) {
layout.props.links = []
}
layout.props.links.push({
links.push({
text: title,
url,
})
await store.actions.layouts.save(layout)
await store.actions.navigation.save({
...navigation,
links: [...links],
})
},
delete: async urls => {
const layout = get(mainLayout)
if (!layout?.props.links?.length) {
const navigation = get(store).navigation
let links = navigation?.links
if (!links?.length) {
return
}
// Filter out the URLs to delete
urls = Array.isArray(urls) ? urls : [urls]
layout.props.links = layout.props.links.filter(
link => !urls.includes(link.url)
)
links = links.filter(link => !urls.includes(link.url))
await store.actions.layouts.save(layout)
await store.actions.navigation.save({
...navigation,
links,
})
},
},
settings: {

View file

@ -5,7 +5,8 @@ export class Screen extends BaseStructure {
constructor() {
super(true)
this._json = {
layoutId: "layout_private_master",
showNavigation: true,
width: "Large",
props: {
_id: Helpers.uuid(),
_component: "@budibase/standard-components/container",
@ -26,6 +27,7 @@ export class Screen extends BaseStructure {
routing: {
route: "",
roleId: "BASIC",
homeScreen: false,
},
name: "screen-id",
}

View file

@ -19,12 +19,23 @@
notifications.error("Error deleting automation")
}
}
async function duplicateAutomation() {
try {
await automationStore.actions.duplicate(automation)
notifications.success("Automation has been duplicated successfully")
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
} catch (error) {
notifications.error("Error duplicating automation")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Duplicate" on:click={duplicateAutomation}>Duplicate</MenuItem>
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>

View file

@ -26,7 +26,7 @@
import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
import { LuceneUtils } from "@budibase/frontend-core"
import { getSchemaForTable } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core"

View file

@ -1,7 +1,7 @@
<script>
import { createEventDispatcher } from "svelte"
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
export let schema
export let filters

View file

@ -5,8 +5,9 @@
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api"
import { ModalContent, Select } from "@budibase/bbui"
import { ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify"
export let row = {}
@ -87,6 +88,15 @@
onConfirm={saveRow}
>
<ErrorsBox {errors} />
<!-- need to explain to the user the readonly fields -->
{#if !creating}
<div>
A user's email, role, first and last names cannot be changed from within
the app builder. Please go to the <Link
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
> to do this.
</div>
{/if}
<RowFieldControl
meta={{ ...tableSchema.email, name: "Email" }}
bind:value={row.email}

View file

@ -157,7 +157,8 @@
<style>
.datasource-icon {
margin-right: 3px;
padding-top: 3px;
display: grid;
place-items: center;
flex: 0 0 24px;
}
</style>

View file

@ -1,6 +1,6 @@
<script>
import { goto } from "@roxi/routify"
import { allScreens, store } from "builderStore"
import { store } from "builderStore"
import { tables, datasources } from "stores/backend"
import {
ActionMenu,
@ -27,7 +27,7 @@
$: allowDeletion = !external || table?.created
function showDeleteModal() {
templateScreens = $allScreens.filter(
templateScreens = $store.screens.filter(
screen => screen.autoTableId === table._id
)
willBeDeleted = ["All table data"].concat(

View file

@ -1,5 +1,5 @@
<script>
import { Icon } from "@budibase/bbui"
import { Icon, StatusLight } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte"
export let icon
@ -13,6 +13,9 @@
export let draggable = false
export let iconText
export let iconColor
export let scrollable = false
export let color
export let highlighted = false
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher()
@ -43,7 +46,10 @@
class="nav-item"
class:border
class:selected
style={`padding-left: ${20 + indentLevel * 14}px`}
class:withActions
class:scrollable
class:highlighted
style={`padding-left: calc(${indentLevel * 14}px)`}
{draggable}
on:dragend
on:dragstart
@ -55,7 +61,13 @@
>
<div class="nav-item-content" bind:this={contentRef}>
{#if withArrow}
<div class:opened class="icon arrow" on:click={onIconClick}>
<div
class:opened
class:relative={indentLevel === 0}
class:absolute={indentLevel > 0}
class="icon arrow"
on:click={onIconClick}
>
<Icon size="S" name="ChevronRight" />
</div>
{/if}
@ -76,6 +88,11 @@
<slot />
</div>
{/if}
{#if color}
<div class="light">
<StatusLight size="L" {color} />
</div>
{/if}
</div>
</div>
@ -85,24 +102,31 @@
color: var(--grey-7);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
padding: 0 var(--spacing-l) 0;
height: 32px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.nav-item.scrollable {
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.nav-item.highlighted {
background-color: var(--spectrum-global-color-gray-200);
}
.nav-item.selected {
background-color: var(--grey-2);
background-color: var(--spectrum-global-color-gray-300);
color: var(--ink);
}
.nav-item:hover {
background-color: var(--grey-3);
background-color: var(--spectrum-global-color-gray-300);
}
.nav-item:hover .actions {
visibility: visible;
}
.nav-item-content {
flex: 1 1 auto;
display: flex;
@ -111,51 +135,84 @@
align-items: center;
gap: var(--spacing-xs);
width: max-content;
overflow: hidden;
position: relative;
padding-left: var(--spacing-l);
pointer-events: none;
}
/* Needed to fully display the actions icon */
.nav-item.scrollable .nav-item-content {
padding-right: 1px;
}
.icon {
font-size: 16px;
flex: 0 0 20px;
flex: 0 0 24px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--spectrum-global-color-gray-600);
}
.icon.arrow {
margin: 0 -2px 0 -6px;
font-size: 12px;
flex: 0 0 20px;
pointer-events: all;
}
.icon.arrow.absolute {
position: absolute;
left: 0;
padding: 8px;
margin-left: -8px;
}
.icon.arrow :global(svg) {
width: 12px;
height: 12px;
}
.icon.arrow.relative {
position: relative;
margin: 0 -6px 0 -4px;
}
.icon.arrow.opened {
transform: rotate(90deg);
}
.icon + .icon {
margin-left: -4px;
}
.text {
font-weight: 600;
font-size: var(--spectrum-global-dimension-font-size-75);
white-space: nowrap;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
flex: 0 0 auto;
}
.actions {
visibility: hidden;
width: 20px;
height: 20px;
cursor: pointer;
position: relative;
display: grid;
margin-left: var(--spacing-s);
place-items: center;
}
.iconText {
margin-top: 1px;
font-size: var(--spectrum-global-dimension-font-size-50);
flex: 0 0 34px;
}
.text {
font-weight: 600;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
color: var(--spectrum-global-color-gray-800);
}
.scrollable .text {
flex: 0 0 auto;
max-width: 160px;
}
.actions {
cursor: pointer;
position: relative;
display: grid;
place-items: center;
visibility: hidden;
}
.actions,
.light :global(.spectrum-StatusLight) {
width: 20px;
height: 20px;
margin-left: var(--spacing-s);
}
.light {
position: absolute;
right: 0;
}
.nav-item.withActions:hover .light {
display: none;
}
</style>

View file

@ -55,7 +55,7 @@
}
</script>
<Button secondary on:click={publishModal.show}>Publish</Button>
<Button cta on:click={publishModal.show}>Publish</Button>
<Modal bind:this={feedbackModal}>
<ModalContent
title="Enjoying Budibase?"

View file

@ -28,7 +28,13 @@
}
</script>
<Icon name="Revert" hoverable on:click={revertModal.show} />
<Icon
name="Revert"
hoverable
on:click={revertModal.show}
tooltip="Revert changes"
dataCy="revert-application-topnav"
/>
<Modal bind:this={revertModal}>
<ModalContent
title="Revert Changes"

View file

@ -1,54 +0,0 @@
<script>
import { notifications, Select } from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
const themeOptions = [
{
label: "Lightest",
value: "spectrum--lightest",
},
{
label: "Light",
value: "spectrum--light",
},
{
label: "Dark",
value: "spectrum--dark",
},
{
label: "Darkest",
value: "spectrum--darkest",
},
]
const onChangeTheme = async theme => {
try {
await store.actions.theme.save(theme)
await store.actions.customTheme.save({
...get(store).customTheme,
navBackground:
theme === "spectrum--light"
? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)",
})
} catch (error) {
notifications.error("Error updating theme")
}
}
</script>
<div>
<Select
value={$store.theme}
options={themeOptions}
placeholder={null}
on:change={e => onChangeTheme(e.detail)}
/>
</div>
<style>
div {
width: 100px;
}
</style>

View file

@ -1,114 +0,0 @@
<script>
import {
ActionMenu,
ActionButton,
MenuItem,
Icon,
notifications,
} from "@budibase/bbui"
import { store, currentAssetName, selectedComponent } from "builderStore"
import structure from "./componentStructure.json"
$: enrichedStructure = enrichStructure(structure, $store.components)
const isChildAllowed = ({ name }, selectedComponent) => {
const currentComponent = store.actions.components.getDefinition(
selectedComponent?._component
)
return currentComponent?.illegalChildren?.includes(name.toLowerCase())
}
const enrichStructure = (structure, definitions) => {
let enrichedStructure = []
structure.forEach(item => {
if (typeof item === "string") {
const def = definitions[`@budibase/standard-components/${item}`]
if (def) {
enrichedStructure.push({
...def,
isCategory: false,
})
}
} else {
enrichedStructure.push({
...item,
isCategory: true,
children: enrichStructure(item.children || [], definitions),
})
}
})
return enrichedStructure
}
const onItemChosen = async item => {
if (!item.isCategory) {
try {
await store.actions.components.create(item.component)
} catch (error) {
notifications.error("Error creating component")
}
}
}
</script>
<div class="components">
{#each enrichedStructure as item}
<ActionMenu disabled={!item.isCategory}>
<ActionButton
icon={item.icon}
disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)}
quiet
size="S"
slot="control"
dataCy={`category-${item.name}`}
on:click={() => onItemChosen(item)}
>
<div class="buttonContent">
{item.name}
{#if item.isCategory}
<Icon size="S" name="ChevronDown" />
{/if}
</div>
</ActionButton>
{#each item.children || [] as item}
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
<MenuItem
dataCy={`component-${item.name}`}
icon={item.icon}
on:click={() => onItemChosen(item)}
disabled={isChildAllowed(item, $selectedComponent)}
>
{item.name}
</MenuItem>
{/if}
{/each}
</ActionMenu>
{/each}
</div>
<style>
.components {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
}
.components :global(> *) {
height: 32px;
display: grid;
place-items: center;
}
.buttonContent {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-end;
}
.buttonContent :global(svg) {
margin-left: 2px !important;
margin-right: 0 !important;
margin-bottom: -1px;
}
</style>

View file

@ -1,156 +0,0 @@
<script>
import { get } from "svelte/store"
import {
ActionButton,
Modal,
ModalContent,
Layout,
ColorPicker,
Label,
Select,
Button,
notifications,
} from "@budibase/bbui"
import { store } from "builderStore"
import AppThemeSelect from "./AppThemeSelect.svelte"
let modal
const defaultTheme = {
primaryColor: "var(--spectrum-global-color-blue-600)",
primaryColorHover: "var(--spectrum-global-color-blue-500)",
buttonBorderRadius: "16px",
navBackground: "var(--spectrum-global-color-gray-50)",
navTextColor: "var(--spectrum-global-color-gray-800)",
}
const buttonBorderRadiusOptions = [
{
label: "None",
value: "0",
},
{
label: "Small",
value: "4px",
},
{
label: "Medium",
value: "8px",
},
{
label: "Large",
value: "16px",
},
]
const updateProperty = property => {
return async e => {
try {
store.actions.customTheme.save({
...get(store).customTheme,
[property]: e.detail,
})
} catch (error) {
notifications.error("Error updating custom theme")
}
}
}
const resetTheme = () => {
try {
const theme = get(store).theme
store.actions.customTheme.save({
...defaultTheme,
navBackground:
theme === "spectrum--light"
? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)",
})
} catch (error) {
notifications.error("Error saving custom theme")
}
}
</script>
<div class="container">
<ActionButton icon="Brush" on:click={modal.show}>Theme</ActionButton>
</div>
<Modal bind:this={modal}>
<ModalContent
showConfirmButton={false}
cancelText="View changes"
showCloseIcon={false}
title="Theme settings"
>
<Layout noPadding gap="S">
<div class="setting">
<Label size="L">Theme</Label>
<AppThemeSelect />
</div>
<div class="setting">
<Label size="L">Button roundness</Label>
<div class="select-wrapper">
<Select
placeholder={null}
value={$store.customTheme?.buttonBorderRadius ||
defaultTheme.buttonBorderRadius}
on:change={updateProperty("buttonBorderRadius")}
options={buttonBorderRadiusOptions}
/>
</div>
</div>
<div class="setting">
<Label size="L">Accent color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor}
on:change={updateProperty("primaryColor")}
/>
</div>
<div class="setting">
<Label size="L">Accent color (hover)</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.customTheme?.primaryColorHover ||
defaultTheme.primaryColorHover}
on:change={updateProperty("primaryColorHover")}
/>
</div>
<div class="setting">
<Label size="L">Navigation bar background color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.customTheme?.navBackground ||
defaultTheme.navBackground}
on:change={updateProperty("navBackground")}
/>
</div>
<div class="setting">
<Label size="L">Navigation bar text color</Label>
<ColorPicker
spectrumTheme={$store.theme}
value={$store.customTheme?.navTextColor || defaultTheme.navTextColor}
on:change={updateProperty("navTextColor")}
/>
</div>
</Layout>
<div slot="footer">
<Button secondary quiet on:click={resetTheme}>Reset</Button>
</div>
</ModalContent>
</Modal>
<style>
.container {
padding-right: 8px;
}
.setting {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.select-wrapper {
width: 100px;
}
</style>

View file

@ -1 +0,0 @@
export { default } from "./CurrentItemPreview.svelte"

View file

@ -1,157 +0,0 @@
<script>
import { store } from "builderStore"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers"
import { notifications } from "@budibase/bbui"
import { selectedComponentPath } from "builderStore"
export let components = []
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let dragDropStore
let closedNodes = {}
const selectComponent = component => {
store.actions.components.select(component)
}
const dragstart = component => e => {
e.dataTransfer.dropEffect = DropEffect.MOVE
dragDropStore.actions.dragstart(component)
}
const dragover = (component, index) => e => {
const definition = store.actions.components.getDefinition(
component._component
)
const canHaveChildrenButIsEmpty =
definition?.hasChildren && !component._children?.length
e.dataTransfer.dropEffect = DropEffect.COPY
// how far down the mouse pointer is on the drop target
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
dragDropStore.actions.dragover({
component,
index,
canHaveChildrenButIsEmpty,
mousePosition,
})
return false
}
const getComponentText = component => {
if (component._instanceName) {
return component._instanceName
}
const type =
component._component.replace("@budibase/standard-components/", "") ||
"component"
return capitalise(type)
}
function toggleNodeOpen(componentId) {
if (closedNodes[componentId]) {
delete closedNodes[componentId]
} else {
closedNodes[componentId] = true
}
closedNodes = closedNodes
}
const onDrop = async () => {
try {
await dragDropStore.actions.drop()
} catch (error) {
notifications.error("Error saving component")
}
}
const isOpen = (component, selectedComponentPath, closedNodes) => {
if (!component?._children?.length) {
return false
}
if (selectedComponentPath.includes(component._id)) {
return true
}
return !closedNodes[component._id]
}
</script>
<ul>
{#each components || [] as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
<div
on:drop={onDrop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level + 1) * 16}px"
/>
{/if}
<NavItem
draggable
on:dragend={dragDropStore.actions.reset}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={onDrop}
text={getComponentText(component)}
withArrow
indentLevel={level + 1}
selected={$store.selectedComponentId === component._id}
opened={isOpen(component, $selectedComponentPath, closedNodes)}
>
<ComponentDropdownMenu {component} />
</NavItem>
{#if isOpen(component, $selectedComponentPath, closedNodes)}
<svelte:self
components={component._children}
{currentComponent}
{onSelect}
{dragDropStore}
level={level + 1}
/>
{/if}
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)}
<div
on:drop={onDrop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level +
($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) *
16}px"
/>
{/if}
</li>
{/each}
</ul>
<style>
ul {
list-style: none;
padding-left: 0;
margin: 0;
}
ul,
li {
min-width: max-content;
}
.drop-item {
border-radius: var(--border-radius-m);
height: 32px;
background: var(--grey-3);
}
</style>

View file

@ -1,74 +0,0 @@
<script>
import { store } from "builderStore"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ActionMenu,
MenuItem,
Icon,
Modal,
ModalContent,
Input,
} from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
export let layout
let confirmDeleteDialog
let editLayoutNameModal
let name = layout.name
const deleteLayout = async () => {
try {
await store.actions.layouts.delete(layout)
notifications.success("Layout deleted successfully")
} catch (err) {
notifications.error("Error deleting layout")
}
}
const saveLayout = async () => {
try {
const layoutToSave = cloneDeep(layout)
layoutToSave.name = name
await store.actions.layouts.save(layoutToSave)
notifications.success("Layout saved successfully")
} catch (err) {
notifications.error("Error saving layout")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Edit" on:click={editLayoutNameModal.show}>Edit</MenuItem>
<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}
/>
<Modal bind:this={editLayoutNameModal}>
<ModalContent
title="Edit Layout Name"
confirmText="Save"
onConfirm={saveLayout}
disabled={!name}
>
<Input thin type="text" label="Name" bind:value={name} />
</ModalContent>
</Modal>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View file

@ -1,82 +0,0 @@
<script>
import { goto } from "@roxi/routify"
import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ActionMenu,
MenuItem,
Icon,
Layout,
notifications,
} from "@budibase/bbui"
import { get } from "svelte/store"
export let path
export let screens
let confirmDeleteDialog
const deleteScreens = async () => {
if (!screens?.length) {
return
}
try {
for (let { id } of screens) {
// We have to fetch the screen to be deleted immediately before deleting
// as otherwise we're very likely to 409
const screen = get(store).screens.find(screen => screen._id === id)
if (!screen) {
continue
}
await store.actions.screens.delete(screen)
}
notifications.success("Screens deleted successfully")
$goto("../")
} catch (error) {
notifications.error("Error deleting screens")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
Delete all screens
</MenuItem>
</ActionMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
okText="Delete screens"
onOk={deleteScreens}
>
<Layout noPadding gap="S">
<div>
Are you sure you want to delete all screens under the <b>{path}</b> route?
</div>
<div>The following screens will be deleted:</div>
<div class="to-delete">
{#each screens as screen}
<div>{screen.route}</div>
{/each}
</div>
</Layout>
</ConfirmDialog>
<style>
.to-delete {
font-weight: bold;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: var(--spacing-xl);
}
.icon {
display: grid;
place-items: center;
}
</style>

View file

@ -1,106 +0,0 @@
<script>
import {
store,
selectedComponent,
currentAsset,
screenSearchString,
} from "builderStore"
import instantiateStore from "./dragDropStore"
import ComponentTree from "./ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import PathDropdownMenu from "./PathDropdownMenu.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { get } from "svelte/store"
const ROUTE_NAME_MAP = {
"/": {
BASIC: "Home",
PUBLIC: "Home",
ADMIN: "Home",
POWER: "Home",
},
}
const dragDropStore = instantiateStore()
export let route
export let path
export let indent
export let border
let routeManuallyOpened = false
$: selectedScreen = $currentAsset
$: allScreens = getAllScreens(route)
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
$: hasSearchMatch = $screenSearchString && filteredScreens.length > 0
$: noSearchMatch = $screenSearchString && !filteredScreens.length
$: routeSelected =
route.subpaths[selectedScreen?.routing?.route] !== undefined
$: routeOpened = routeManuallyOpened || routeSelected || hasSearchMatch
const changeScreen = screenId => {
store.actions.screens.select(screenId)
}
const getAllScreens = route => {
let screens = []
Object.entries(route.subpaths).forEach(([route, subpath]) => {
Object.entries(subpath.screens).forEach(([role, id]) => {
screens.push({ id, route, role })
})
})
return screens
}
const getFilteredScreens = (screens, searchString) => {
return screens.filter(
screen => !searchString || screen.route.includes(searchString)
)
}
const toggleManuallyOpened = () => {
if (get(screenSearchString)) {
return
}
routeManuallyOpened = !routeManuallyOpened
}
</script>
{#if !noSearchMatch}
<NavItem
icon="FolderOutline"
text={path}
on:click={toggleManuallyOpened}
opened={routeOpened}
{border}
withArrow={route.subpaths}
>
<PathDropdownMenu screens={allScreens} {path} />
</NavItem>
{#if routeOpened}
{#each filteredScreens as screen (screen.id)}
<NavItem
icon="WebPage"
indentLevel={indent || 1}
selected={$store.selectedScreenId === screen.id &&
$store.currentView === "detail"}
opened={$store.selectedScreenId === screen.id}
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
withArrow={route.subpaths}
on:click={() => changeScreen(screen.id)}
>
<ScreenDropdownMenu screenId={screen.id} />
</NavItem>
{#if selectedScreen?._id === screen.id}
<ComponentTree
level={1}
components={selectedScreen.props._children}
currentComponent={$selectedComponent}
{dragDropStore}
/>
{/if}
{/each}
{/if}
{/if}

View file

@ -1,104 +0,0 @@
import { writable, get } from "svelte/store"
import { store as frontendStore } from "builderStore"
import { findComponentPath } from "builderStore/componentUtils"
export const DropEffect = {
MOVE: "move",
COPY: "copy",
}
export const DropPosition = {
ABOVE: "above",
BELOW: "below",
INSIDE: "inside",
}
export default function () {
const store = writable({})
store.actions = {
dragstart: component => {
store.update(state => {
state.dragged = component
return state
})
},
dragover: ({
component,
index,
canHaveChildrenButIsEmpty,
mousePosition,
}) => {
store.update(state => {
state.targetComponent = component
// only allow dropping inside when container is empty
// if container has children, drag over them
if (canHaveChildrenButIsEmpty && index === 0) {
// hovered above center of target
if (mousePosition < 0.4) {
state.dropPosition = DropPosition.ABOVE
}
// hovered around bottom of target
if (mousePosition > 0.8) {
state.dropPosition = DropPosition.BELOW
}
// hovered in center of target
if (mousePosition > 0.4 && mousePosition < 0.8) {
state.dropPosition = DropPosition.INSIDE
}
return state
}
// bottom half
if (mousePosition > 0.5) {
state.dropPosition = DropPosition.BELOW
} else {
state.dropPosition = canHaveChildrenButIsEmpty
? DropPosition.INSIDE
: DropPosition.ABOVE
}
return state
})
},
reset: () => {
store.update(state => {
state.dropPosition = ""
state.targetComponent = null
state.dragged = null
return state
})
},
drop: async () => {
const state = get(store)
// Stop if the target and source are the same
if (state.targetComponent === state.dragged) {
return
}
// Stop if the target or source are null
if (!state.targetComponent || !state.dragged) {
return
}
// Stop if the target is a child of source
const path = findComponentPath(state.dragged, state.targetComponent._id)
const ids = path.map(component => component._id)
if (ids.includes(state.targetComponent._id)) {
return
}
// Cut and paste the component
frontendStore.actions.components.copy(state.dragged, true)
await frontendStore.actions.components.paste(
state.targetComponent,
state.dropPosition
)
store.actions.reset()
},
}
return store
}

View file

@ -1,83 +0,0 @@
<script>
import { store, selectedAccessRole } from "builderStore"
import PathTree from "./PathTree.svelte"
let routes = {}
let paths = []
$: allRoutes = $store.routes
$: selectedScreenId = $store.selectedScreenId
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId)
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => {
const sortedPaths = Object.keys(allRoutes || {}).sort()
let found = false
let firstValidScreenId
let filteredRoutes = {}
let screenRoleId
// Filter all routes down to only those which match the current role
sortedPaths.forEach(path => {
const config = allRoutes[path]
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => {
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => {
if (screenId === selectedScreenId) {
screenRoleId = roleId
found = roleId === selectedRoleId
}
if (roleId === selectedRoleId) {
if (!firstValidScreenId) {
firstValidScreenId = screenId
}
if (!filteredRoutes[path]) {
filteredRoutes[path] = { subpaths: {} }
}
filteredRoutes[path].subpaths[subpath] = {
screens: {
[selectedRoleId]: screenId,
},
}
}
})
})
})
routes = { ...filteredRoutes }
paths = Object.keys(routes || {}).sort()
// Select the correct role for the current screen ID
if (!found && screenRoleId) {
selectedAccessRole.set(screenRoleId)
if (screenRoleId !== selectedRoleId) {
updatePaths(allRoutes, screenRoleId, selectedScreenId)
}
}
// If the selected screen isn't in this filtered list, select the first one
else if (!found && firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
}
</script>
<div class="root" class:has-screens={!!paths?.length}>
{#each paths as path, idx (path)}
<PathTree border={idx > 0} {path} route={routes[path]} />
{/each}
{#if !paths.length}
<div class="empty">
There aren't any screens configured with this access role.
</div>
{/if}
</div>
<style>
.root.has-screens {
min-width: max-content;
}
div.empty {
font-size: var(--font-size-s);
color: var(--grey-5);
padding: var(--spacing-xs) var(--spacing-xl);
}
</style>

View file

@ -1,229 +0,0 @@
<script>
import { onMount, setContext } from "svelte"
import { goto, params } from "@roxi/routify"
import {
store,
allScreens,
selectedAccessRole,
screenSearchString,
} from "builderStore"
import { roles } from "stores/backend"
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.svelte"
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
import {
Icon,
Modal,
Select,
Search,
Tabs,
Tab,
Layout as BBUILayout,
notifications,
} from "@budibase/bbui"
export let showModal
let scrollRef
const scrollTo = bounds => {
if (!bounds) {
return
}
const sidebarWidth = 259
const navItemHeight = 32
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
let scrollBounds = scrollRef.getBoundingClientRect()
let newOffsets = {}
// Calculate left offset
const offsetX = bounds.left + bounds.width + scrollLeft + 20
if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth
} else {
newOffsets.left = 0
}
if (newOffsets.left === scrollLeft) {
delete newOffsets.left
}
// Calculate top offset
const offsetY = bounds.top - scrollBounds?.top + scrollTop
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
} else if (offsetY < scrollTop + navItemHeight) {
newOffsets.top = offsetY - navItemHeight
} else {
delete newOffsets.top
}
// Skip if offset is unchanged
if (newOffsets.left == null && newOffsets.top == null) {
return
}
// Smoothly scroll to the offset
scrollRef.scroll({
...newOffsets,
behavior: "smooth",
})
}
setContext("scroll", {
scrollTo,
})
const tabs = [
{
title: "Screens",
key: "screen",
},
{
title: "Layouts",
key: "layout",
},
]
let newLayoutModal
$: selected = tabs.find(t => t.key === $params.assetType)?.title || "Screens"
const navigate = ({ detail }) => {
const { key } = tabs.find(t => t.title === detail)
$goto(`../${key}`)
}
const updateAccessRole = event => {
const role = event.detail
// Select a valid screen with this new role - otherwise we'll not be
// able to change role at all because ComponentNavigationTree will kick us
// back the current role again because the same screen ID is still selected
const firstValidScreenId = $allScreens.find(
screen => screen.routing.roleId === role
)?._id
if (firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
// Otherwise clear the selected screen ID so that the first new valid screen
// can be selected by ComponentNavigationTree
else {
store.update(state => {
state.selectedScreenId = null
return state
})
}
selectedAccessRole.set(role)
}
onMount(async () => {
try {
await store.actions.routing.fetch()
} catch (error) {
notifications.error("Error fetching routes")
}
})
</script>
<div class="title">
<Tabs {selected} on:select={navigate}>
<Tab title="Screens">
<div class="tab-content-padding">
<BBUILayout noPadding gap="XS">
<Select
on:change={updateAccessRole}
value={$selectedAccessRole}
label="Filter by Access"
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
options={$roles}
/>
<Search
placeholder="Enter a route to search"
label="Search Screens"
bind:value={$screenSearchString}
/>
</BBUILayout>
<div class="nav-items-container" bind:this={scrollRef}>
<ComponentNavigationTree />
</div>
</div>
</Tab>
<Tab title="Layouts">
<div class="tab-content-padding">
<div
class="nav-items-container nav-items-container--layouts"
bind:this={scrollRef}
>
<div class="layouts-container">
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
</div>
</div>
<Modal bind:this={newLayoutModal}>
<NewLayoutModal />
</Modal>
</div>
</Tab>
</Tabs>
<div class="add-button">
<Icon
hoverable
name="AddCircle"
on:click={selected === "Layouts" ? newLayoutModal.show() : showModal()}
/>
</div>
</div>
<style>
.title {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
flex: 1 1 auto;
}
.title :global(.spectrum-Tabs-content),
.title :global(.spectrum-Tabs-content > div),
.title :global(.spectrum-Tabs-content > div > div) {
height: 100%;
}
.add-button {
position: absolute;
top: var(--spacing-l);
right: var(--spacing-xl);
}
.tab-content-padding {
padding: 0 var(--spacing-xl);
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.nav-items-container {
border-top: var(--border-light);
margin: 0 calc(-1 * var(--spacing-xl));
padding: var(--spacing-m) 0;
flex: 1 1 auto;
overflow: auto;
height: 0;
position: relative;
}
.nav-items-container--layouts {
border-top: none;
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
}
.layouts-container {
min-width: max-content;
}
</style>

View file

@ -1,36 +0,0 @@
<script>
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte"
import initDragDropStore from "./ComponentNavigationTree/dragDropStore"
import NavItem from "components/common/NavItem.svelte"
import { store, selectedComponent } from "builderStore"
export let layout
export let border
const dragDropStore = initDragDropStore()
const selectLayout = () => {
store.actions.layouts.select(layout._id)
}
</script>
<NavItem
{border}
icon="ClassicGridView"
text={layout.name}
withArrow
selected={$store.selectedLayoutId === layout._id}
opened={$store.selectedLayoutId === layout._id}
on:click={selectLayout}
>
<LayoutDropdownMenu {layout} />
</NavItem>
{#if $store.selectedLayoutId === layout._id && layout.props?._children}
<ComponentTree
components={layout.props._children}
currentComponent={$selectedComponent}
{dragDropStore}
/>
{/if}

View file

@ -1,93 +0,0 @@
<script>
import { ModalContent, Body, Detail } from "@budibase/bbui"
export let selectedScreens
export let chooseModal
export let save
let selectedNav
let createdScreens = []
$: blankSelected = selectedScreens.length === 1
</script>
<ModalContent
title="Select navigation"
cancelText="Back"
onCancel={() => (blankSelected ? chooseModal(1) : chooseModal(0))}
size="M"
onConfirm={() => {
save(createdScreens)
}}
disabled={!selectedNav}
>
<Body size="S"
>Please select your preferred layout for the new application:</Body
>
<div class="wrapper">
<div
data-cy="left-nav"
on:click={() => (selectedNav = "Left")}
class:unselected={selectedNav && selectedNav !== "Left"}
>
<div class="box">
<div class="side-nav" />
</div>
<div><Detail>Side Nav</Detail></div>
</div>
<div
on:click={() => (selectedNav = "Top")}
class:unselected={selectedNav && selectedNav !== "Top"}
>
<div class="box">
<div class="top-nav" />
</div>
<div><Detail>Top Nav</Detail></div>
</div>
<div
on:click={() => (selectedNav = "None")}
class:unselected={selectedNav && selectedNav !== "None"}
>
<div class="box" />
<div><Detail>No Nav</Detail></div>
</div>
</div>
</ModalContent>
<style>
.side-nav {
float: left;
background: #d3d3d3 0% 0% no-repeat padding-box;
border-radius: 2px 0px 0px 2px;
height: 100%;
width: 10%;
}
.top-nav {
background: #d3d3d3 0% 0% no-repeat padding-box;
vertical-align: top;
width: 100%;
height: 15%;
}
.box {
display: inline-block;
background: #eaeaea 0% 0% no-repeat padding-box;
border: 1px solid #d3d3d3;
border-radius: 2px;
opacity: 1;
width: 120px;
height: 70px;
margin-right: 20px;
}
.wrapper {
display: flex;
padding-top: 4%;
list-style-type: none;
margin: 0;
padding: 0;
margin-right: 5%;
}
.unselected {
opacity: 0.3;
}
</style>

View file

@ -1,20 +0,0 @@
<script>
import { notifications } from "@budibase/bbui"
import { store } from "builderStore"
import { Input, ModalContent } from "@budibase/bbui"
let name = ""
async function save() {
try {
await store.actions.layouts.save({ name })
notifications.success(`Layout ${name} created successfully`)
} catch (error) {
notifications.error("Error creating layout")
}
}
</script>
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}>
<Input thin label="Name" bind:value={name} />
</ModalContent>

View file

@ -0,0 +1,112 @@
<script>
import { Icon, Heading } from "@budibase/bbui"
export let title
export let icon
export let showAddButton = false
export let showBackButton = false
export let showExpandIcon = false
export let onClickAddButton
export let onClickBackButton
export let borderLeft = false
export let borderRight = false
let wide = false
</script>
<div class="panel" class:wide class:borderLeft class:borderRight>
<div class="header">
{#if showBackButton}
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
{/if}
{#if icon}
<Icon name={icon} />
{/if}
<div class="title">
<Heading size="XXS">{title || ""}</Heading>
</div>
{#if showExpandIcon}
<Icon
name={wide ? "Minimize" : "Maximize"}
hoverable
on:click={() => (wide = !wide)}
/>
{/if}
{#if showAddButton}
<div class="add-button" on:click={onClickAddButton}>
<Icon name="Add" />
</div>
{/if}
</div>
<div class="body">
<slot />
</div>
</div>
<style>
.panel {
width: 260px;
background: var(--background);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
transition: width 130ms ease-out;
}
.panel.borderLeft {
border-left: var(--border-light);
}
.panel.borderRight {
border-right: var(--border-light);
}
.panel.wide {
width: 420px;
}
.header {
flex: 0 0 48px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 var(--spacing-l);
border-bottom: var(--border-light);
gap: var(--spacing-l);
}
.title {
flex: 1 1 auto;
width: 0;
}
.title :global(h1) {
overflow: hidden;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.add-button {
flex: 0 0 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 4px;
position: relative;
cursor: pointer;
background: var(--spectrum-semantic-cta-color-background-default);
transition: background var(--spectrum-global-animation-duration-100, 130ms)
ease-out;
}
.add-button:hover {
background: var(--spectrum-semantic-cta-color-background-hover);
}
.add-button :global(svg) {
fill: white;
}
.body {
flex: 1 1 auto;
overflow: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View file

@ -1,63 +0,0 @@
<script>
import { store, selectedComponent, currentAsset } from "builderStore"
import { Tabs, Tab } from "@budibase/bbui"
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
import {
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding"
$: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
$: componentBindings = getComponentBindableProperties(
$currentAsset,
$store.selectedComponentId
)
</script>
<Tabs selected="Settings" noPadding>
<Tab title="Settings">
<div class="container">
{#key componentInstance?._id}
<ScreenSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
{componentBindings}
/>
<DesignSection {componentInstance} {componentDefinition} {bindings} />
<CustomStylesSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ConditionalUISection
{componentInstance}
{componentDefinition}
{bindings}
/>
{/key}
</div>
</Tab>
</Tabs>
<style>
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View file

@ -1,25 +0,0 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import NavigationDrawer from "./NavigationDrawer.svelte"
import { cloneDeep } from "lodash/fp"
export let value = []
let drawer
let links = cloneDeep(value || [])
const dispatch = createEventDispatcher()
const save = () => {
dispatch("change", links)
drawer.hide()
}
</script>
<ActionButton on:click={drawer.show}>Configure links</ActionButton>
<Drawer bind:this={drawer} title={"Navigation Links"}>
<svelte:fragment slot="description">
Configure the links in your navigation bar.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<NavigationDrawer slot="body" bind:links />
</Drawer>

View file

@ -1,68 +0,0 @@
import { Checkbox, Select, Stepper } from "@budibase/bbui"
import DataSourceSelect from "./DataSourceSelect.svelte"
import S3DataSourceSelect from "./S3DataSourceSelect.svelte"
import DataProviderSelect from "./DataProviderSelect.svelte"
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
import TableSelect from "./TableSelect.svelte"
import ColorPicker from "./ColorPicker.svelte"
import { IconSelect } from "./IconSelect"
import FieldSelect from "./FieldSelect.svelte"
import MultiFieldSelect from "./MultiFieldSelect.svelte"
import SearchFieldSelect from "./SearchFieldSelect.svelte"
import SchemaSelect from "./SchemaSelect.svelte"
import SectionSelect from "./SectionSelect.svelte"
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
import URLSelect from "./URLSelect.svelte"
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
import ColumnEditor from "./ColumnEditor/ColumnEditor.svelte"
const componentMap = {
text: DrawerBindableCombobox,
select: Select,
dataSource: DataSourceSelect,
"dataSource/s3": S3DataSourceSelect,
dataProvider: DataProviderSelect,
boolean: Checkbox,
number: Stepper,
event: ButtonActionEditor,
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: FieldSelect,
multifield: MultiFieldSelect,
searchfield: SearchFieldSelect,
options: OptionsEditor,
schema: SchemaSelect,
section: SectionSelect,
navigation: NavigationEditor,
filter: FilterEditor,
url: URLSelect,
columns: ColumnEditor,
"field/string": FormFieldSelect,
"field/number": FormFieldSelect,
"field/options": FormFieldSelect,
"field/boolean": FormFieldSelect,
"field/longform": FormFieldSelect,
"field/datetime": FormFieldSelect,
"field/attachment": FormFieldSelect,
"field/link": FormFieldSelect,
"field/array": FormFieldSelect,
"field/json": FormFieldSelect,
// Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor,
"validation/array": ValidationEditor,
"validation/number": ValidationEditor,
"validation/boolean": ValidationEditor,
"validation/datetime": ValidationEditor,
"validation/attachment": ValidationEditor,
"validation/link": ValidationEditor,
}
export const getComponentForSettingType = type => {
return componentMap[type]
}

View file

@ -1,125 +0,0 @@
<script>
import { get } from "svelte/store"
import { get as deepGet, setWith } from "lodash"
import { Input, DetailSummary, notifications } from "@budibase/bbui"
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import { currentAsset, store } from "builderStore"
import { FrontendTypes } from "constants"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { allScreens, selectedAccessRole } from "builderStore"
export let componentInstance
export let bindings
let errors = {}
const routeTaken = url => {
const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
const roleTaken = roleId => {
const url = get(currentAsset)?.routing.route
return get(allScreens).some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
const setAssetProps = (name, value, parser, validate) => {
if (parser) {
value = parser(value)
}
if (validate) {
const error = validate(value)
errors = {
...errors,
[name]: error,
}
if (error) {
return
}
} else {
errors = {
...errors,
[name]: null,
}
}
const selectedAsset = get(currentAsset)
store.update(state => {
if (
name === "_instanceName" &&
state.currentFrontEndType === FrontendTypes.SCREEN
) {
selectedAsset.props._instanceName = value
} else {
setWith(selectedAsset, name.split("."), value, Object)
}
return state
})
try {
store.actions.preview.saveSelected()
} catch (error) {
notifications.error("Error saving settings")
}
}
const screenSettings = [
{
key: "routing.route",
label: "Route",
control: Input,
parser: val => {
if (!val.startsWith("/")) {
val = "/" + val
}
return sanitizeUrl(val)
},
validate: val => {
const exisingValue = get(currentAsset)?.routing.route
if (val !== exisingValue && routeTaken(val)) {
return "That URL is already in use for this role"
}
return null
},
},
{
key: "routing.roleId",
label: "Access",
control: RoleSelect,
validate: val => {
const exisingValue = get(currentAsset)?.routing.roleId
if (val !== exisingValue && roleTaken(val)) {
return "That role is already in use for this URL"
}
return null
},
},
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
</script>
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN}
<DetailSummary name="Screen" collapsible={false}>
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
control={def.control}
label={def.label}
key={def.key}
value={deepGet($currentAsset, def.key)}
onChange={val => setAssetProps(def.key, val, def.parser, def.validate)}
{bindings}
props={{ error: errors[def.key] }}
/>
{/each}
</DetailSummary>
{/if}

View file

@ -0,0 +1,77 @@
import { Checkbox, Select, Stepper } from "@budibase/bbui"
import DataSourceSelect from "./controls/DataSourceSelect.svelte"
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte"
import DataProviderSelect from "./controls/DataProviderSelect.svelte"
import ButtonActionEditor from "./controls/ButtonActionEditor/ButtonActionEditor.svelte"
import TableSelect from "./controls/TableSelect.svelte"
import ColorPicker from "./controls/ColorPicker.svelte"
import { IconSelect } from "./controls/IconSelect"
import FieldSelect from "./controls/FieldSelect.svelte"
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte"
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte"
import SchemaSelect from "./controls/SchemaSelect.svelte"
import SectionSelect from "./controls/SectionSelect.svelte"
import FilterEditor from "./controls/FilterEditor/FilterEditor.svelte"
import URLSelect from "./controls/URLSelect.svelte"
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BarButtonList from "./controls/BarButtonList.svelte"
const componentMap = {
text: DrawerBindableCombobox,
select: Select,
dataSource: DataSourceSelect,
"dataSource/s3": S3DataSourceSelect,
dataProvider: DataProviderSelect,
boolean: Checkbox,
number: Stepper,
event: ButtonActionEditor,
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: FieldSelect,
multifield: MultiFieldSelect,
searchfield: SearchFieldSelect,
options: OptionsEditor,
schema: SchemaSelect,
section: SectionSelect,
filter: FilterEditor,
url: URLSelect,
columns: ColumnEditor,
"field/string": FormFieldSelect,
"field/number": FormFieldSelect,
"field/options": FormFieldSelect,
"field/boolean": FormFieldSelect,
"field/longform": FormFieldSelect,
"field/datetime": FormFieldSelect,
"field/attachment": FormFieldSelect,
"field/link": FormFieldSelect,
"field/array": FormFieldSelect,
"field/json": FormFieldSelect,
// Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor,
"validation/array": ValidationEditor,
"validation/number": ValidationEditor,
"validation/boolean": ValidationEditor,
"validation/datetime": ValidationEditor,
"validation/attachment": ValidationEditor,
"validation/link": ValidationEditor,
}
export const getComponentForSetting = setting => {
const { type, showInBar, barStyle } = setting || {}
if (!type) {
return null
}
// We can show a clone of the bar settings for certain select settings
if (showInBar && type === "select" && barStyle === "buttons") {
return BarButtonList
}
return componentMap[type]
}

View file

@ -0,0 +1,18 @@
<script>
import { ActionButton, ActionGroup } from "@budibase/bbui"
export let value
export let onChange
export let options
</script>
<ActionGroup>
{#each options as option}
<ActionButton
icon={option.barIcon}
quiet
on:click={() => onChange(option.value)}
selected={option.value === value}
/>
{/each}
</ActionGroup>

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