Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes

This commit is contained in:
Andrew Kingston 2021-08-25 15:31:30 +01:00
commit ec5c3d48bc
75 changed files with 1019 additions and 419 deletions

View File

@ -1,85 +0,0 @@
# This workflow will build and push a new container image to Amazon ECR,
# and then will deploy a new task definition to Amazon ECS, when a release is created
#
# To use this workflow, you will need to complete the following set-up steps:
#
# 1. Create an ECR repository to store your images.
# For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`.
# Replace the value of `ECR_REPOSITORY` in the workflow below with your repository's name.
# Replace the value of `aws-region` in the workflow below with your repository's region.
#
# 2. Create an ECS task definition, an ECS cluster, and an ECS service.
# For example, follow the Getting Started guide on the ECS console:
# https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun
# Replace the values for `service` and `cluster` in the workflow below with your service and cluster names.
#
# 3. Store your ECS task definition as a JSON file in your repository.
# The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`.
# Replace the value of `task-definition` in the workflow below with your JSON file's name.
# Replace the value of `container-name` in the workflow below with the name of the container
# in the `containerDefinitions` section of the task definition.
#
# 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
# See the documentation for each action used below for the recommended IAM policies for this IAM user,
# and best practices on handling the access key credentials.
on:
push:
tags:
- 'v*'
name: Deploy to Amazon ECS
jobs:
deploy:
name: deploy
runs-on: ubuntu-16.04
steps:
- name: Checkout
uses: actions/checkout@v2
- 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: Download task definition
run: |
aws ecs describe-task-definition --task-definition ProdAppServerStackprodbudiapplbfargateserviceprodbudiappserverfargatetaskdefinition2EF7F1E7 --query taskDefinition > task-definition.json
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: prod-budi-app-server
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS
cd packages/server
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: prod-budi-app-server
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: prod-budi-app-server-service
cluster: prod-budi-app-server
wait-for-service-stability: true

View File

@ -42,6 +42,10 @@ jobs:
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release yarn release
- name: Get Previous tag
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: Build/release Docker images - name: Build/release Docker images
run: | run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
@ -50,10 +54,12 @@ jobs:
env: env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
- uses: azure/setup-helm@v1 - uses: azure/setup-helm@v1
id: install id: install
# So, we need to inject the values into this
- run: yarn release:helm - run: yarn release:helm
- name: Run chart-releaser - name: Run chart-releaser
@ -62,3 +68,4 @@ jobs:
charts_dir: docs charts_dir: docs
env: env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -38,3 +38,4 @@ dependencies:
- name: ingress-nginx - name: ingress-nginx
version: 3.35.0 version: 3.35.0
repository: https://github.com/kubernetes/ingress-nginx repository: https://github.com/kubernetes/ingress-nginx
condition: services.ingress.nginx

View File

@ -0,0 +1,35 @@
{{- if .Values.ingress.aws }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-budibase
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
{{- if .Values.ingress.certificateArn }}
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certificateArn }}
{{- end }}
spec:
rules:
- http:
paths:
{{- if .Values.ingress.certificateArn }}
- path: /
pathType: Prefix
backend:
service:
name: ssl-redirect
port:
name: use-annotation
{{- end }}
- path: /
pathType: Prefix
backend:
service:
name: proxy-service
port:
number: {{ .Values.services.proxy.port }}
{{- end }}

View File

@ -58,6 +58,10 @@ spec:
key: jwtSecret key: jwtSecret
- name: LOG_LEVEL - name: LOG_LEVEL
value: {{ .Values.services.apps.logLevel | default "info" | quote }} value: {{ .Values.services.apps.logLevel | default "info" | quote }}
{{ if .Values.services.objectStore.region }}
- name: AWS_REGION
value: {{ .Values.services.objectStore.region }}
{{ end }}
- name: MINIO_ACCESS_KEY - name: MINIO_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@ -55,6 +55,10 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ template "budibase.fullname" . }} name: {{ template "budibase.fullname" . }}
key: jwtSecret key: jwtSecret
{{ if .Values.services.objectStore.region }}
- name: AWS_REGION
value: {{ .Values.services.objectStore.region }}
{{ end }}
- name: MINIO_ACCESS_KEY - name: MINIO_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@ -41,6 +41,7 @@ service:
ingress: ingress:
enabled: true enabled: true
nginx: true
certificateArn: "" certificateArn: ""
className: "" className: ""
annotations: annotations:
@ -135,6 +136,7 @@ services:
replicaCount: 1 replicaCount: 1
accessKey: "" # AWS_ACCESS_KEY if using S3 or existing minio access key accessKey: "" # AWS_ACCESS_KEY if using S3 or existing minio access key
secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret
region: "" # AWS_REGION if using S3 or existing minio secret
url: "" # only change if pointing to existing minio cluster and minio: false url: "" # only change if pointing to existing minio cluster and minio: false
storage: 100Mi storage: 100Mi

View File

@ -1,12 +1,23 @@
#!/bin/bash #!/bin/bash
tag=$1 tag=$1
tag=${tag:-latest} production=$2
echo "Tagging images with SHA: $GITHUB_SHA and tag: $tag" if [[ ! "$tag" ]]; then
echo "No tag present. You must pass a tag to this script"
exit 1
fi
echo "Tagging images with tag: $tag"
docker tag app-service budibase/apps:$tag docker tag app-service budibase/apps:$tag
docker tag worker-service budibase/worker:$tag docker tag worker-service budibase/worker:$tag
docker push budibase/apps:$tag if [[ "$production" ]]; then
docker push budibase/worker:$tag echo "Production Deployment. Tagging latest.."
docker tag app-service budibase/apps:latest
docker tag worker-service budibase/worker:latest
fi
docker push --all-tags budibase/apps
docker push --all-tags budibase/worker

View File

@ -1,5 +1,5 @@
{ {
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -43,7 +43,7 @@
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint", "lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test", "test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci", "test:e2e:ci": "lerna run cy:ci",
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -", "build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION release && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"release:helm": "./scripts/release_helm_chart.sh", "release:helm": "./scripts/release_helm_chart.sh",
"multi:enable": "lerna run multi:enable", "multi:enable": "lerna run multi:enable",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -65,6 +65,7 @@
"@spectrum-css/search": "^3.0.2", "@spectrum-css/search": "^3.0.2",
"@spectrum-css/sidenav": "^3.0.2", "@spectrum-css/sidenav": "^3.0.2",
"@spectrum-css/statuslight": "^3.0.2", "@spectrum-css/statuslight": "^3.0.2",
"@spectrum-css/stepper": "^3.0.3",
"@spectrum-css/switch": "^1.0.2", "@spectrum-css/switch": "^1.0.2",
"@spectrum-css/table": "^3.0.1", "@spectrum-css/table": "^3.0.1",
"@spectrum-css/tabs": "^3.0.1", "@spectrum-css/tabs": "^3.0.1",

View File

@ -12,6 +12,7 @@
export let getOptionValue = option => option export let getOptionValue = option => option
export let readonly = false export let readonly = false
export let autocomplete = false export let autocomplete = false
export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: selectedLookupMap = getSelectedLookupMap(value) $: selectedLookupMap = getSelectedLookupMap(value)
@ -83,4 +84,5 @@
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
onSelectOption={toggleOption} onSelectOption={toggleOption}
{sort}
/> />

View File

@ -25,11 +25,12 @@
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
export let autocomplete = false export let autocomplete = false
export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let searchTerm = null let searchTerm = null
$: sortedOptions = getSortedOptions(options, getOptionLabel) $: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
$: filteredOptions = getFilteredOptions( $: filteredOptions = getFilteredOptions(
sortedOptions, sortedOptions,
searchTerm, searchTerm,
@ -45,10 +46,13 @@
open = true open = true
} }
const getSortedOptions = (options, getLabel) => { const getSortedOptions = (options, getLabel, sort) => {
if (!options?.length || !Array.isArray(options)) { if (!options?.length || !Array.isArray(options)) {
return [] return []
} }
if (!sort) {
return options
}
return options.sort((a, b) => { return options.sort((a, b) => {
const labelA = getLabel(a) const labelA = getLabel(a)
const labelB = getLabel(b) const labelB = getLabel(b)

View File

@ -15,6 +15,7 @@
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
export let autocomplete = false export let autocomplete = false
export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let open = false let open = false
@ -72,6 +73,7 @@
{getOptionIcon} {getOptionIcon}
{fieldIcon} {fieldIcon}
{autocomplete} {autocomplete}
{sort}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}
placeholderOption={placeholder} placeholderOption={placeholder}
isOptionSelected={option => option === value} isOptionSelected={option => option === value}

View File

@ -0,0 +1,172 @@
<script>
import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/actionbutton/dist/index-vars.css"
import "@spectrum-css/stepper/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let value = null
export let placeholder = null
export let disabled = false
export let error = null
export let id = null
export let readonly = false
export let updateOnChange = true
export let quiet = false
export let min
export let max
export let step
const dispatch = createEventDispatcher()
let focus = false
// We need to keep the field value bound to a different variable in order
// to properly handle erroneous values. If we don't do this then it is
// possible for the field to show stale text which does not represent the
// real value. The reactive statement is to ensure that external changes to
// the value prop are reflected.
let fieldValue = value
$: fieldValue = value
// Ensure step is always a numeric value defaulting to 1
$: step = step == null || isNaN(step) ? 1 : step
const updateValue = value => {
if (readonly) {
return
}
const float = parseFloat(value)
value = isNaN(float) ? null : float
if (value != null) {
if (min != null && value < min) {
value = min
} else if (max != null && value > max) {
value = max
}
}
dispatch("change", value)
fieldValue = value
}
const onFocus = () => {
if (readonly) {
return
}
focus = true
}
const onBlur = event => {
if (readonly) {
return
}
focus = false
updateValue(event.target.value)
}
const onInput = event => {
if (readonly || !updateOnChange) {
return
}
updateValue(event.target.value)
}
const updateValueOnEnter = event => {
if (readonly) {
return
}
if (event.key === "Enter") {
updateValue(event.target.value)
}
}
const stepUp = () => {
if (value == null || isNaN(value)) {
updateValue(step)
} else {
updateValue(value + step)
}
}
const stepDown = () => {
if (value == null || isNaN(value)) {
updateValue(step)
} else {
updateValue(value - step)
}
}
</script>
<div
class="spectrum-Stepper"
class:spectrum-Stepper--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<div class="spectrum-Textfield spectrum-Stepper-textfield">
<input
{disabled}
{readonly}
{id}
bind:value={fieldValue}
placeholder={placeholder || ""}
type="number"
class="spectrum-Textfield-input spectrum-Stepper-input"
on:click
on:blur
on:focus
on:input
on:keyup
on:blur={onBlur}
on:focus={onFocus}
on:input={onInput}
on:keyup={updateValueOnEnter}
/>
</div>
<span class="spectrum-Stepper-buttons">
<button
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepUp"
tabindex="-1"
on:click={stepUp}
>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronUp75"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron75" />
</svg>
</button>
<button
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepDown"
tabindex="-1"
on:click={stepDown}
>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown75"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron75" />
</svg>
</button>
</span>
</div>
<style>
.spectrum-Stepper {
width: 100%;
}
.spectrum-Stepper::before {
display: none;
}
</style>

View File

@ -9,3 +9,4 @@ export { default as CoreSwitch } from "./Switch.svelte"
export { default as CoreSearch } from "./Search.svelte" export { default as CoreSearch } from "./Search.svelte"
export { default as CoreDatePicker } from "./DatePicker.svelte" export { default as CoreDatePicker } from "./DatePicker.svelte"
export { default as CoreDropzone } from "./Dropzone.svelte" export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreStepper } from "./Stepper.svelte"

View File

@ -13,6 +13,7 @@
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -29,6 +30,7 @@
{value} {value}
{options} {options}
{placeholder} {placeholder}
{sort}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
on:change={onChange} on:change={onChange}

View File

@ -16,6 +16,7 @@
export let getOptionIcon = option => option?.icon export let getOptionIcon = option => option?.icon
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -41,6 +42,7 @@
{options} {options}
{placeholder} {placeholder}
{autoWidth} {autoWidth}
{sort}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
{getOptionIcon} {getOptionIcon}

View File

@ -0,0 +1,45 @@
<script>
import Field from "./Field.svelte"
import Stepper from "./Core/Stepper.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = null
export let labelPosition = "above"
export let placeholder = null
export let disabled = false
export let readonly = false
export let error = null
export let updateOnChange = true
export let quiet = false
export let min = null
export let max = null
export let step = 1
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Field {label} {labelPosition} {error}>
<Stepper
{updateOnChange}
{error}
{disabled}
{readonly}
{value}
{placeholder}
{quiet}
{min}
{max}
{step}
on:change={onChange}
on:click
on:input
on:blur
on:focus
on:keyup
/>
</Field>

View File

@ -19,7 +19,7 @@
<li <li
data-cy={dataCy} data-cy={dataCy}
on:click|preventDefault={onClick} on:click|preventDefault={disabled ? null : onClick}
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-disabled={disabled} class:is-disabled={disabled}
role="menuitem" role="menuitem"

View File

@ -5,6 +5,7 @@ import "@spectrum-css/icon/dist/index-vars.css"
// Components // Components
export { default as Input } from "./Form/Input.svelte" export { default as Input } from "./Form/Input.svelte"
export { default as Stepper } from "./Form/Stepper.svelte"
export { default as TextArea } from "./Form/TextArea.svelte" export { default as TextArea } from "./Form/TextArea.svelte"
export { default as Select } from "./Form/Select.svelte" export { default as Select } from "./Form/Select.svelte"
export { default as Combobox } from "./Form/Combobox.svelte" export { default as Combobox } from "./Form/Combobox.svelte"

View File

@ -206,6 +206,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5" resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
integrity sha512-xodB8g8vGJH20XmUj9ZsPlM1jHrGeRbvmVXkz0q7YvQrYAhim8pP3W+XKKZAletPFAuu8cmUOc6SWn6i4X4z6w== integrity sha512-xodB8g8vGJH20XmUj9ZsPlM1jHrGeRbvmVXkz0q7YvQrYAhim8pP3W+XKKZAletPFAuu8cmUOc6SWn6i4X4z6w==
"@spectrum-css/stepper@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.3.tgz#ae89846886431e3edeee060207b8f81540f73a34"
integrity sha512-prAD61ImlOTs9b6PfB3cB08x4lAfxtvnW+RZiTYky0E8GgZdrc/MfCkL5/oqQaIQUtyQv/3Lb7ELAf/0K8QTXw==
"@spectrum-css/switch@^1.0.2": "@spectrum-css/switch@^1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44" resolved "https://registry.yarnpkg.com/@spectrum-css/switch/-/switch-1.0.2.tgz#f0b4c69271964573e02b08e90998096e49e1de44"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.105-alpha.28", "@budibase/bbui": "^0.9.105-alpha.31",
"@budibase/client": "^0.9.105-alpha.28", "@budibase/client": "^0.9.105-alpha.31",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.105-alpha.28", "@budibase/string-templates": "^0.9.105-alpha.31",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -120,23 +120,30 @@ const getContextBindings = (asset, componentId) => {
// Create bindings for each data provider // Create bindings for each data provider
dataProviders.forEach(component => { dataProviders.forEach(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
const contextDefinition = def.context const contexts = Array.isArray(def.context) ? def.context : [def.context]
// Create bindings for each context block provided by this data provider
contexts.forEach(context => {
if (!context?.type) {
return
}
let schema let schema
let readablePrefix let readablePrefix
if (contextDefinition.type === "form") { if (context.type === "form") {
// Forms do not need table schemas // Forms do not need table schemas
// Their schemas are built from their component field names // Their schemas are built from their component field names
schema = buildFormSchema(component) schema = buildFormSchema(component)
readablePrefix = "Fields" readablePrefix = "Fields"
} else if (contextDefinition.type === "static") { } else if (context.type === "static") {
// Static contexts are fully defined by the components // Static contexts are fully defined by the components
schema = {} schema = {}
const values = contextDefinition.values || [] const values = context.values || []
values.forEach(value => { values.forEach(value => {
schema[value.key] = { name: value.label, type: "string" } schema[value.key] = { name: value.label, type: "string" }
}) })
} else if (contextDefinition.type === "schema") { } else if (context.type === "schema") {
// Schema contexts are generated dynamically depending on their data // Schema contexts are generated dynamically depending on their data
const datasource = getDatasourceForProvider(asset, component) const datasource = getDatasourceForProvider(asset, component)
if (!datasource) { if (!datasource) {
@ -188,6 +195,7 @@ const getContextBindings = (asset, componentId) => {
}) })
}) })
}) })
})
return bindings return bindings
} }

View File

@ -20,7 +20,12 @@ import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api" import api from "../api"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import analytics from "analytics" import analytics from "analytics"
import { findComponentType, findComponentParent } from "../storeUtils" import {
findComponentType,
findComponentParent,
findClosestMatchingComponent,
findAllMatchingComponents,
} from "../storeUtils"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding" import { removeBindings } from "../dataBinding"
@ -334,6 +339,18 @@ export const getFrontendStore = () => {
if (definition.hasChildren) { if (definition.hasChildren) {
extras._children = [] extras._children = []
} }
if (componentName.endsWith("/formstep")) {
const parentForm = findClosestMatchingComponent(
get(currentAsset).props,
get(selectedComponent)._id,
component => component._component.endsWith("/form")
)
const formSteps = findAllMatchingComponents(parentForm, component =>
component._component.endsWith("/formstep")
)
extras.step = formSteps.length + 1
extras._instanceName = `Step ${formSteps.length + 1}`
}
return { return {
_id: uuid(), _id: uuid(),

View File

@ -86,7 +86,7 @@ const createScreen = table => {
valueType: "Binding", valueType: "Binding",
}, },
], ],
limit: table.type === "external" ? undefined : 1, limit: 1,
paginate: false, paginate: false,
}) })
@ -94,6 +94,7 @@ const createScreen = table => {
.instanceName("Repeater") .instanceName("Repeater")
.customProps({ .customProps({
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`, dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
noRowsMessage: "We couldn't find a row to display",
}) })
const form = makeMainForm() const form = makeMainForm()

View File

@ -19,6 +19,7 @@
data-cy="{meta.name}-select" data-cy="{meta.name}-select"
bind:value bind:value
options={meta.constraints.inclusion} options={meta.constraints.inclusion}
sort
/> />
{:else if type === "datetime"} {:else if type === "datetime"}
<DatePicker {label} bind:value /> <DatePicker {label} bind:value />

View File

@ -153,6 +153,7 @@
label="Display Column" label="Display Column"
bind:value={primaryDisplay} bind:value={primaryDisplay}
options={fields} options={fields}
sort
/> />
</div> </div>
{/if} {/if}

View File

@ -47,6 +47,7 @@
getOptionValue={row => row._id} getOptionValue={row => row._id}
on:change={e => (linkedIds = e.detail ? [e.detail] : [])} on:change={e => (linkedIds = e.detail ? [e.detail] : [])}
{label} {label}
sort
/> />
{:else} {:else}
<Multiselect <Multiselect
@ -55,5 +56,6 @@
options={rows} options={rows}
getOptionLabel={getPrettyName} getOptionLabel={getPrettyName}
getOptionValue={row => row._id} getOptionValue={row => row._id}
sort
/> />
{/if} {/if}

View File

@ -46,7 +46,7 @@
<ActionMenu disabled={!item.isCategory}> <ActionMenu disabled={!item.isCategory}>
<ActionButton <ActionButton
icon={item.icon} icon={item.icon}
disabled={isChildAllowed(item, $selectedComponent)} disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)}
quiet quiet
size="S" size="S"
slot="control" slot="control"
@ -66,6 +66,7 @@
dataCy={`component-${item.name}`} dataCy={`component-${item.name}`}
icon={item.icon} icon={item.icon}
on:click={() => onItemChosen(item)} on:click={() => onItemChosen(item)}
disabled={isChildAllowed(item, $selectedComponent)}
> >
{item.name} {item.name}
</MenuItem> </MenuItem>

View File

@ -10,6 +10,7 @@
"icon": "Form", "icon": "Form",
"children": [ "children": [
"form", "form",
"formstep",
"fieldgroup", "fieldgroup",
"stringfield", "stringfield",
"numberfield", "numberfield",

View File

@ -1,7 +1,6 @@
<script> <script>
import { store, allScreens, selectedAccessRole } from "builderStore" import { store, allScreens, selectedAccessRole } from "builderStore"
import { tables } from "stores/backend" import { tables, roles } from "stores/backend"
import { roles } from "stores/backend"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import analytics from "analytics" import analytics from "analytics"
@ -16,7 +15,7 @@
let createLink = true let createLink = true
let roleId = $selectedAccessRole || "BASIC" let roleId = $selectedAccessRole || "BASIC"
$: templates = getTemplates($store, $tables.list) $: templates = getTemplates($store, tables.getDataSources())
$: route = !route && $allScreens.length === 0 ? "*" : route $: route = !route && $allScreens.length === 0 ? "*" : route
$: { $: {
if (templates && templateIndex === undefined) { if (templates && templateIndex === undefined) {

View File

@ -85,6 +85,8 @@
props={{ props={{
options: setting.options || [], options: setting.options || [],
placeholder: setting.placeholder || null, placeholder: setting.placeholder || null,
min: setting.min || null,
max: setting.max || null,
}} }}
{bindings} {bindings}
{componentDefinition} {componentDefinition}

View File

@ -31,7 +31,7 @@
export let bindings = [] export let bindings = []
$: text = value?.label ?? "Choose an option" $: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({ $: tables = tablesStore.getDataSources().map(m => ({
label: m.name, label: m.name,
tableId: m._id, tableId: m._id,
type: "table", type: "table",

View File

@ -33,7 +33,7 @@
$: selectedActionComponent = $: selectedActionComponent =
selectedAction && selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component
// Select the first action if we delete an action // Select the first action if we delete an action
$: { $: {
@ -116,7 +116,7 @@
</ActionMenu> </ActionMenu>
</Layout> </Layout>
<Layout noPadding> <Layout noPadding>
{#if selectedAction} {#if selectedActionComponent}
<div class="selected-action-container"> <div class="selected-action-container">
<svelte:component <svelte:component
this={selectedActionComponent} this={selectedActionComponent}

View File

@ -0,0 +1,68 @@
<script>
import { Select, Label, Stepper } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding"
import { onMount } from "svelte"
export let parameters
$: actionProviders = getActionProviderComponents(
$currentAsset,
$store.selectedComponentId,
"ChangeFormStep"
)
const typeOptions = [
{
label: "Next step",
value: "next",
},
{
label: "Previous step",
value: "prev",
},
{
label: "First step",
value: "first",
},
{
label: "Specific step",
value: "specific",
},
]
onMount(() => {
if (!parameters.type) {
parameters.type = "next"
}
})
</script>
<div class="root">
<Label small>Form</Label>
<Select
placeholder={null}
bind:value={parameters.componentId}
options={actionProviders}
getOptionLabel={x => x._instanceName}
getOptionValue={x => x._id}
/>
<Label small>Step</Label>
<Select bind:value={parameters.type} options={typeOptions} />
{#if parameters.type === "specific"}
<Label small>Number</Label>
<Stepper bind:value={parameters.number} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -29,7 +29,7 @@
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr; grid-template-columns: 60px 1fr;
align-items: center; align-items: center;
max-width: 800px; max-width: 400px;
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View File

@ -11,7 +11,6 @@
<style> <style>
.root { .root {
max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View File

@ -8,7 +8,6 @@
<style> <style>
.root { .root {
max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View File

@ -25,7 +25,7 @@
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
max-width: 800px; max-width: 400px;
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label, Checkbox } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviderComponents } from "builderStore/dataBinding"
@ -20,6 +20,11 @@
getOptionLabel={x => x._instanceName} getOptionLabel={x => x._instanceName}
getOptionValue={x => x._id} getOptionValue={x => x._id}
/> />
<div />
<Checkbox
text="Validate only current step"
bind:value={parameters.onlyCurrentStep}
/>
</div> </div>
<style> <style>
@ -29,7 +34,7 @@
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr; grid-template-columns: 60px 1fr;
align-items: center; align-items: center;
max-width: 800px; max-width: 400px;
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View File

@ -7,6 +7,7 @@ import ValidateForm from "./ValidateForm.svelte"
import LogOut from "./LogOut.svelte" import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte" import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte" import CloseScreenModal from "./CloseScreenModal.svelte"
import ChangeFormStep from "./ChangeFormStep.svelte"
// Defines which actions are available to configure in the front end. // Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't // Unfortunately the "name" property is used as the identifier so please don't
@ -52,4 +53,8 @@ export default [
name: "Close Screen Modal", name: "Close Screen Modal",
component: CloseScreenModal, component: CloseScreenModal,
}, },
{
name: "Change Form Step",
component: ChangeFormStep,
},
] ]

View File

@ -17,7 +17,7 @@
} }
</script> </script>
<ActionButton on:click={drawer.show}>Configure Validation</ActionButton> <ActionButton on:click={drawer.show}>Configure validation</ActionButton>
<Drawer bind:this={drawer} title="Validation Rules"> <Drawer bind:this={drawer} title="Validation Rules">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure validation rules for this field. Configure validation rules for this field.

View File

@ -1,4 +1,4 @@
import { Checkbox, Input, Select } from "@budibase/bbui" import { Checkbox, Input, Select, Stepper } from "@budibase/bbui"
import DataSourceSelect from "./DataSourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect.svelte"
import DataProviderSelect from "./DataProviderSelect.svelte" import DataProviderSelect from "./DataProviderSelect.svelte"
import EventsEditor from "./EventsEditor" import EventsEditor from "./EventsEditor"
@ -22,7 +22,7 @@ const componentMap = {
dataSource: DataSourceSelect, dataSource: DataSourceSelect,
dataProvider: DataProviderSelect, dataProvider: DataProviderSelect,
boolean: Checkbox, boolean: Checkbox,
number: Input, number: Stepper,
event: EventsEditor, event: EventsEditor,
table: TableSelect, table: TableSelect,
color: ColorPicker, color: ColorPicker,

View File

@ -87,6 +87,7 @@ export function createTablesStore() {
draft: {}, draft: {},
}) })
}, },
getDataSources: () => get(store).list.filter(t => t.name !== "Users"),
delete: async table => { delete: async table => {
await api.delete(`/api/tables/${table._id}/${table._rev}`) await api.delete(`/api/tables/${table._id}/${table._rev}`)
update(state => ({ update(state => ({

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -18,9 +18,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.105-alpha.28", "@budibase/bbui": "^0.9.105-alpha.31",
"@budibase/standard-components": "^0.9.105-alpha.28", "@budibase/standard-components": "^0.9.105-alpha.31",
"@budibase/string-templates": "^0.9.105-alpha.28", "@budibase/string-templates": "^0.9.105-alpha.31",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -63,6 +63,7 @@
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.inBuilder &&
$builderStore.selectedComponentId === instance._id $builderStore.selectedComponentId === instance._id
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
$: interactive = $builderStore.previewType === "layout" || insideScreenslot $: interactive = $builderStore.previewType === "layout" || insideScreenslot
$: evaluateConditions(enrichedSettings?._conditions) $: evaluateConditions(enrichedSettings?._conditions)
$: componentSettings = { ...enrichedSettings, ...conditionalSettings } $: componentSettings = { ...enrichedSettings, ...conditionalSettings }
@ -74,7 +75,6 @@
styles: { ...instance._styles, id, empty, interactive }, styles: { ...instance._styles, id, empty, interactive },
empty, empty,
selected, selected,
props: componentSettings,
name, name,
}) })
@ -175,13 +175,12 @@
</script> </script>
{#key propsHash} {#key propsHash}
{#if constructor && componentSettings && visible} {#if constructor && componentSettings && (visible || inSelectedPath)}
<div <div
class={`component ${id}`} class={`component ${id}`}
data-type={interactive ? "component" : ""} data-type={interactive ? "component" : ""}
data-id={id} data-id={id}
data-name={name} data-name={name}
class:hidden={!visible}
> >
<svelte:component this={constructor} {...componentSettings}> <svelte:component this={constructor} {...componentSettings}>
{#if children.length} {#if children.length}

View File

@ -61,7 +61,7 @@
// Sanity limit of 100 active indicators // Sanity limit of 100 active indicators
const children = Array.from(parents) const children = Array.from(parents)
.map(parent => parent?.childNodes?.[0]) .map(parent => parent?.childNodes?.[0])
.filter(child => child != null) .filter(node => node?.nodeType === 1)
.slice(0, 100) .slice(0, 100)
// If there aren't any nodes then reset // If there aren't any nodes then reset

View File

@ -7,6 +7,7 @@ export const ActionTypes = {
RefreshDatasource: "RefreshDatasource", RefreshDatasource: "RefreshDatasource",
SetDataProviderQuery: "SetDataProviderQuery", SetDataProviderQuery: "SetDataProviderQuery",
ClearForm: "ClearForm", ClearForm: "ClearForm",
ChangeFormStep: "ChangeFormStep",
} }
export const ApiVersion = "1" export const ApiVersion = "1"

View File

@ -1,5 +1,6 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import Manifest from "@budibase/standard-components/manifest.json" import Manifest from "@budibase/standard-components/manifest.json"
import { findComponentById, findComponentPathById } from "../utils/components"
const dispatchEvent = (type, data = {}) => { const dispatchEvent = (type, data = {}) => {
window.dispatchEvent( window.dispatchEvent(
@ -9,25 +10,6 @@ const dispatchEvent = (type, data = {}) => {
) )
} }
const findComponentById = (component, componentId) => {
if (!component || !componentId) {
return null
}
if (component._id === componentId) {
return component
}
if (!component._children?.length) {
return null
}
for (let child of component._children) {
const result = findComponentById(child, componentId)
if (result) {
return result
}
}
return null
}
const createBuilderStore = () => { const createBuilderStore = () => {
const initialState = { const initialState = {
inBuilder: false, inBuilder: false,
@ -37,9 +19,15 @@ const createBuilderStore = () => {
selectedComponentId: null, selectedComponentId: null,
previewId: null, previewId: null,
previewType: null, previewType: null,
selectedPath: [],
} }
const writableStore = writable(initialState) const writableStore = writable(initialState)
const derivedStore = derived(writableStore, $state => { const derivedStore = derived(writableStore, $state => {
// Avoid any of this logic if we aren't in the builder preview
if (!$state.inBuilder) {
return $state
}
// Derive the selected component instance and definition // Derive the selected component instance and definition
const { layout, screen, previewType, selectedComponentId } = $state const { layout, screen, previewType, selectedComponentId } = $state
const asset = previewType === "layout" ? layout : screen const asset = previewType === "layout" ? layout : screen
@ -47,10 +35,15 @@ const createBuilderStore = () => {
const prefix = "@budibase/standard-components/" const prefix = "@budibase/standard-components/"
const type = component?._component?.replace(prefix, "") const type = component?._component?.replace(prefix, "")
const definition = type ? Manifest[type] : null const definition = type ? Manifest[type] : null
// Derive the selected component path
const path = findComponentPathById(asset.props, selectedComponentId) || []
return { return {
...$state, ...$state,
selectedComponent: component, selectedComponent: component,
selectedComponentDefinition: definition, selectedComponentDefinition: definition,
selectedComponentPath: path?.map(component => component._id),
} }
}) })
@ -67,6 +60,14 @@ const createBuilderStore = () => {
notifyLoaded: () => { notifyLoaded: () => {
dispatchEvent("preview-loaded") dispatchEvent("preview-loaded")
}, },
setSelectedPath: path => {
console.log("set to ")
console.log(path)
writableStore.update(state => {
state.selectedPath = path
return state
})
},
} }
return { return {
...writableStore, ...writableStore,

View File

@ -1,7 +1,12 @@
import { derived } from "svelte/store" import { derived, get } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import {
findComponentPathById,
findChildrenByType,
findComponentById,
} from "../utils/components"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -36,8 +41,39 @@ const createScreenStore = () => {
} }
) )
// Utils to parse component definitions
const actions = {
findComponentById: componentId => {
const { activeScreen, activeLayout } = get(store)
let result = findComponentById(activeScreen?.props, componentId)
if (result) {
return result
}
return findComponentById(activeLayout?.props)
},
findComponentPathById: componentId => {
const { activeScreen, activeLayout } = get(store)
let result = findComponentPathById(activeScreen?.props, componentId)
if (result) {
return result
}
return findComponentPathById(activeLayout?.props)
},
findChildrenByType: (componentId, type) => {
const component = actions.findComponentById(componentId)
if (!component || !component._children) {
return null
}
let children = []
findChildrenByType(component, type, children)
console.log(children)
return children
},
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions,
} }
} }

View File

@ -66,10 +66,15 @@ const queryExecutionHandler = async action => {
}) })
} }
const executeActionHandler = async (context, componentId, actionType) => { const executeActionHandler = async (
context,
componentId,
actionType,
params
) => {
const fn = context[`${componentId}_${actionType}`] const fn = context[`${componentId}_${actionType}`]
if (fn) { if (fn) {
return await fn() return await fn(params)
} }
} }
@ -77,7 +82,8 @@ const validateFormHandler = async (action, context) => {
return await executeActionHandler( return await executeActionHandler(
context, context,
action.parameters.componentId, action.parameters.componentId,
ActionTypes.ValidateForm ActionTypes.ValidateForm,
action.parameters.onlyCurrentStep
) )
} }
@ -101,6 +107,15 @@ const clearFormHandler = async (action, context) => {
) )
} }
const changeFormStepHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.ChangeFormStep,
action.parameters
)
}
const closeScreenModalHandler = () => { const closeScreenModalHandler = () => {
// Emit this as a window event, so parent screens which are iframing us in // Emit this as a window event, so parent screens which are iframing us in
// can close the modal // can close the modal
@ -118,6 +133,7 @@ const handlerMap = {
["Log Out"]: logoutHandler, ["Log Out"]: logoutHandler,
["Clear Form"]: clearFormHandler, ["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler, ["Close Screen Modal"]: closeScreenModalHandler,
["Change Form Step"]: changeFormStepHandler,
} }
const confirmTextMap = { const confirmTextMap = {

View File

@ -51,6 +51,12 @@ export const enrichProps = (props, context) => {
condition.settingValue, condition.settingValue,
totalContext totalContext
) )
// If there is an onclick function in here then it won't be serialised
// properly, and therefore will not be updated properly.
// The solution to this is add a rand which will ensure diffs happen
// every time.
condition.rand = Math.random()
} }
}) })
} }

View File

@ -0,0 +1,62 @@
/**
* Finds a component instance by ID
*/
export const findComponentById = (component, componentId) => {
if (!component || !componentId) {
return null
}
if (component._id === componentId) {
return component
}
if (!component._children?.length) {
return null
}
for (let child of component._children) {
const result = findComponentById(child, componentId)
if (result) {
return result
}
}
return null
}
/**
* Finds the component path to a component
*/
export const findComponentPathById = (component, componentId, path = []) => {
if (!component || !componentId) {
return null
}
path = [...path, component]
if (component._id === componentId) {
return path
}
if (!component._children?.length) {
return null
}
for (let child of component._children) {
const result = findComponentPathById(child, componentId, path)
if (result) {
return result
}
}
return null
}
/**
* Finds all children instances of a certain component type of a given component
*/
export const findChildrenByType = (component, type, children = []) => {
if (!component) {
return
}
if (component._component.endsWith(`/${type}`)) {
children.push(component)
}
if (!component._children?.length) {
return
}
component._children.forEach(child => {
findChildrenByType(child, type, children)
})
}

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -62,9 +62,9 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.105-alpha.28", "@budibase/auth": "^0.9.105-alpha.31",
"@budibase/client": "^0.9.105-alpha.28", "@budibase/client": "^0.9.105-alpha.31",
"@budibase/string-templates": "^0.9.105-alpha.28", "@budibase/string-templates": "^0.9.105-alpha.31",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1", "@sendgrid/mail": "7.1.1",
@ -117,7 +117,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4", "@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.105-alpha.28", "@budibase/standard-components": "^0.9.105-alpha.31",
"@jest/test-sequencer": "^24.8.0", "@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1", "@types/bull": "^3.15.1",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",

View File

@ -1702,10 +1702,11 @@
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
"hasChildren": true, "hasChildren": true,
"illegalChildren": ["section"], "illegalChildren": ["section", "form"],
"actions": [ "actions": [
"ValidateForm", "ValidateForm",
"ClearForm" "ClearForm",
"ChangeFormStep"
], ],
"styles": ["size"], "styles": ["size"],
"settings": [ "settings": [
@ -1747,9 +1748,44 @@
"defaultValue": false "defaultValue": false
} }
], ],
"context": { "context": [
{
"type": "static",
"values": [
{
"label": "Valid",
"key": "__valid"
},
{
"label": "Current Step",
"key": "__currentStep"
},
{
"label": "Current Step Valid",
"key": "__currentStepValid"
}
]
},
{
"type": "form" "type": "form"
} }
]
},
"formstep": {
"name": "Form Step",
"icon": "AssetsAdded",
"hasChildren": true,
"illegalChildren": ["section", "form", "form step"],
"styles": ["size"],
"settings": [
{
"type": "number",
"label": "Step",
"key": "step",
"defaultValue": 1,
"min": 1
}
]
}, },
"fieldgroup": { "fieldgroup": {
"name": "Field Group", "name": "Field Group",

View File

@ -29,11 +29,11 @@
"keywords": [ "keywords": [
"svelte" "svelte"
], ],
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"license": "MIT", "license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.105-alpha.28", "@budibase/bbui": "^0.9.105-alpha.31",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -42,11 +42,11 @@
bind:fieldApi bind:fieldApi
defaultValue={[]} defaultValue={[]}
> >
{#if $fieldState} {#if fieldState}
<CoreDropzone <CoreDropzone
value={$fieldState.value} value={fieldState.value}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
on:change={e => { on:change={e => {
fieldApi.setValue(e.detail) fieldApi.setValue(e.detail)
}} }}

View File

@ -39,10 +39,10 @@
> >
{#if fieldState} {#if fieldState}
<CoreCheckbox <CoreCheckbox
value={$fieldState.value} value={fieldState.value}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
id={$fieldState.fieldId} id={fieldState.fieldId}
{size} {size}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
{text} {text}

View File

@ -51,11 +51,11 @@
> >
{#if fieldState} {#if fieldState}
<CoreDatePicker <CoreDatePicker
value={$fieldState.value} value={fieldState.value}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
id={$fieldState.fieldId} id={fieldState.fieldId}
{enableTime} {enableTime}
{placeholder} {placeholder}
/> />

View File

@ -1,7 +1,7 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte" import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext } from "svelte" import { getContext, onDestroy } from "svelte"
export let label export let label
export let field export let field
@ -15,7 +15,8 @@
// Get contexts // Get contexts
const formContext = getContext("form") const formContext = getContext("form")
const fieldGroupContext = getContext("fieldGroup") const formStepContext = getContext("form-step")
const fieldGroupContext = getContext("field-group")
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -26,16 +27,23 @@
field, field,
defaultValue, defaultValue,
disabled, disabled,
validation validation,
formStepContext || 1
) )
// Expose field properties to parent component // Update form properties in parent component on every store change
fieldState = formField?.fieldState const unsubscribe = formField?.subscribe(value => {
fieldApi = formField?.fieldApi fieldState = value?.fieldState
fieldSchema = formField?.fieldSchema fieldApi = value?.fieldApi
fieldSchema = value?.fieldSchema
})
onDestroy(() => unsubscribe && unsubscribe())
// Keep validation rules up to date // Keep validation rules up to date
$: fieldApi?.updateValidation(validation) $: updateValidation(validation)
const updateValidation = validation => {
fieldApi?.updateValidation(validation)
}
// Extract label position from field group context // Extract label position from field group context
$: labelPositionClass = $: labelPositionClass =
@ -46,7 +54,7 @@
<div class="spectrum-Form-item" use:styleable={$component.styles}> <div class="spectrum-Form-item" use:styleable={$component.styles}>
<label <label
class:hidden={!label} class:hidden={!label}
for={$fieldState?.fieldId} for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`} class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}
> >
{label || ""} {label || ""}
@ -64,8 +72,8 @@
/> />
{:else} {:else}
<slot /> <slot />
{#if $fieldState.error} {#if fieldState.error}
<div class="error">{$fieldState.error}</div> <div class="error">{fieldState.error}</div>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@ -5,7 +5,7 @@
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
setContext("fieldGroup", { labelPosition }) setContext("field-group", { labelPosition })
</script> </script>
<div class="wrapper" use:styleable={$component.styles}> <div class="wrapper" use:styleable={$component.styles}>

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const fieldGroupContext = getContext("fieldGroup") const fieldGroupContext = getContext("field-group")
</script> </script>
{#if fieldGroupContext} {#if fieldGroupContext}

View File

@ -1,5 +1,5 @@
<script> <script>
import { getContext } from "svelte" import { getContext, onMount } from "svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
export let dataSource export let dataSource
@ -9,6 +9,11 @@
export let actionType = "Create" export let actionType = "Create"
const context = getContext("context") const context = getContext("context")
const { API } = getContext("sdk")
let loaded = false
let schema
let table
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, context) => { const getInitialValues = (type, dataSource, context) => {
@ -32,19 +37,48 @@
return closestContext return closestContext
} }
// Fetches the form schema from this form's dataSource, if one exists
const fetchSchema = async () => {
if (!dataSource?.tableId) {
schema = {}
table = null
} else {
table = await API.fetchTableDefinition(dataSource?.tableId)
if (table) {
if (dataSource?.type === "query") {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
} else {
schema = table.schema || {}
}
}
}
loaded = true
}
$: initialValues = getInitialValues(actionType, dataSource, $context) $: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = JSON.stringify(initialValues) $: resetKey = JSON.stringify(initialValues)
// Load the form schema on mount
onMount(fetchSchema)
</script> </script>
{#key resetKey} {#if loaded}
{#key resetKey}
<InnerForm <InnerForm
{dataSource} {dataSource}
{theme} {theme}
{size} {size}
{disabled} {disabled}
{actionType} {actionType}
{schema}
{table}
{initialValues} {initialValues}
> >
<slot /> <slot />
</InnerForm> </InnerForm>
{/key} {/key}
{/if}

View File

@ -0,0 +1,35 @@
<script>
import { getContext, setContext } from "svelte"
import Placeholder from "../Placeholder.svelte"
export let step = 1
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
const formContext = getContext("form")
// Set form step context so fields know what step they are within
setContext("form-step", step || 1)
$: formState = formContext?.formState
$: currentStep = $formState?.currentStep
// If in the builder preview, show this step if a child is selected
$: {
if (
formContext &&
$builderStore.inBuilder &&
$builderStore.selectedComponentPath?.includes($component.id)
) {
formContext.formApi.setStep(step)
}
}
</script>
{#if !formContext}
<Placeholder text="Form steps need to be wrapped in a form" />
{:else if step === currentStep}
<div use:styleable={$component.styles}>
<slot />
</div>
{/if}

View File

@ -1,6 +1,6 @@
<script> <script>
import { setContext, getContext, onMount } from "svelte" import { setContext, getContext } from "svelte"
import { writable, get } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { createValidatorFromConstraints } from "./validation" import { createValidatorFromConstraints } from "./validation"
import { generateID } from "../helpers" import { generateID } from "../helpers"
@ -8,30 +8,83 @@
export let disabled = false export let disabled = false
export let initialValues export let initialValues
export let size export let size
export let schema
export let table
const component = getContext("component") const component = getContext("component")
const { styleable, API, Provider, ActionTypes } = getContext("sdk") const { styleable, Provider, ActionTypes } = getContext("sdk")
let loaded = false let fields = []
let schema const currentStep = writable(1)
let table const formState = writable({
let fieldMap = {} values: {},
errors: {},
valid: true,
currentStep: 1,
})
// Form state contains observable data about the form // Reactive derived stores to derive form state from field array
const formState = writable({ values: initialValues, errors: {}, valid: true }) $: values = deriveFieldProperty(fields, f => f.fieldState.value)
$: errors = deriveFieldProperty(fields, f => f.fieldState.error)
$: valid = !Object.values($errors).some(error => error != null)
// Derive whether the current form step is valid
$: currentStepValid = derived(
[currentStep, ...fields],
([currentStepValue, ...fieldsValue]) => {
return !fieldsValue
.filter(f => f.step === currentStepValue)
.some(f => f.fieldState.error != null)
}
)
// Update form state store from derived stores
$: {
formState.set({
values: $values,
errors: $errors,
valid,
currentStep: $currentStep,
})
}
// Generates a derived store from an array of fields, comprised of a map of
// extracted values from the field array
const deriveFieldProperty = (fieldStores, getProp) => {
return derived(fieldStores, fieldValues => {
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) })
return fieldValues.reduce(reducer, {})
})
}
// Searches the field array for a certain field
const getField = name => {
return fields.find(field => get(field).name === name)
}
// Form API contains functions to control the form
const formApi = { const formApi = {
registerField: ( registerField: (
field, field,
defaultValue = null, defaultValue = null,
fieldDisabled = false, fieldDisabled = false,
validationRules validationRules,
step = 1
) => { ) => {
if (!field) { if (!field) {
return return
} }
// If we've already registered this field then wipe any errors and
// return the existing field
const existingField = getField(field)
if (existingField) {
existingField.update(state => {
state.fieldState.error = null
return state
})
return existingField
}
// Auto columns are always disabled // Auto columns are always disabled
const isAutoColumn = !!schema?.[field]?.autocolumn const isAutoColumn = !!schema?.[field]?.autocolumn
@ -44,85 +97,86 @@
table table
) )
// Construct field object // Construct field info
fieldMap[field] = { const fieldInfo = writable({
fieldState: makeFieldState( name: field,
field, step: step || 1,
validator, fieldState: {
fieldId: `id-${generateID()}`,
value: initialValues[field] ?? defaultValue,
error: null,
disabled: disabled || fieldDisabled || isAutoColumn,
defaultValue, defaultValue,
disabled || fieldDisabled || isAutoColumn validator,
), },
fieldApi: makeFieldApi(field, defaultValue), fieldApi: makeFieldApi(field, defaultValue),
fieldSchema: schema?.[field] ?? {}, fieldSchema: schema?.[field] ?? {},
})
// Add this field
fields = [...fields, fieldInfo]
return fieldInfo
},
validate: (onlyCurrentStep = false) => {
let valid = true
let validationFields = fields
// Reduce fields to only the current step if required
if (onlyCurrentStep) {
validationFields = fields.filter(f => get(f).step === get(currentStep))
} }
// Set initial value // Validate fields and check if any are invalid
const initialValue = get(fieldMap[field].fieldState).value validationFields.forEach(field => {
formState.update(state => ({ if (!get(field).fieldApi.validate()) {
...state, valid = false
values: { }
...state.values,
[field]: initialValue,
},
}))
return fieldMap[field]
},
validate: () => {
const fields = Object.keys(fieldMap)
fields.forEach(field => {
const { fieldApi } = fieldMap[field]
fieldApi.validate()
}) })
return get(formState).valid return valid
}, },
clear: () => { clear: () => {
const fields = Object.keys(fieldMap) // Clear the form by clearing each individual field
fields.forEach(field => { fields.forEach(field => {
const { fieldApi } = fieldMap[field] get(field).fieldApi.clearValue()
fieldApi.clearValue()
}) })
}, },
changeStep: ({ type, number }) => {
if (type === "next") {
currentStep.update(step => step + 1)
} else if (type === "prev") {
currentStep.update(step => Math.max(1, step - 1))
} else if (type === "first") {
currentStep.set(1)
} else if (type === "specific" && number && !isNaN(number)) {
currentStep.set(number)
}
},
setStep: step => {
if (step) {
currentStep.set(step)
}
},
} }
// Provide both form API and state to children
setContext("form", { formApi, formState, dataSource })
// Action context to pass to children
const actions = [
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
{ type: ActionTypes.ClearForm, callback: formApi.clear },
]
// Creates an API for a specific field // Creates an API for a specific field
const makeFieldApi = field => { const makeFieldApi = field => {
// Sets the value for a certain field and invokes validation // Sets the value for a certain field and invokes validation
const setValue = (value, skipCheck = false) => { const setValue = (value, skipCheck = false) => {
const { fieldState } = fieldMap[field] const fieldInfo = getField(field)
const { validator } = get(fieldState) const { fieldState } = get(fieldInfo)
const { validator } = fieldState
// Skip if the value is the same // Skip if the value is the same
if (!skipCheck && get(fieldState).value === value) { if (!skipCheck && fieldState.value === value) {
return return
} }
// Update field state // Update field state
const error = validator ? validator(value) : null const error = validator ? validator(value) : null
fieldState.update(state => { fieldInfo.update(state => {
state.value = value state.fieldState.value = value
state.error = error state.fieldState.error = error
return state
})
// Update form state
formState.update(state => {
state.values = { ...state.values, [field]: value }
if (error) {
state.errors = { ...state.errors, [field]: error }
} else {
delete state.errors[field]
}
state.valid = Object.keys(state.errors).length === 0
return state return state
}) })
@ -131,30 +185,23 @@
// Clears the value of a certain field back to the initial value // Clears the value of a certain field back to the initial value
const clearValue = () => { const clearValue = () => {
const { fieldState } = fieldMap[field] const fieldInfo = getField(field)
const { defaultValue } = get(fieldState) const { fieldState } = get(fieldInfo)
const newValue = initialValues[field] ?? defaultValue const newValue = initialValues[field] ?? fieldState.defaultValue
// Update field state // Update field state
fieldState.update(state => { fieldInfo.update(state => {
state.value = newValue state.fieldState.value = newValue
state.error = null state.fieldState.error = null
return state
})
// Update form state
formState.update(state => {
state.values = { ...state.values, [field]: newValue }
delete state.errors[field]
state.valid = Object.keys(state.errors).length === 0
return state return state
}) })
} }
// Updates the validator rules for a certain field // Updates the validator rules for a certain field
const updateValidation = validationRules => { const updateValidation = validationRules => {
const { fieldState } = fieldMap[field] const fieldInfo = getField(field)
const { value, error } = get(fieldState) const { fieldState } = get(fieldInfo)
const { value, error } = fieldState
// Create new validator // Create new validator
const schemaConstraints = schema?.[field]?.constraints const schemaConstraints = schema?.[field]?.constraints
@ -166,8 +213,8 @@
) )
// Update validator // Update validator
fieldState.update(state => { fieldInfo.update(state => {
state.validator = validator state.fieldState.validator = validator
return state return state
}) })
@ -183,58 +230,48 @@
clearValue, clearValue,
updateValidation, updateValidation,
validate: () => { validate: () => {
const { fieldState } = fieldMap[field] // Validate the field by force setting the same value again
setValue(get(fieldState).value, true) const { fieldState } = get(getField(field))
return setValue(fieldState.value, true)
}, },
} }
} }
// Creates observable state data about a specific field // Provide form state and api for full control by children
const makeFieldState = (field, validator, defaultValue, fieldDisabled) => { setContext("form", {
return writable({ formState,
field, formApi,
fieldId: `id-${generateID()}`,
value: initialValues[field] ?? defaultValue,
error: null,
disabled: fieldDisabled,
defaultValue,
validator,
})
}
// Fetches the form schema from this form's dataSource, if one exists // Data source is needed by attachment fields to be able to upload files
const fetchSchema = async () => { // to the correct table ID
if (!dataSource?.tableId) { dataSource,
schema = {}
table = null
} else {
table = await API.fetchTableDefinition(dataSource?.tableId)
if (table) {
if (dataSource?.type === "query") {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
}) })
} else {
schema = table.schema || {}
}
}
}
loaded = true
}
// Load the form schema on mount // Provide form step context so that forms without any step components
onMount(fetchSchema) // register their fields to step 1
setContext("form-step", 1)
// Action context to pass to children
const actions = [
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
{ type: ActionTypes.ClearForm, callback: formApi.clear },
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
]
// Create data context to provide
$: dataContext = {
...initialValues,
...$values,
// These static values are prefixed to avoid clashes with actual columns
__valid: valid,
__currentStep: $currentStep,
__currentStepValid: $currentStepValid,
}
</script> </script>
<Provider <Provider {actions} data={dataContext}>
{actions}
data={{ ...$formState.values, tableId: dataSource?.tableId }}
>
<div use:styleable={$component.styles} class={size}> <div use:styleable={$component.styles} class={size}>
{#if loaded}
<slot /> <slot />
{/if}
</div> </div>
</Provider> </Provider>

View File

@ -25,11 +25,11 @@
> >
{#if fieldState} {#if fieldState}
<CoreTextArea <CoreTextArea
value={$fieldState.value} value={fieldState.value}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
id={$fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
/> />
{/if} {/if}

View File

@ -77,23 +77,24 @@
{#if fieldState} {#if fieldState}
{#if !optionsType || optionsType === "select"} {#if !optionsType || optionsType === "select"}
<CoreSelect <CoreSelect
value={$fieldState.value} value={fieldState.value}
id={$fieldState.fieldId} id={fieldState.fieldId}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
{options} {options}
{placeholder} {placeholder}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
getOptionLabel={flatOptions ? x => x : x => x.label} getOptionLabel={flatOptions ? x => x : x => x.label}
getOptionValue={flatOptions ? x => x : x => x.value} getOptionValue={flatOptions ? x => x : x => x.value}
{autocomplete} {autocomplete}
sort={true}
/> />
{:else if optionsType === "radio"} {:else if optionsType === "radio"}
<CoreRadioGroup <CoreRadioGroup
value={$fieldState.value} value={fieldState.value}
id={$fieldState.fieldId} id={fieldState.fieldId}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
{options} {options}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
getOptionLabel={flatOptions ? x => x : x => x.label} getOptionLabel={flatOptions ? x => x : x => x.label}

View File

@ -23,8 +23,8 @@
$: linkedTableId = fieldSchema?.tableId $: linkedTableId = fieldSchema?.tableId
$: fetchRows(linkedTableId) $: fetchRows(linkedTableId)
$: fetchTable(linkedTableId) $: fetchTable(linkedTableId)
$: singleValue = flatten($fieldState?.value)?.[0] $: singleValue = flatten(fieldState?.value)?.[0]
$: multiValue = flatten($fieldState?.value) ?? [] $: multiValue = flatten(fieldState?.value) ?? []
$: component = multiselect ? CoreMultiselect : CoreSelect $: component = multiselect ? CoreMultiselect : CoreSelect
const fetchTable = async id => { const fetchTable = async id => {
@ -81,12 +81,13 @@
{autocomplete} {autocomplete}
value={multiselect ? multiValue : singleValue} value={multiselect ? multiValue : singleValue}
on:change={multiselect ? multiHandler : singleHandler} on:change={multiselect ? multiHandler : singleHandler}
id={$fieldState.fieldId} id={fieldState.fieldId}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
getOptionLabel={getDisplayName} getOptionLabel={getDisplayName}
getOptionValue={option => option._id} getOptionValue={option => option._id}
{placeholder} {placeholder}
sort={true}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -27,11 +27,11 @@
{#if fieldState} {#if fieldState}
<CoreTextField <CoreTextField
updateOnChange={false} updateOnChange={false}
value={$fieldState.value} value={fieldState.value}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
disabled={$fieldState.disabled} disabled={fieldState.disabled}
error={$fieldState.error} error={fieldState.error}
id={$fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
{type} {type}
/> />

View File

@ -9,3 +9,4 @@ export { default as datetimefield } from "./DateTimeField.svelte"
export { default as attachmentfield } from "./AttachmentField.svelte" export { default as attachmentfield } from "./AttachmentField.svelte"
export { default as relationshipfield } from "./RelationshipField.svelte" export { default as relationshipfield } from "./RelationshipField.svelte"
export { default as passwordfield } from "./PasswordField.svelte" export { default as passwordfield } from "./PasswordField.svelte"
export { default as formstep } from "./FormStep.svelte"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.105-alpha.28", "version": "0.9.105-alpha.31",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -23,8 +23,8 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.105-alpha.28", "@budibase/auth": "^0.9.105-alpha.31",
"@budibase/string-templates": "^0.9.105-alpha.28", "@budibase/string-templates": "^0.9.105-alpha.31",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0", "aws-sdk": "^2.811.0",