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

This commit is contained in:
mike12345567 2023-11-21 18:16:11 +00:00
commit 0144a5b844
125 changed files with 2013 additions and 882 deletions

View File

@ -12,6 +12,13 @@ on:
- master - master
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
workflow_call:
inputs:
run_as_oss:
type: boolean
required: false
description: Force running checks as if it was an OSS contributor
default: false
env: env:
BRANCH: ${{ github.event.pull_request.head.ref }} BRANCH: ${{ github.event.pull_request.head.ref }}
@ -19,7 +26,7 @@ env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
NX_BASE_BRANCH: origin/${{ github.base_ref }} NX_BASE_BRANCH: origin/${{ github.base_ref }}
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' }} USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' }}
IS_OSS_CONTRIBUTOR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' }} IS_OSS_CONTRIBUTOR: ${{ inputs.run_as_oss == true || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase') }}
jobs: jobs:
lint: lint:
@ -200,6 +207,9 @@ jobs:
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Build packages - name: Build packages
run: yarn build --scope @budibase/server --scope @budibase/worker run: yarn build --scope @budibase/server --scope @budibase/worker
- name: Build backend-core for OSS contributor (required for pro)
if: ${{ env.IS_OSS_CONTRIBUTOR == 'true' }}
run: yarn build --scope @budibase/backend-core
- name: Run tests - name: Run tests
run: | run: |
cd qa-core cd qa-core

View File

@ -0,0 +1,35 @@
name: OSS contributor checks
on:
workflow_dispatch:
schedule:
- cron: "0 8,16 * * 1-5" # on weekdays at 8am and 4pm
jobs:
run-checks:
name: Publish server and worker docker images
uses: ./.github/workflows/budibase_ci.yml
with:
run_as_oss: true
secrets: inherit
notify-error:
needs: ["run-checks"]
if: ${{ failure() }}
name: Notify error
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set commit SHA
id: set_sha
run: echo "::set-output name=sha::$(git rev-parse --short ${{ github.sha }})"
- name: Notify error
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.OSS_CHECKS_WEBHOOK_URL }}
embed-title: 🚨 OSS checks failed in master
embed-url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-description: |
Git sha: `${{ steps.set_sha.outputs.sha }}`

View File

@ -7,6 +7,7 @@ on:
jobs: jobs:
release: release:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -1,13 +1,11 @@
node_modules node_modules
dist dist
*.spec.js
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
packages/server/builder packages/server/builder
packages/server/coverage packages/server/coverage
packages/worker/coverage
packages/backend-core/coverage
packages/server/client packages/server/client
packages/server/src/definitions/openapi.ts packages/server/src/definitions/openapi.ts
packages/worker/coverage
packages/backend-core/coverage
packages/builder/.routify packages/builder/.routify
packages/sdk/sdk packages/sdk/sdk
packages/pro/coverage packages/pro/coverage

View File

@ -46,11 +46,9 @@ spec:
image: minio/minio image: minio/minio
imagePullPolicy: "" imagePullPolicy: ""
livenessProbe: livenessProbe:
exec: httpGet:
command: path: /minio/health/live
- curl port: 9000
- -f
- http://localhost:9000/minio/health/live
failureThreshold: 3 failureThreshold: 3
periodSeconds: 30 periodSeconds: 30
timeoutSeconds: 20 timeoutSeconds: 20

View File

@ -1,5 +1,5 @@
{ {
"version": "2.13.10", "version": "2.13.14",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -5,7 +5,6 @@ const { getDB } = require("../db")
describe("db", () => { describe("db", () => {
describe("getDB", () => { describe("getDB", () => {
it("returns a db", async () => { it("returns a db", async () => {
const dbName = structures.db.id() const dbName = structures.db.id()
const db = getDB(dbName) const db = getDB(dbName)
expect(db).toBeDefined() expect(db).toBeDefined()

View File

@ -3,6 +3,7 @@ import { getLockClient } from "./init"
import { LockOptions, LockType } from "@budibase/types" import { LockOptions, LockType } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import { logWarn } from "../logging"
async function getClient( async function getClient(
type: LockType, type: LockType,
@ -116,7 +117,7 @@ export async function doWithLock<T>(
const result = await task() const result = await task()
return { executed: true, result } return { executed: true, result }
} catch (e: any) { } catch (e: any) {
console.warn("lock error") logWarn(`lock type: ${opts.type} error`, e)
// lock limit exceeded // lock limit exceeded
if (e.name === "LockError") { if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) { if (opts.type === LockType.TRY_ONCE) {
@ -124,11 +125,9 @@ export async function doWithLock<T>(
// due to retry count (0) exceeded // due to retry count (0) exceeded
return { executed: false } return { executed: false }
} else { } else {
console.error(e)
throw e throw e
} }
} else { } else {
console.error(e)
throw e throw e
} }
} finally { } finally {

View File

@ -75,10 +75,12 @@ export function getRedisConnectionDetails() {
} }
const [host, port] = url.split(":") const [host, port] = url.split(":")
const portNumber = parseInt(port)
return { return {
host, host,
password, password,
port: parseInt(port), // assume default port for redis if invalid found
port: isNaN(portNumber) ? 6379 : portNumber,
} }
} }

View File

@ -1,4 +1,4 @@
const _ = require('lodash/fp') const _ = require("lodash/fp")
const { structures } = require("../../../tests") const { structures } = require("../../../tests")
jest.mock("../../../src/context") jest.mock("../../../src/context")
@ -7,10 +7,9 @@ jest.mock("../../../src/db")
const context = require("../../../src/context") const context = require("../../../src/context")
const db = require("../../../src/db") const db = require("../../../src/db")
const {getCreatorCount} = require('../../../src/users/users') const { getCreatorCount } = require("../../../src/users/users")
describe("Users", () => { describe("Users", () => {
let getGlobalDBMock let getGlobalDBMock
let getGlobalUserParamsMock let getGlobalUserParamsMock
let paginationMock let paginationMock
@ -34,18 +33,18 @@ describe("Users", () => {
getGlobalDBMock.mockImplementation(() => ({ getGlobalDBMock.mockImplementation(() => ({
name: "fake-db", name: "fake-db",
allDocs: () => ({ allDocs: () => ({
rows: [...page1Data, ...page2Data] rows: [...page1Data, ...page2Data],
}) }),
})) }))
paginationMock.mockImplementationOnce(() => ({ paginationMock.mockImplementationOnce(() => ({
data: page1Data, data: page1Data,
hasNextPage: true, hasNextPage: true,
nextPage: "1" nextPage: "1",
})) }))
paginationMock.mockImplementation(() => ({ paginationMock.mockImplementation(() => ({
data: page2Data, data: page2Data,
hasNextPage: false, hasNextPage: false,
nextPage: undefined nextPage: undefined,
})) }))
const creatorsCount = await getCreatorCount() const creatorsCount = await getCreatorCount()
expect(creatorsCount).toBe(4) expect(creatorsCount).toBe(4)

View File

@ -10,6 +10,7 @@
export let disabled = false export let disabled = false
export let error = null export let error = null
export let size = "M" export let size = "M"
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -18,6 +19,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} /> <Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} />
</Field> </Field>

View File

@ -11,6 +11,7 @@
export let error = null export let error = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let options = [] export let options = []
export let helpText = null
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Combobox <Combobox
{error} {error}
{disabled} {disabled}

View File

@ -4,7 +4,6 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = false export let value = false
export let error = null
export let id = null export let id = null
export let text = null export let text = null
export let disabled = false export let disabled = false
@ -22,7 +21,6 @@
<label <label
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}" class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error}
class:checked={value} class:checked={value}
class:is-indeterminate={indeterminate} class:is-indeterminate={indeterminate}
class:readonly class:readonly

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = [] export let value = []
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -34,7 +33,6 @@
<div <div
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<label <label

View File

@ -10,7 +10,6 @@
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
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
@ -39,12 +38,10 @@
<div <div
class="spectrum-InputGroup" class="spectrum-InputGroup"
class:is-focused={open || focus} class:is-focused={open || focus}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
> >
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={open || focus} class:is-focused={open || focus}
> >

View File

@ -10,7 +10,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let enableTime = true export let enableTime = true
export let value = null export let value = null
export let placeholder = null export let placeholder = null
@ -188,7 +187,6 @@
<div <div
id={flatpickrId} id={flatpickrId}
class:is-disabled={disabled || readonly} class:is-disabled={disabled || readonly}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker" class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open} class:is-focused={open}
aria-readonly="false" aria-readonly="false"
@ -199,17 +197,7 @@
on:click={flatpickr?.open} on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
> >
{#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}
<input <input
{disabled} {disabled}
{readonly} {readonly}
@ -227,7 +215,6 @@
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button" class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1" tabindex="-1"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open} on:click={flatpickr?.open}
> >
<svg <svg

View File

@ -22,7 +22,6 @@
export let handleFileTooLarge = null export let handleFileTooLarge = null
export let handleTooManyFiles = null export let handleTooManyFiles = null
export let gallery = true export let gallery = true
export let error = null
export let fileTags = [] export let fileTags = []
export let maximum = null export let maximum = null
export let extensions = "*" export let extensions = "*"
@ -222,7 +221,6 @@
{#if showDropzone} {#if showDropzone}
<div <div
class="spectrum-Dropzone" class="spectrum-Dropzone"
class:is-invalid={!!error}
class:disabled class:disabled
role="region" role="region"
tabindex="0" tabindex="0"
@ -351,9 +349,6 @@
.spectrum-Dropzone { .spectrum-Dropzone {
user-select: none; user-select: none;
} }
.spectrum-Dropzone.is-invalid {
border-color: var(--spectrum-global-color-red-400);
}
input[type="file"] { input[type="file"] {
display: none; display: none;
} }

View File

@ -14,7 +14,6 @@
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
@ -111,27 +110,12 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} 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}
<input <input
{id} {id}
on:click on:click

View File

@ -6,7 +6,6 @@
export let id = null export let id = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
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
@ -84,7 +83,6 @@
<Picker <Picker
on:loadMore on:loadMore
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}

View File

@ -14,7 +14,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let error = null
export let fieldText = "" export let fieldText = ""
export let fieldIcon = "" export let fieldIcon = ""
export let fieldColour = "" export let fieldColour = ""
@ -113,7 +112,6 @@
class="spectrum-Picker spectrum-Picker--sizeM" class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet} class:spectrum-Picker--quiet={quiet}
{disabled} {disabled}
class:is-invalid={!!error}
class:is-open={open} class:is-open={open}
aria-haspopup="listbox" aria-haspopup="listbox"
on:click={onClick} on:click={onClick}
@ -142,16 +140,6 @@
> >
{fieldText} {fieldText}
</span> </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}
<svg <svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false" focusable="false"

View File

@ -16,7 +16,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let error = null
export let secondaryOptions = [] export let secondaryOptions = []
export let primaryOptions = [] export let primaryOptions = []
export let secondaryFieldText = "" export let secondaryFieldText = ""
@ -105,14 +104,9 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
class:is-full-width={!secondaryOptions.length} class:is-full-width={!secondaryOptions.length}

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = null export let value = null
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -40,7 +39,6 @@
<div <div
title={getOptionTitle(option)} title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized" class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<input <input

View File

@ -5,14 +5,13 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let height = null export let height = null
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
</script> </script>
<div class:error> <div>
<MarkdownEditor <MarkdownEditor
{value} {value}
{placeholder} {placeholder}
@ -27,18 +26,4 @@
</div> </div>
<style> <style>
.error :global(.EasyMDEContainer .editor-toolbar) {
border-top-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .CodeMirror) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .editor-preview-side) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
</style> </style>

View File

@ -6,7 +6,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option" export let placeholder = "Choose an option"
export let disabled = false export let disabled = false
export let error = null
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
@ -71,7 +70,6 @@
on:loadMore on:loadMore
{quiet} {quiet}
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}

View File

@ -7,7 +7,6 @@
export let value = null export let value = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -98,20 +97,9 @@
<div <div
class="spectrum-Stepper" class="spectrum-Stepper"
class:spectrum-Stepper--quiet={quiet} class:spectrum-Stepper--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} 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"> <div class="spectrum-Textfield spectrum-Stepper-textfield">
<input <input
{disabled} {disabled}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let id = null export let id = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
@ -41,20 +40,9 @@
<div <div
style={`${heightString}${minHeightString}`} style={`${heightString}${minHeightString}`}
class="spectrum-Textfield spectrum-Textfield--multiline" class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} 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}
<!-- prettier-ignore --> <!-- prettier-ignore -->
<textarea <textarea
bind:this={textarea} bind:this={textarea}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -78,19 +77,9 @@
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet} class:spectrum-Textfield--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} 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}
<input <input
bind:this={field} bind:this={field}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let appendTo = undefined export let appendTo = undefined
export let ignoreTimezones = false export let ignoreTimezones = false
export let range = false export let range = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -30,7 +31,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<DatePicker <DatePicker
{error} {error}
{disabled} {disabled}

View File

@ -17,6 +17,7 @@
export let fileTags = [] export let fileTags = []
export let maximum = undefined export let maximum = undefined
export let compact = false export let compact = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<CoreDropzone <CoreDropzone
{error} {error}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let autofocus export let autofocus
export let variables export let variables
export let showModal export let showModal
export let helpText = null
export let environmentVariablesEnabled export let environmentVariablesEnabled
export let handleUpgradePanel export let handleUpgradePanel
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<EnvDropdown <EnvDropdown
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -1,11 +1,13 @@
<script> <script>
import "@spectrum-css/fieldlabel/dist/index-vars.css" import "@spectrum-css/fieldlabel/dist/index-vars.css"
import FieldLabel from "./FieldLabel.svelte" import FieldLabel from "./FieldLabel.svelte"
import Icon from "../Icon/Icon.svelte"
export let id = null export let id = null
export let label = null export let label = null
export let labelPosition = "above" export let labelPosition = "above"
export let error = null export let error = null
export let helpText = null
export let tooltip = "" export let tooltip = ""
</script> </script>
@ -17,6 +19,10 @@
<slot /> <slot />
{#if error} {#if error}
<div class="error">{error}</div> <div class="error">{error}</div>
{:else if helpText}
<div class="helpText">
<Icon name="HelpOutline" /> <span>{helpText}</span>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -39,4 +45,21 @@
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spectrum-global-dimension-size-75); margin-top: var(--spectrum-global-dimension-size-75);
} }
.helpText {
display: flex;
margin-top: var(--spectrum-global-dimension-size-75);
align-items: center;
}
.helpText :global(svg) {
width: 14px;
color: var(--grey-5);
margin-right: 6px;
}
.helpText span {
color: var(--grey-7);
font-size: var(--spectrum-global-dimension-font-size-75);
}
</style> </style>

View File

@ -14,6 +14,7 @@
export let title = null export let title = null
export let value = null export let value = null
export let tooltip = null export let tooltip = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -22,7 +23,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<CoreFile <CoreFile
{error} {error}
{disabled} {disabled}

View File

@ -15,6 +15,7 @@
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let autocomplete export let autocomplete
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextField <TextField
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -15,6 +15,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let helpText = null
export let options = [] export let options = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -29,7 +30,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<InputDropdown <InputDropdown
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -18,6 +18,7 @@
export let autocomplete = false export let autocomplete = false
export let searchTerm = null export let searchTerm = null
export let customPopoverHeight export let customPopoverHeight
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -26,7 +27,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Multiselect <Multiselect
{error} {error}
{disabled} {disabled}

View File

@ -26,6 +26,7 @@
export let secondaryOptions = [] export let secondaryOptions = []
export let searchTerm export let searchTerm
export let showClearIcon = true export let showClearIcon = true
export let helpText = null
let primaryLabel let primaryLabel
let secondaryLabel let secondaryLabel
@ -93,7 +94,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<PickerDropdown <PickerDropdown
{searchTerm} {searchTerm}
{autocomplete} {autocomplete}

View File

@ -13,6 +13,7 @@
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionTitle = option => extractProperty(option, "label") export let getOptionTitle = option => extractProperty(option, "label")
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RadioGroup <RadioGroup
{error} {error}
{disabled} {disabled}

View File

@ -13,6 +13,7 @@
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -21,7 +22,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RichTextField <RichTextField
{error} {error}
{disabled} {disabled}

View File

@ -11,6 +11,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let inputRef export let inputRef
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,7 +20,7 @@
} }
</script> </script>
<Field {label} {labelPosition}> <Field {helpText} {label} {labelPosition}>
<Search <Search
{updateOnChange} {updateOnChange}
{disabled} {disabled}

View File

@ -26,6 +26,7 @@
export let align export let align
export let footer = null export let footer = null
export let tag = null export let tag = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
value = e.detail value = e.detail
@ -40,7 +41,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<Select <Select
{quiet} {quiet}
{error} {error}

View File

@ -11,6 +11,7 @@
export let step = 1 export let step = 1
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,6 +20,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} /> <Slider {disabled} {value} {min} {max} {step} on:change={onChange} />
</Field> </Field>

View File

@ -15,6 +15,7 @@
export let min = null export let min = null
export let max = null export let max = null
export let step = 1 export let step = 1
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Stepper <Stepper
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -12,6 +12,7 @@
export let getCaretPosition = null export let getCaretPosition = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -20,7 +21,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextArea <TextArea
bind:getCaretPosition bind:getCaretPosition
{error} {error}

View File

@ -9,6 +9,7 @@
export let text = null export let text = null
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -17,6 +18,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Switch {error} {disabled} {text} {value} on:change={onChange} /> <Switch {error} {disabled} {text} {value} on:change={onChange} />
</Field> </Field>

View File

@ -16,10 +16,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = e => { const onClick = () => {
if (!disabled) { if (!disabled) {
dispatch("click") dispatch("click")
e.stopPropagation()
} }
} }
</script> </script>

View File

@ -8,7 +8,8 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import download from "downloadjs" import download from "downloadjs"
import { API } from "api" import { API } from "api"
import { Constants, LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils } from "@budibase/frontend-core"
import { utils } from "@budibase/shared-core"
import { ROW_EXPORT_FORMATS } from "constants/backend" import { ROW_EXPORT_FORMATS } from "constants/backend"
export let view export let view
@ -32,6 +33,8 @@
}, },
] ]
$: appliedFilters = filters?.filter(filter => !filter.onEmptyFilter)
$: options = FORMATS.filter(format => { $: options = FORMATS.filter(format => {
if (formats && !formats.includes(format.key)) { if (formats && !formats.includes(format.key)) {
return false return false
@ -46,23 +49,20 @@
exportFormat = Array.isArray(options) ? options[0]?.key : [] exportFormat = Array.isArray(options) ? options[0]?.key : []
} }
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters) $: luceneFilter = LuceneUtils.buildLuceneQuery(appliedFilters)
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters) $: exportOpDisplay = buildExportOpDisplay(
sorting,
filterDisplay,
appliedFilters
)
const buildFilterLookup = () => { filterLookup = utils.filterValueToLabel()
return Object.keys(Constants.OperatorOptions).reduce((acc, key) => {
const op = Constants.OperatorOptions[key]
acc[op.value] = op.label
return acc
}, {})
}
filterLookup = buildFilterLookup()
const filterDisplay = () => { const filterDisplay = () => {
if (!filters) { if (!appliedFilters) {
return [] return []
} }
return filters.map(filter => { return appliedFilters.map(filter => {
let newFieldName = filter.field + "" let newFieldName = filter.field + ""
const parts = newFieldName.split(":") const parts = newFieldName.split(":")
parts.shift() parts.shift()
@ -77,7 +77,7 @@
const buildExportOpDisplay = (sorting, filterDisplay) => { const buildExportOpDisplay = (sorting, filterDisplay) => {
let filterDisplayConfig = filterDisplay() let filterDisplayConfig = filterDisplay()
if (sorting) { if (sorting?.sortColumn) {
filterDisplayConfig = [ filterDisplayConfig = [
...filterDisplayConfig, ...filterDisplayConfig,
{ {
@ -132,7 +132,7 @@
format: exportFormat, format: exportFormat,
}) })
downloadWithBlob(data, `export.${exportFormat}`) downloadWithBlob(data, `export.${exportFormat}`)
} else if (filters || sorting) { } else if (appliedFilters || sorting) {
let response let response
try { try {
response = await API.exportRows({ response = await API.exportRows({
@ -163,29 +163,33 @@
title="Export Data" title="Export Data"
confirmText="Export" confirmText="Export"
onConfirm={exportRows} onConfirm={exportRows}
size={filters?.length || sorting ? "M" : "S"} size={appliedFilters?.length || sorting ? "M" : "S"}
> >
{#if selectedRows?.length} {#if selectedRows?.length}
<Body size="S"> <Body size="S">
<span data-testid="exporting-n-rows">
<strong>{selectedRows?.length}</strong> <strong>{selectedRows?.length}</strong>
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`} {`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
</span>
</Body> </Body>
{:else if filters || (sorting?.sortOrder && sorting?.sortColumn)} {:else if appliedFilters?.length || (sorting?.sortOrder && sorting?.sortColumn)}
<Body size="S"> <Body size="S">
{#if !filters} {#if !appliedFilters}
<span data-testid="exporting-rows">
Exporting <strong>all</strong> rows Exporting <strong>all</strong> rows
</span>
{:else} {:else}
Filters applied <span data-testid="filters-applied">Filters applied</span>
{/if} {/if}
</Body> </Body>
<div class="table-wrap"> <div class="table-wrap" data-testid="export-config-table">
<Table <Table
schema={displaySchema} schema={displaySchema}
data={exportOpDisplay} data={exportOpDisplay}
{filters} {appliedFilters}
loading={false} loading={false}
rowCount={filters?.length + 1} rowCount={appliedFilters?.length + 1}
disableSorting={true} disableSorting={true}
allowSelectRows={false} allowSelectRows={false}
allowEditRows={false} allowEditRows={false}
@ -196,10 +200,12 @@
</div> </div>
{:else} {:else}
<Body size="S"> <Body size="S">
<span data-testid="export-all-rows">
Exporting <strong>all</strong> rows Exporting <strong>all</strong> rows
</span>
</Body> </Body>
{/if} {/if}
<span data-testid="format-select">
<Select <Select
label="Format" label="Format"
bind:value={exportFormat} bind:value={exportFormat}
@ -208,6 +214,7 @@
getOptionLabel={x => x.name} getOptionLabel={x => x.name}
getOptionValue={x => x.key} getOptionValue={x => x.key}
/> />
</span>
</ModalContent> </ModalContent>
<style> <style>

View File

@ -0,0 +1,240 @@
import { it, expect, describe, vi } from "vitest"
import { render, screen } from "@testing-library/svelte"
import "@testing-library/jest-dom"
import ExportModal from "./ExportModal.svelte"
import { utils } from "@budibase/shared-core"
const labelLookup = utils.filterValueToLabel()
const rowText = filter => {
let readableField = filter.field.split(":")[1]
let rowLabel = labelLookup[filter.operator]
let value = Array.isArray(filter.value)
? JSON.stringify(filter.value)
: filter.value
return `${readableField}${rowLabel}${value}`.trim()
}
const defaultFilters = [
{
onEmptyFilter: "all",
},
]
vi.mock("svelte", async () => {
return {
getContext: () => {
return {
hide: vi.fn(),
cancel: vi.fn(),
}
},
createEventDispatcher: vi.fn(),
onDestroy: vi.fn(),
}
})
vi.mock("api", async () => {
return {
API: {
exportView: vi.fn(),
exportRows: vi.fn(),
},
}
})
describe("Export Modal", () => {
it("show default messaging with no export config specified", () => {
render(ExportModal, {
props: {},
})
expect(screen.getByTestId("export-all-rows")).toBeVisible()
expect(screen.getByTestId("export-all-rows")).toHaveTextContent(
"Exporting all rows"
)
expect(screen.queryByTestId("export-config-table")).toBe(null)
})
it("indicate that a filter is being applied to the export", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
...defaultFilters,
],
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.getByTestId("filters-applied")).toBeVisible()
expect(screen.getByTestId("filters-applied").textContent).toBe(
"Filters applied"
)
const ele = screen.queryByTestId("export-config-table")
expect(ele).toBeVisible()
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(1)
let rowTextContent = rowText(propsCfg.filters[0])
//"CostLess than or equal to100"
expect(rows[0].textContent?.trim()).toEqual(rowTextContent)
})
it("Show only selected row messaging if rows are supplied", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
...defaultFilters,
],
sorting: {
sortColumn: "Cost",
sortOrder: "descending",
},
selectedRows: [
{
_id: "ro_ta_bb_expenses_57d5f6fe1b6640d8bb22b15f5eae62cd",
},
{
_id: "ro_ta_bb_expenses_99ce5760a53a430bab4349cd70335a07",
},
],
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.queryByTestId("export-config-table")).toBeNull()
expect(screen.queryByTestId("filters-applied")).toBeNull()
expect(screen.queryByTestId("exporting-n-rows")).toBeVisible()
expect(screen.queryByTestId("exporting-n-rows").textContent).toEqual(
"2 rows will be exported"
)
})
it("Show only the configured sort when no filters are specified", () => {
const propsCfg = {
filters: [...defaultFilters],
sorting: {
sortColumn: "Cost",
sortOrder: "descending",
},
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.queryByTestId("export-config-table")).toBeVisible()
const ele = screen.queryByTestId("export-config-table")
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(1)
expect(rows[0].textContent?.trim()).toEqual(
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
)
})
it("Display all currently configured filters and applied sort", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
{
id: "2ot-aB0gE",
field: "2:Expense Tags",
operator: "contains",
value: ["Equipment", "Services"],
valueType: "Value",
type: "array",
noValue: false,
},
...defaultFilters,
],
sorting: {
sortColumn: "Payment Due",
sortOrder: "ascending",
},
}
render(ExportModal, {
props: propsCfg,
})
const ele = screen.queryByTestId("export-config-table")
expect(ele).toBeVisible()
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(3)
let rowTextContent1 = rowText(propsCfg.filters[0])
expect(rows[0].textContent?.trim()).toEqual(rowTextContent1)
let rowTextContent2 = rowText(propsCfg.filters[1])
expect(rows[1].textContent?.trim()).toEqual(rowTextContent2)
expect(rows[2].textContent?.trim()).toEqual(
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
)
})
it("show only the valid, configured download formats", () => {
const propsCfg = {
formats: ["badger", "json"],
}
render(ExportModal, {
props: propsCfg,
})
let ele = screen.getByTestId("format-select")
expect(ele).toBeVisible()
let formatDisplay = ele.getElementsByTagName("button")[0]
expect(formatDisplay.textContent.trim()).toBe("JSON")
})
it("Load the default format config when no explicit formats are configured", () => {
render(ExportModal, {
props: {},
})
let ele = screen.getByTestId("format-select")
expect(ele).toBeVisible()
let formatDisplay = ele.getElementsByTagName("button")[0]
expect(formatDisplay.textContent.trim()).toBe("CSV")
})
})

View File

@ -21,6 +21,7 @@
export let allowHelpers = true export let allowHelpers = true
export let updateOnChange = true export let updateOnChange = true
export let drawerLeft export let drawerLeft
export let disableBindings = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
@ -62,7 +63,7 @@
{placeholder} {placeholder}
{updateOnChange} {updateOnChange}
/> />
{#if !disabled} {#if !disabled && !disableBindings}
<div <div
class="icon" class="icon"
on:click={() => { on:click={() => {

View File

@ -20,7 +20,12 @@
import analytics, { Events, EventSource } from "analytics" import analytics, { Events, EventSource } from "analytics"
import { API } from "api" import { API } from "api"
import { apps } from "stores/portal" import { apps } from "stores/portal"
import { deploymentStore, store, isOnlyUser } from "builderStore" import {
deploymentStore,
store,
isOnlyUser,
sortedScreens,
} from "builderStore"
import TourWrap from "components/portal/onboarding/TourWrap.svelte" import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -48,7 +53,7 @@
$store.upgradableVersion && $store.upgradableVersion &&
$store.version && $store.version &&
$store.upgradableVersion !== $store.version $store.upgradableVersion !== $store.version
$: canPublish = !publishing && loaded $: canPublish = !publishing && loaded && $sortedScreens.length > 0
$: lastDeployed = getLastDeployedString($deploymentStore) $: lastDeployed = getLastDeployedString($deploymentStore)
const initialiseApp = async () => { const initialiseApp = async () => {
@ -175,7 +180,12 @@
<div class="app-action-button preview"> <div class="app-action-button preview">
<div class="app-action"> <div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}> <ActionButton
disabled={$sortedScreens.length === 0}
quiet
icon="PlayCircle"
on:click={previewApp}
>
Preview Preview
</ActionButton> </ActionButton>
</div> </div>

View File

@ -21,7 +21,8 @@
$: schemaComponents = getContextProviderComponents( $: schemaComponents = getContextProviderComponents(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"schema" "schema",
{ includeSelf: nested }
) )
$: providerOptions = getProviderOptions(formComponents, schemaComponents) $: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields(parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)

View File

@ -4,10 +4,15 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getEventContextBindings } from "builderStore/dataBinding"
export let componentInstance
export let componentBindings export let componentBindings
export let bindings export let bindings
export let value export let value
export let key
export let nested
export let max
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -15,12 +20,18 @@
$: buttonList = sanitizeValue(value) || [] $: buttonList = sanitizeValue(value) || []
$: buttonCount = buttonList.length $: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({
componentInstance,
settingKey: key,
})
$: allBindings = [...bindings, ...eventContextBindings]
$: itemProps = { $: itemProps = {
componentBindings: componentBindings || [], componentBindings: componentBindings || [],
bindings, bindings: allBindings,
removeButton, removeButton,
canRemove: buttonCount > 1, nested,
} }
$: canAddButtons = max == null || buttonList.length < max
const sanitizeValue = val => { const sanitizeValue = val => {
return val?.map(button => { return val?.map(button => {
@ -86,11 +97,16 @@
focus={focusItem} focus={focusItem}
draggable={buttonCount > 1} draggable={buttonCount > 1}
/> />
{/if}
<div class="list-footer" on:click={addButton}> <div
class="list-footer"
class:disabled={!canAddButtons}
on:click={addButton}
class:empty={!buttonCount}
>
<div class="add-button">Add button</div> <div class="add-button">Add button</div>
</div> </div>
{/if}
</div> </div>
<style> <style>
@ -120,15 +136,21 @@
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)); var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
cursor: pointer; cursor: pointer;
} }
.list-footer.empty {
.add-button { border-radius: 4px;
margin: var(--spacing-s); }
.list-footer.disabled {
color: var(--spectrum-global-color-gray-500);
pointer-events: none;
} }
.list-footer:hover { .list-footer:hover {
background-color: var( background-color: var(
--spectrum-table-row-background-color-hover, --spectrum-table-row-background-color-hover,
var(--spectrum-alias-highlight-hover) var(--spectrum-alias-highlight-hover)
); );
} }
.add-button {
margin: var(--spacing-s);
}
</style> </style>

View File

@ -9,11 +9,33 @@
export let bindings export let bindings
export let anchor export let anchor
export let removeButton export let removeButton
export let canRemove export let nested
$: readableText = isJSBinding(item.text) $: readableText = isJSBinding(item.text)
? "(JavaScript function)" ? "(JavaScript function)"
: runtimeToReadableBinding([...bindings, componentBindings], item.text) : runtimeToReadableBinding([...bindings, componentBindings], item.text)
// If this is a nested setting (for example inside a grid or form block) then
// we need to mark all the settings of the actual buttons as nested too, to
// allow us to reference context provided by the block.
// We will need to update this in future if the normal button component
// gets broken into multiple settings sections, as we assume a flat array.
const updatedNestedFlags = settings => {
if (!nested || !settings?.length) {
return settings
}
let newSettings = settings.map(setting => ({
...setting,
nested: true,
}))
// We need to prevent bindings for the button names because of how grid
// blocks work. This is an edge case but unavoidable.
let name = newSettings.find(x => x.key === "text")
if (name) {
name.disableBindings = true
}
return newSettings
}
</script> </script>
<div class="list-item-body"> <div class="list-item-body">
@ -24,12 +46,12 @@
{componentBindings} {componentBindings}
{bindings} {bindings}
on:change on:change
parseSettings={updatedNestedFlags}
/> />
<div class="field-label">{readableText || "Button"}</div> <div class="field-label">{readableText || "Button"}</div>
</div> </div>
<div class="list-item-right"> <div class="list-item-right">
<Icon <Icon
disabled={!canRemove}
size="S" size="S"
name="Close" name="Close"
hoverable hoverable

View File

@ -18,6 +18,7 @@
export let value export let value
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let sanitisedFields let sanitisedFields
let fieldList let fieldList
let schema let schema

View File

@ -23,6 +23,7 @@
export let highlighted = false export let highlighted = false
export let propertyFocus = false export let propertyFocus = false
export let info = null export let info = null
export let disableBindings = false
$: nullishValue = value == null || value === "" $: nullishValue = value == null || value === ""
$: allBindings = getAllBindings(bindings, componentBindings, nested) $: allBindings = getAllBindings(bindings, componentBindings, nested)
@ -99,6 +100,7 @@
{nested} {nested}
{key} {key}
{type} {type}
{disableBindings}
{...props} {...props}
on:drawerHide on:drawerHide
on:drawerShow on:drawerShow

View File

@ -32,7 +32,9 @@
} }
</script> </script>
<ActionButton on:click={modal.show}>{layoutMap[value].name}</ActionButton> <ActionButton on:click={modal.show}>
{layoutMap[value || "mainSidebar"].name}
</ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ModalContent <ModalContent
onConfirm={() => dispatch("change", selected)} onConfirm={() => dispatch("change", selected)}

View File

@ -404,7 +404,7 @@
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
const datasourceUrl = datasource?.config.url const datasourceUrl = datasource?.config.url
const qs = query?.fields.queryString const qs = query?.fields.queryString
breakQs = restUtils.breakQueryString(encodeURI(qs)) breakQs = restUtils.breakQueryString(encodeURI(qs ?? ""))
breakQs = runtimeToReadableMap(mergedBindings, breakQs) breakQs = runtimeToReadableMap(mergedBindings, breakQs)
const path = query.fields.path const path = query.fields.path

View File

@ -179,6 +179,7 @@
highlighted={$store.highlightedSettingKey === setting.key} highlighted={$store.highlightedSettingKey === setting.key}
propertyFocus={$store.propertyFocus === setting.key} propertyFocus={$store.propertyFocus === setting.key}
info={setting.info} info={setting.info}
disableBindings={setting.disableBindings}
props={{ props={{
// Generic settings // Generic settings
placeholder: setting.placeholder || null, placeholder: setting.placeholder || null,

View File

@ -0,0 +1,65 @@
<xml version="1.0" />
<svg
xmlns="http://www.w3.org/2000/svg"
enable-background="new 0 0 48 48"
height="48px"
viewBox="0 0 48 48"
width="48px"
xml:space="preserve"
>
<g>
<g>
<path
d="M9,42H3c-0.552,0-1-0.449-1-1v-3.5C2,37.224,2.224,37,2.5,37S3,37.224,3,37.5V41h6 c0.276,0,0.5,0.224,0.5,0.5S9.276,42,9,42z"
/>
<path
d="M45,42h-6c-0.276,0-0.5-0.224-0.5-0.5S38.724,41,39,41h6V13H3v27c0,0.276-0.224,0.5-0.5,0.5S2,40.276,2,40 V12h44v29C46,41.551,45.552,42,45,42z"
/>
<g>
<path
d="M45.5,13h-43C2.224,13,2,12.776,2,12.5v-5C2,6.673,2.673,6,3.5,6h41C45.327,6,46,6.673,46,7.5v5 C46,12.776,45.776,13,45.5,13z M3,12h42V7.5C45,7.224,44.775,7,44.5,7h-41C3.225,7,3,7.224,3,7.5V12z"
/>
</g>
<g>
<g>
<path
d="M16.5,11c-0.827,0-1.5-0.673-1.5-1.5S15.673,8,16.5,8S18,8.673,18,9.5S17.327,11,16.5,11z M16.5,9 C16.225,9,16,9.224,16,9.5s0.225,0.5,0.5,0.5S17,9.776,17,9.5S16.775,9,16.5,9z"
/>
</g>
<g>
<path
d="M11.5,11c-0.827,0-1.5-0.673-1.5-1.5S10.673,8,11.5,8S13,8.673,13,9.5S12.327,11,11.5,11z M11.5,9 C11.225,9,11,9.224,11,9.5s0.225,0.5,0.5,0.5S12,9.776,12,9.5S11.775,9,11.5,9z"
/>
</g>
<g>
<path
d="M6.5,11C5.673,11,5,10.327,5,9.5S5.673,8,6.5,8S8,8.673,8,9.5S7.327,11,6.5,11z M6.5,9 C6.225,9,6,9.224,6,9.5S6.225,10,6.5,10S7,9.776,7,9.5S6.775,9,6.5,9z"
/>
</g>
</g>
</g>
<g>
<g>
<path
d="M35.696,44H12.304c-0.728,0-1.313-0.284-1.605-0.779c-0.289-0.489-0.259-1.126,0.084-1.749L22.58,19.996 c0.709-1.285,2.132-1.285,2.839,0l11.799,21.477v0c0.343,0.623,0.373,1.26,0.084,1.749C37.01,43.716,36.424,44,35.696,44z M24,20 c-0.176,0-0.379,0.179-0.544,0.478L11.659,41.954c-0.168,0.306-0.205,0.582-0.101,0.758C11.667,42.895,11.938,43,12.304,43 h23.393c0.365,0,0.637-0.105,0.745-0.288c0.104-0.177,0.067-0.453-0.101-0.758v0L24.543,20.478C24.379,20.179,24.176,20,24,20z"
/>
</g>
<g>
<path
d="M24,36L24,36c-0.225,0-0.421-0.15-0.481-0.366C23.456,35.412,22,30.169,22,28c0-1.103,0.897-2,2-2 s2,0.897,2,2c0,2.232-1.457,7.417-1.519,7.636C24.42,35.851,24.224,36,24,36z M24,27c-0.552,0-1,0.449-1,1 c0,1.266,0.569,3.793,1.002,5.531C24.435,31.806,25,29.301,25,28C25,27.449,24.552,27,24,27z"
/>
</g>
<g>
<path
d="M24,41c-1.103,0-2-0.897-2-2s0.897-2,2-2s2,0.897,2,2S25.103,41,24,41z M24,38c-0.552,0-1,0.449-1,1 s0.448,1,1,1s1-0.449,1-1S24.552,38,24,38z"
/>
</g>
</g>
</g>
</svg>
<style>
svg {
fill: var(--ink);
}
</style>

View File

@ -1,8 +1,10 @@
<script> <script>
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import { apps, auth, sideBarCollapsed } from "stores/portal" import { apps, auth, sideBarCollapsed } from "stores/portal"
import { ActionButton } from "@budibase/bbui" import { Link, Body, ActionButton } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
$: app = $apps.find(app => app.appId === $params.appId) $: app = $apps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app) $: iframeUrl = getIframeURL(app)
@ -14,6 +16,18 @@
} }
return `/${app.devId}` return `/${app.devId}`
} }
let noScreens = false
// Normally fetched in builder/src/pages/builder/app/[application]/_layout.svelte
const fetchScreens = async appId => {
if (!appId) return
const pkg = await API.fetchAppPackage(appId)
noScreens = pkg.screens.length === 0
}
$: fetchScreens(app?.devId)
</script> </script>
<div class="container"> <div class="container">
@ -45,6 +59,7 @@
</ActionButton> </ActionButton>
{/if} {/if}
<ActionButton <ActionButton
disabled={noScreens}
quiet quiet
icon="LinkOut" icon="LinkOut"
on:click={() => window.open(iframeUrl, "_blank")} on:click={() => window.open(iframeUrl, "_blank")}
@ -52,7 +67,19 @@
Fullscreen Fullscreen
</ActionButton> </ActionButton>
</div> </div>
{#if noScreens}
<div class="noScreens">
<ErrorSVG />
<Body>You haven't added any screens to your app yet.</Body>
<Body>
<Link size="L" href={`/builder/app/${app.devId}/design`}
>Click here</Link
> to add some.
</Body>
</div>
{:else}
<iframe src={iframeUrl} title={app.name} /> <iframe src={iframeUrl} title={app.name} />
{/if}
</div> </div>
<style> <style>
@ -64,6 +91,7 @@
align-items: stretch; align-items: stretch;
padding: 0 var(--spacing-l) var(--spacing-l) var(--spacing-l); padding: 0 var(--spacing-l) var(--spacing-l) var(--spacing-l);
} }
.header { .header {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -71,9 +99,27 @@
gap: var(--spacing-xs); gap: var(--spacing-xs);
flex: 0 0 50px; flex: 0 0 50px;
} }
iframe { iframe {
flex: 1 1 auto; flex: 1 1 auto;
border-radius: var(--spacing-s); border-radius: var(--spacing-s);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
} }
.noScreens {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
}
.noScreens :global(svg) {
width: 100px;
height: 100px;
margin-bottom: 10px;
}
</style> </style>

View File

@ -270,7 +270,6 @@
{ {
"type": "buttonConfiguration", "type": "buttonConfiguration",
"key": "buttons", "key": "buttons",
"nested": true,
"defaultValue": [ "defaultValue": [
{ {
"type": "cta", "type": "cta",
@ -2732,6 +2731,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -2863,6 +2867,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -2960,6 +2969,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3176,6 +3190,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
@ -3336,6 +3355,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3560,6 +3584,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3658,6 +3687,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3800,6 +3834,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3895,6 +3934,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
@ -4145,6 +4189,11 @@
"label": "Label", "label": "Label",
"key": "label" "key": "label"
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "text", "type": "text",
"label": "Extensions", "label": "Extensions",
@ -4248,6 +4297,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -4356,6 +4410,11 @@
"key": "defaultValue", "key": "defaultValue",
"supportsConditions": false "supportsConditions": false
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -6339,8 +6398,29 @@
"label": "High contrast", "label": "High contrast",
"key": "stripeRows", "key": "stripeRows",
"defaultValue": false "defaultValue": false
},
{
"section": true,
"name": "Buttons",
"settings": [
{
"type": "buttonConfiguration",
"key": "buttons",
"nested": true,
"max": 3,
"context": [
{
"label": "Clicked row",
"key": "row"
} }
] ]
}
]
}
],
"context": {
"type": "schema"
}
}, },
"bbreferencefield": { "bbreferencefield": {
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
@ -6375,6 +6455,11 @@
"label": "Default value", "label": "Default value",
"key": "defaultValue" "key": "defaultValue"
}, },
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",

View File

@ -2,6 +2,7 @@
// NOTE: this is not a block - it's just named as such to avoid confusing users, // NOTE: this is not a block - it's just named as such to avoid confusing users,
// because it functions similarly to one // because it functions similarly to one
import { getContext } from "svelte" import { getContext } from "svelte"
import { get } from "svelte/store"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
// table is actually any datasource, but called table for legacy compatibility // table is actually any datasource, but called table for legacy compatibility
@ -16,12 +17,21 @@
export let fixedRowHeight = null export let fixedRowHeight = null
export let columns = null export let columns = null
export let onRowClick = null export let onRowClick = null
export let buttons = null
const context = getContext("context")
const component = getContext("component") const component = getContext("component")
const { styleable, API, builderStore, notificationStore } = getContext("sdk") const {
styleable,
API,
builderStore,
notificationStore,
enrichButtonActions,
} = getContext("sdk")
$: columnWhitelist = columns?.map(col => col.name) $: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns) $: schemaOverrides = getSchemaOverrides(columns)
$: enrichedButtons = enrichButtons(buttons)
const getSchemaOverrides = columns => { const getSchemaOverrides = columns => {
let overrides = {} let overrides = {}
@ -33,6 +43,25 @@
}) })
return overrides return overrides
} }
const enrichButtons = buttons => {
if (!buttons?.length) {
return null
}
return buttons.map(settings => ({
size: "M",
text: settings.text,
type: settings.type,
onClick: async row => {
// We add a fake context binding in here, which allows us to pretend
// that the grid provides a "schema" binding - that lets us use the
// clicked row in things like save row actions
const enrichedContext = { ...get(context), [get(component).id]: row }
const fn = enrichButtonActions(settings.onClick, enrichedContext)
return await fn?.({ row })
},
}))
}
</script> </script>
<div <div
@ -58,6 +87,7 @@
showControls={false} showControls={false}
notifySuccess={notificationStore.actions.success} notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error} notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
on:rowclick={e => onRowClick?.({ row: e.detail })} on:rowclick={e => onRowClick?.({ row: e.detail })}
/> />
</div> </div>

View File

@ -13,6 +13,7 @@
export let onChange export let onChange
export let maximum = undefined export let maximum = undefined
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -76,6 +77,7 @@
{readonly} {readonly}
{validation} {validation}
{span} {span}
{helpText}
type="attachment" type="attachment"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi

View File

@ -11,6 +11,7 @@
export let validation export let validation
export let defaultValue export let defaultValue
export let onChange export let onChange
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -42,6 +43,7 @@
{disabled} {disabled}
{readonly} {readonly}
{validation} {validation}
{helpText}
defaultValue={isTruthy(defaultValue)} defaultValue={isTruthy(defaultValue)}
type="boolean" type="boolean"
bind:fieldState bind:fieldState

View File

@ -16,6 +16,7 @@
export let beepFrequency export let beepFrequency
export let customFrequency export let customFrequency
export let preferredCamera export let preferredCamera
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -38,6 +39,7 @@
{validation} {validation}
{defaultValue} {defaultValue}
{type} {type}
{helpText}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
> >

View File

@ -15,6 +15,7 @@
export let defaultValue export let defaultValue
export let onChange export let onChange
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -35,6 +36,7 @@
{validation} {validation}
{defaultValue} {defaultValue}
{span} {span}
{helpText}
type="datetime" type="datetime"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi

View File

@ -1,6 +1,7 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
import { Icon } from "@budibase/bbui"
export let label export let label
export let field export let field
@ -13,6 +14,7 @@
export let readonly = false export let readonly = false
export let validation export let validation
export let span = 6 export let span = 6
export let helpText = null
// Get contexts // Get contexts
const formContext = getContext("form") const formContext = getContext("form")
@ -97,7 +99,14 @@
{:else} {:else}
<slot /> <slot />
{#if fieldState.error} {#if fieldState.error}
<div class="error">{fieldState.error}</div> <div class="error">
<Icon name="Alert" />
<span>{fieldState.error}</span>
</div>
{:else if helpText}
<div class="helpText">
<Icon name="HelpOutline" /> <span>{helpText}</span>
</div>
{/if} {/if}
{/if} {/if}
</div> </div>
@ -127,13 +136,45 @@
position: relative; position: relative;
width: 100%; width: 100%;
} }
.error { .error {
display: flex;
margin-top: var(--spectrum-global-dimension-size-75);
align-items: center;
}
.error :global(svg) {
width: 14px;
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
);
margin-right: 4px;
}
.error span {
color: var( color: var(
--spectrum-semantic-negative-color-default, --spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500) var(--spectrum-global-color-red-500)
); );
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
}
.helpText {
display: flex;
margin-top: var(--spectrum-global-dimension-size-75); margin-top: var(--spectrum-global-dimension-size-75);
align-items: center;
}
.helpText :global(svg) {
width: 14px;
color: var(--grey-7);
margin-right: 6px;
}
.helpText span {
color: var(--grey-5);
font-size: var(--spectrum-global-dimension-font-size-75);
} }
.spectrum-FieldLabel--right, .spectrum-FieldLabel--right,
.spectrum-FieldLabel--left { .spectrum-FieldLabel--left {

View File

@ -10,6 +10,7 @@
export let readonly = false export let readonly = false
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
export let helpText = null
const component = getContext("component") const component = getContext("component")
const validation = [ const validation = [
@ -52,6 +53,7 @@
{readonly} {readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{helpText}
type="json" type="json"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi

View File

@ -13,6 +13,7 @@
export let defaultValue = "" export let defaultValue = ""
export let format = "auto" export let format = "auto"
export let onChange export let onChange
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -62,6 +63,7 @@
{readonly} {readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{helpText}
type="longform" type="longform"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi

View File

@ -19,6 +19,7 @@
export let optionsType = "select" export let optionsType = "select"
export let direction = "vertical" export let direction = "vertical"
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -60,6 +61,7 @@
{readonly} {readonly}
{validation} {validation}
{span} {span}
{helpText}
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
type="array" type="array"
bind:fieldState bind:fieldState

View File

@ -20,6 +20,7 @@
export let onChange export let onChange
export let sort = true export let sort = true
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -51,6 +52,7 @@
{validation} {validation}
{defaultValue} {defaultValue}
{span} {span}
{helpText}
type="options" type="options"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi

View File

@ -20,6 +20,7 @@
export let datasourceType = "table" export let datasourceType = "table"
export let primaryDisplay export let primaryDisplay
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -192,6 +193,7 @@
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
{type} {type}
{span} {span}
{helpText}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
bind:fieldSchema bind:fieldSchema

View File

@ -13,6 +13,7 @@
export let align export let align
export let onChange export let onChange
export let span export let span
export let helpText = null
let fieldState let fieldState
let fieldApi let fieldApi
@ -33,6 +34,7 @@
{validation} {validation}
{defaultValue} {defaultValue}
{span} {span}
{helpText}
type={type === "number" ? "number" : "string"} type={type === "number" ? "number" : "string"}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -44,7 +46,6 @@
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly} readonly={fieldState.readonly}
error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
{type} {type}

View File

@ -14,6 +14,7 @@ import {
dndIsDragging, dndIsDragging,
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore,
} from "stores" } from "stores"
import { styleable } from "utils/styleable" import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "utils/linkable"
@ -24,9 +25,13 @@ import BlockComponent from "components/BlockComponent.svelte"
import { ActionTypes } from "./constants" import { ActionTypes } from "./constants"
import { fetchDatasourceSchema } from "./utils/schema.js" import { fetchDatasourceSchema } from "./utils/schema.js"
import { getAPIKey } from "./utils/api.js" import { getAPIKey } from "./utils/api.js"
import { enrichButtonActions } from "./utils/buttonActions.js"
import { processStringSync, makePropSafe } from "@budibase/string-templates"
export default { export default {
API, API,
// Stores
authStore, authStore,
notificationStore, notificationStore,
routeStore, routeStore,
@ -41,13 +46,23 @@ export default {
currentRole, currentRole,
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore,
// Utils
styleable, styleable,
linkable, linkable,
getAction, getAction,
fetchDatasourceSchema, fetchDatasourceSchema,
Provider,
ActionTypes,
getAPIKey, getAPIKey,
enrichButtonActions,
processStringSync,
makePropSafe,
// Components
Provider,
Block, Block,
BlockComponent, BlockComponent,
// Constants
ActionTypes,
} }

View File

@ -15,7 +15,7 @@
$: style = getStyle(width, selectedUser) $: style = getStyle(width, selectedUser)
const getStyle = (width, selectedUser) => { const getStyle = (width, selectedUser) => {
let style = `flex: 0 0 ${width}px;` let style = width === "auto" ? "width: auto;" : `flex: 0 0 ${width}px;`
if (selectedUser) { if (selectedUser) {
style += `--user-color:${selectedUser.color};` style += `--user-color:${selectedUser.color};`
} }

View File

@ -0,0 +1,144 @@
<script>
import { getContext, onMount } from "svelte"
import { Button } from "@budibase/bbui"
import GridCell from "../cells/GridCell.svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
const {
renderedRows,
hoveredRowId,
props,
width,
rows,
focusedRow,
selectedRows,
visibleColumns,
scroll,
isDragging,
buttonColumnWidth,
} = getContext("grid")
let measureContainer
$: buttons = $props.buttons?.slice(0, 3) || []
$: columnsWidth = $visibleColumns.reduce(
(total, col) => (total += col.width),
0
)
$: end = columnsWidth - 1 - $scroll.left
$: left = Math.min($width - $buttonColumnWidth, end)
const handleClick = async (button, row) => {
await button.onClick?.(rows.actions.cleanRow(row))
// Refresh the row in case it changed
await rows.actions.refreshRow(row._id)
}
onMount(() => {
const observer = new ResizeObserver(entries => {
const width = entries?.[0]?.contentRect?.width ?? 0
buttonColumnWidth.set(width)
})
observer.observe(measureContainer)
})
</script>
<!-- Hidden copy of buttons to measure -->
<div class="measure" bind:this={measureContainer}>
<GridCell width="auto">
<div class="buttons">
{#each buttons as button}
<Button size="S">
{button.text || "Button"}
</Button>
{/each}
</div>
</GridCell>
</div>
<div
class="button-column"
style="left:{left}px"
class:hidden={$buttonColumnWidth === 0}
>
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
<GridScrollWrapper scrollVertically attachHandlers>
{#each $renderedRows as row}
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
{@const rowFocused = $focusedRow?._id === row._id}
<div
class="row"
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
>
<GridCell
width="auto"
rowIdx={row.__idx}
selected={rowSelected}
highlighted={rowHovered || rowFocused}
>
<div class="buttons">
{#each buttons as button}
<Button
newStyles
size="S"
cta={button.type === "cta"}
primary={button.type === "primary"}
secondary={button.type === "secondary"}
warning={button.type === "warning"}
overBackground={button.type === "overBackground"}
on:click={() => handleClick(button, row)}
>
{button.text || "Button"}
</Button>
{/each}
</div>
</GridCell>
</div>
{/each}
</GridScrollWrapper>
</div>
</div>
<style>
.button-column {
display: flex;
flex-direction: column;
background: var(--cell-background);
position: absolute;
top: 0;
}
.button-column.hidden {
opacity: 0;
}
.content {
position: relative;
flex: 1 1 auto;
}
.row {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
}
.buttons {
display: flex;
align-items: center;
padding: 0 var(--cell-padding);
gap: var(--cell-padding);
height: inherit;
}
/* Add left cell border */
.button-column :global(.cell) {
border-left: var(--cell-border);
}
/* Hidden copy of buttons to measure width against */
.measure {
position: absolute;
opacity: 0;
pointer-events: none;
}
</style>

View File

@ -48,6 +48,7 @@
export let fixedRowHeight = null export let fixedRowHeight = null
export let notifySuccess = null export let notifySuccess = null
export let notifyError = null export let notifyError = null
export let buttons = null
// Unique identifier for DOM nodes inside this instance // Unique identifier for DOM nodes inside this instance
const rand = Math.random() const rand = Math.random()
@ -99,6 +100,7 @@
fixedRowHeight, fixedRowHeight,
notifySuccess, notifySuccess,
notifyError, notifyError,
buttons,
}) })
// Set context for children to consume // Set context for children to consume

View File

@ -3,6 +3,7 @@
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import GridRow from "./GridRow.svelte" import GridRow from "./GridRow.svelte"
import { BlankRowID } from "../lib/constants" import { BlankRowID } from "../lib/constants"
import ButtonColumn from "./ButtonColumn.svelte"
const { const {
bounds, bounds,
@ -13,6 +14,7 @@
dispatch, dispatch,
isDragging, isDragging,
config, config,
props,
} = getContext("grid") } = getContext("grid")
let body let body
@ -54,6 +56,9 @@
/> />
{/if} {/if}
</GridScrollWrapper> </GridScrollWrapper>
{#if $props.buttons?.length}
<ButtonColumn />
{/if}
</div> </div>
<style> <style>

View File

@ -314,8 +314,12 @@ export const createActions = context => {
// Refreshes a specific row // Refreshes a specific row
const refreshRow = async id => { const refreshRow = async id => {
try {
const row = await datasource.actions.getRow(id) const row = await datasource.actions.getRow(id)
replaceRow(id, row) replaceRow(id, row)
} catch {
// Do nothing - we probably just don't support refreshing individual rows
}
} }
// Refreshes all data // Refreshes all data

View File

@ -20,8 +20,15 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { rows, visibleColumns, stickyColumn, rowHeight, width, height } = const {
context rows,
visibleColumns,
stickyColumn,
rowHeight,
width,
height,
buttonColumnWidth,
} = context
// Memoize store primitives // Memoize store primitives
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0) const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
@ -40,9 +47,10 @@ export const deriveStores = context => {
// Derive horizontal limits // Derive horizontal limits
const contentWidth = derived( const contentWidth = derived(
[visibleColumns, stickyColumnWidth], [visibleColumns, stickyColumnWidth, buttonColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => { ([$visibleColumns, $stickyColumnWidth, $buttonColumnWidth]) => {
let width = GutterWidth + Padding + $stickyColumnWidth const space = Math.max(Padding, $buttonColumnWidth - 1)
let width = GutterWidth + space + $stickyColumnWidth
$visibleColumns.forEach(col => { $visibleColumns.forEach(col => {
width += col.width width += col.width
}) })

View File

@ -18,6 +18,7 @@ export const createStores = context => {
const previousFocusedRowId = writable(null) const previousFocusedRowId = writable(null)
const gridFocused = writable(false) const gridFocused = writable(false)
const isDragging = writable(false) const isDragging = writable(false)
const buttonColumnWidth = writable(0)
// Derive the current focused row ID // Derive the current focused row ID
const focusedRowId = derived( const focusedRowId = derived(
@ -51,6 +52,7 @@ export const createStores = context => {
rowHeight, rowHeight,
gridFocused, gridFocused,
isDragging, isDragging,
buttonColumnWidth,
selectedRows: { selectedRows: {
...selectedRows, ...selectedRows,
actions: { actions: {

View File

@ -1,8 +1,8 @@
const { Curl } = require("../../curl") const { Curl } = require("../../curl")
const fs = require("fs") const fs = require("fs")
const path = require('path') const path = require("path")
const getData = (file) => { const getData = file => {
return fs.readFileSync(path.join(__dirname, `./data/${file}.txt`), "utf8") return fs.readFileSync(path.join(__dirname, `./data/${file}.txt`), "utf8")
} }
@ -28,7 +28,7 @@ describe("Curl Import", () => {
expect(supported).toBe(false) expect(supported).toBe(false)
}) })
const init = async (file) => { const init = async file => {
await curl.isSupported(getData(file)) await curl.isSupported(getData(file))
} }
@ -39,8 +39,7 @@ describe("Curl Import", () => {
}) })
describe("Returns queries", () => { describe("Returns queries", () => {
const getQueries = async file => {
const getQueries = async (file) => {
await init(file) await init(file)
const queries = await curl.getQueries() const queries = await curl.getQueries()
expect(queries.length).toBe(1) expect(queries.length).toBe(1)
@ -77,7 +76,10 @@ describe("Curl Import", () => {
it("populates headers", async () => { it("populates headers", async () => {
await testHeaders("get", {}) await testHeaders("get", {})
await testHeaders("headers", { "x-bb-header-1" : "123", "x-bb-header-2" : "456"} ) await testHeaders("headers", {
"x-bb-header-1": "123",
"x-bb-header-2": "456",
})
}) })
const testQuery = async (file, queryString) => { const testQuery = async (file, queryString) => {
@ -91,12 +93,14 @@ describe("Curl Import", () => {
const testBody = async (file, body) => { const testBody = async (file, body) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].fields.requestBody).toStrictEqual(JSON.stringify(body, null, 2)) expect(queries[0].fields.requestBody).toStrictEqual(
JSON.stringify(body, null, 2)
)
} }
it("populates body", async () => { it("populates body", async () => {
await testBody("get", undefined) await testBody("get", undefined)
await testBody("post", { "key" : "val" }) await testBody("post", { key: "val" })
await testBody("empty-body", {}) await testBody("empty-body", {})
}) })
}) })

View File

@ -1,9 +1,12 @@
const { OpenAPI2 } = require("../../openapi2") const { OpenAPI2 } = require("../../openapi2")
const fs = require("fs") const fs = require("fs")
const path = require('path') const path = require("path")
const getData = (file, extension) => { const getData = (file, extension) => {
return fs.readFileSync(path.join(__dirname, `./data/${file}/${file}.${extension}`), "utf8") return fs.readFileSync(
path.join(__dirname, `./data/${file}/${file}.${extension}`),
"utf8"
)
} }
describe("OpenAPI2 Import", () => { describe("OpenAPI2 Import", () => {
@ -49,7 +52,7 @@ describe("OpenAPI2 Import", () => {
}) })
describe("Returns queries", () => { describe("Returns queries", () => {
const indexQueries = (queries) => { const indexQueries = queries => {
return queries.reduce((acc, query) => { return queries.reduce((acc, query) => {
acc[query.name] = query acc[query.name] = query
return acc return acc
@ -72,12 +75,12 @@ describe("OpenAPI2 Import", () => {
it("populates verb", async () => { it("populates verb", async () => {
const assertions = { const assertions = {
"createEntity" : "create", createEntity: "create",
"getEntities" : "read", getEntities: "read",
"getEntity" : "read", getEntity: "read",
"updateEntity" : "update", updateEntity: "update",
"patchEntity" : "patch", patchEntity: "patch",
"deleteEntity" : "delete" deleteEntity: "delete",
} }
await runTests("crud", testVerb, assertions) await runTests("crud", testVerb, assertions)
}) })
@ -91,12 +94,12 @@ describe("OpenAPI2 Import", () => {
it("populates path", async () => { it("populates path", async () => {
const assertions = { const assertions = {
"createEntity" : "http://example.com/entities", createEntity: "http://example.com/entities",
"getEntities" : "http://example.com/entities", getEntities: "http://example.com/entities",
"getEntity" : "http://example.com/entities/{{entityId}}", getEntity: "http://example.com/entities/{{entityId}}",
"updateEntity" : "http://example.com/entities/{{entityId}}", updateEntity: "http://example.com/entities/{{entityId}}",
"patchEntity" : "http://example.com/entities/{{entityId}}", patchEntity: "http://example.com/entities/{{entityId}}",
"deleteEntity" : "http://example.com/entities/{{entityId}}" deleteEntity: "http://example.com/entities/{{entityId}}",
} }
await runTests("crud", testPath, assertions) await runTests("crud", testPath, assertions)
}) })
@ -114,22 +117,20 @@ describe("OpenAPI2 Import", () => {
it("populates headers", async () => { it("populates headers", async () => {
const assertions = { const assertions = {
"createEntity" : { createEntity: {
...contentTypeHeader ...contentTypeHeader,
}, },
"getEntities" : { getEntities: {},
getEntity: {},
updateEntity: {
...contentTypeHeader,
}, },
"getEntity" : { patchEntity: {
...contentTypeHeader,
}, },
"updateEntity" : { deleteEntity: {
...contentTypeHeader
},
"patchEntity" : {
...contentTypeHeader
},
"deleteEntity" : {
"x-api-key": "{{x-api-key}}", "x-api-key": "{{x-api-key}}",
} },
} }
await runTests("crud", testHeaders, assertions) await runTests("crud", testHeaders, assertions)
@ -138,18 +139,20 @@ describe("OpenAPI2 Import", () => {
const testQuery = async (file, extension, assertions) => { const testQuery = async (file, extension, assertions) => {
const queries = await getQueries(file, extension) const queries = await getQueries(file, extension)
for (let [operationId, queryString] of Object.entries(assertions)) { for (let [operationId, queryString] of Object.entries(assertions)) {
expect(queries[operationId].fields.queryString).toStrictEqual(queryString) expect(queries[operationId].fields.queryString).toStrictEqual(
queryString
)
} }
} }
it("populates query", async () => { it("populates query", async () => {
const assertions = { const assertions = {
"createEntity" : "", createEntity: "",
"getEntities" : "page={{page}}&size={{size}}", getEntities: "page={{page}}&size={{size}}",
"getEntity" : "", getEntity: "",
"updateEntity" : "", updateEntity: "",
"patchEntity" : "", patchEntity: "",
"deleteEntity" : "" deleteEntity: "",
} }
await runTests("crud", testQuery, assertions) await runTests("crud", testQuery, assertions)
}) })
@ -163,45 +166,45 @@ describe("OpenAPI2 Import", () => {
it("populates parameters", async () => { it("populates parameters", async () => {
const assertions = { const assertions = {
"createEntity" : [], createEntity: [],
"getEntities" : [ getEntities: [
{ {
"name" : "page", name: "page",
"default" : "", default: "",
}, },
{ {
"name" : "size", name: "size",
"default" : "", default: "",
} },
], ],
"getEntity" : [ getEntity: [
{ {
"name" : "entityId", name: "entityId",
"default" : "", default: "",
} },
], ],
"updateEntity" : [ updateEntity: [
{ {
"name" : "entityId", name: "entityId",
"default" : "", default: "",
} },
], ],
"patchEntity" : [ patchEntity: [
{ {
"name" : "entityId", name: "entityId",
"default" : "", default: "",
} },
], ],
"deleteEntity" : [ deleteEntity: [
{ {
"name" : "entityId", name: "entityId",
"default" : "", default: "",
}, },
{ {
"name" : "x-api-key", name: "x-api-key",
"default" : "", default: "",
} },
] ],
} }
await runTests("crud", testParameters, assertions) await runTests("crud", testParameters, assertions)
}) })
@ -209,28 +212,30 @@ describe("OpenAPI2 Import", () => {
const testBody = async (file, extension, assertions) => { const testBody = async (file, extension, assertions) => {
const queries = await getQueries(file, extension) const queries = await getQueries(file, extension)
for (let [operationId, body] of Object.entries(assertions)) { for (let [operationId, body] of Object.entries(assertions)) {
expect(queries[operationId].fields.requestBody).toStrictEqual(JSON.stringify(body, null, 2)) expect(queries[operationId].fields.requestBody).toStrictEqual(
JSON.stringify(body, null, 2)
)
} }
} }
it("populates body", async () => { it("populates body", async () => {
const assertions = { const assertions = {
"createEntity" : { createEntity: {
"name" : "name", name: "name",
"type" : "type", type: "type",
}, },
"getEntities" : undefined, getEntities: undefined,
"getEntity" : undefined, getEntity: undefined,
"updateEntity" : { updateEntity: {
"id": 1, id: 1,
"name" : "name", name: "name",
"type" : "type", type: "type",
}, },
"patchEntity" : { patchEntity: {
"id": 1, id: 1,
"name" : "name", name: "name",
"type" : "type", type: "type",
}, },
"deleteEntity" : undefined deleteEntity: undefined,
} }
await runTests("crud", testBody, assertions) await runTests("crud", testBody, assertions)
}) })

View File

@ -1,11 +1,14 @@
const TestConfig = require("../../../../../tests/utilities/TestConfiguration") const TestConfig = require("../../../../../tests/utilities/TestConfiguration")
const { RestImporter } = require("../index") const { RestImporter } = require("../index")
const fs = require("fs") const fs = require("fs")
const path = require('path') const path = require("path")
const { events } = require("@budibase/backend-core") const { events } = require("@budibase/backend-core")
const getData = (file) => { const getData = file => {
return fs.readFileSync(path.join(__dirname, `../sources/tests/${file}`), "utf8") return fs.readFileSync(
path.join(__dirname, `../sources/tests/${file}`),
"utf8"
)
} }
// openapi2 (swagger) // openapi2 (swagger)
@ -35,7 +38,7 @@ const datasets = {
oapi3PetstoreJson, oapi3PetstoreJson,
oapi3PetstoreYaml, oapi3PetstoreYaml,
// curl // curl
curl curl,
} }
describe("Rest Importer", () => { describe("Rest Importer", () => {
@ -47,7 +50,7 @@ describe("Rest Importer", () => {
let restImporter let restImporter
const init = async (data) => { const init = async data => {
restImporter = new RestImporter(data) restImporter = new RestImporter(data)
await restImporter.init() await restImporter.init()
} }
@ -69,35 +72,35 @@ describe("Rest Importer", () => {
it("gets info", async () => { it("gets info", async () => {
const assertions = { const assertions = {
// openapi2 (swagger) // openapi2 (swagger)
"oapi2CrudJson" : { oapi2CrudJson: {
name: "CRUD", name: "CRUD",
}, },
"oapi2CrudYaml" : { oapi2CrudYaml: {
name: "CRUD", name: "CRUD",
}, },
"oapi2PetstoreJson" : { oapi2PetstoreJson: {
name: "Swagger Petstore", name: "Swagger Petstore",
}, },
"oapi2PetstoreYaml" :{ oapi2PetstoreYaml: {
name: "Swagger Petstore", name: "Swagger Petstore",
}, },
// openapi3 // openapi3
"oapi3CrudJson" : { oapi3CrudJson: {
name: "CRUD", name: "CRUD",
}, },
"oapi3CrudYaml" : { oapi3CrudYaml: {
name: "CRUD", name: "CRUD",
}, },
"oapi3PetstoreJson" : { oapi3PetstoreJson: {
name: "Swagger Petstore - OpenAPI 3.0", name: "Swagger Petstore - OpenAPI 3.0",
}, },
"oapi3PetstoreYaml" :{ oapi3PetstoreYaml: {
name: "Swagger Petstore - OpenAPI 3.0", name: "Swagger Petstore - OpenAPI 3.0",
}, },
// curl // curl
"curl": { curl: {
name: "example.com", name: "example.com",
} },
} }
await runTest(testGetInfo, assertions) await runTest(testGetInfo, assertions)
}) })
@ -109,7 +112,11 @@ describe("Rest Importer", () => {
expect(importResult.errorQueries.length).toBe(0) expect(importResult.errorQueries.length).toBe(0)
expect(importResult.queries.length).toBe(assertions[key].count) expect(importResult.queries.length).toBe(assertions[key].count)
expect(events.query.imported).toBeCalledTimes(1) expect(events.query.imported).toBeCalledTimes(1)
expect(events.query.imported).toBeCalledWith(datasource, assertions[key].source, assertions[key].count) expect(events.query.imported).toBeCalledWith(
datasource,
assertions[key].source,
assertions[key].count
)
jest.clearAllMocks() jest.clearAllMocks()
} }
@ -118,44 +125,44 @@ describe("Rest Importer", () => {
// makes it through the importer // makes it through the importer
const assertions = { const assertions = {
// openapi2 (swagger) // openapi2 (swagger)
"oapi2CrudJson" : { oapi2CrudJson: {
count: 6, count: 6,
source: "openapi2.0", source: "openapi2.0",
}, },
"oapi2CrudYaml" :{ oapi2CrudYaml: {
count: 6, count: 6,
source: "openapi2.0" source: "openapi2.0",
}, },
"oapi2PetstoreJson" : { oapi2PetstoreJson: {
count: 20, count: 20,
source: "openapi2.0" source: "openapi2.0",
}, },
"oapi2PetstoreYaml" :{ oapi2PetstoreYaml: {
count: 20, count: 20,
source: "openapi2.0" source: "openapi2.0",
}, },
// openapi3 // openapi3
"oapi3CrudJson" : { oapi3CrudJson: {
count: 6, count: 6,
source: "openapi3.0" source: "openapi3.0",
}, },
"oapi3CrudYaml" :{ oapi3CrudYaml: {
count: 6, count: 6,
source: "openapi3.0" source: "openapi3.0",
}, },
"oapi3PetstoreJson" : { oapi3PetstoreJson: {
count: 19, count: 19,
source: "openapi3.0" source: "openapi3.0",
}, },
"oapi3PetstoreYaml" :{ oapi3PetstoreYaml: {
count: 19, count: 19,
source: "openapi3.0" source: "openapi3.0",
}, },
// curl // curl
"curl": { curl: {
count: 1, count: 1,
source: "curl" source: "curl",
} },
} }
await runTest(testImportQueries, assertions) await runTest(testImportQueries, assertions)
}) })

View File

@ -1,4 +1,4 @@
import { ValidFileExtensions } from "@budibase/shared-core" import { InvalidFileExtensions } from "@budibase/shared-core"
require("svelte/register") require("svelte/register")
@ -91,7 +91,10 @@ export const uploadFile = async function (
) )
} }
if (!env.SELF_HOSTED && !ValidFileExtensions.includes(extension)) { if (
!env.SELF_HOSTED &&
InvalidFileExtensions.includes(extension.toLowerCase())
) {
throw new BadRequestError( throw new BadRequestError(
`File "${file.name}" has an invalid extension: "${extension}"` `File "${file.name}" has an invalid extension: "${extension}"`
) )

View File

@ -1,65 +1,75 @@
const viewTemplate = require("../viewBuilder").default; const viewTemplate = require("../viewBuilder").default
describe("viewBuilder", () => { describe("viewBuilder", () => {
describe("Filter", () => { describe("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => { it("creates a view with multiple filters and conjunctions", () => {
expect(viewTemplate({ expect(
"name": "Test View", viewTemplate({
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0", name: "Test View",
"filters": [{ tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
"value": "Test", filters: [
"condition": "EQUALS", {
"key": "Name" value: "Test",
}, { condition: "EQUALS",
"value": "Value", key: "Name",
"condition": "MT", },
"key": "Yes", {
"conjunction": "OR" value: "Value",
}] condition: "MT",
})).toMatchSnapshot() key: "Yes",
conjunction: "OR",
},
],
})
).toMatchSnapshot()
}) })
}) })
describe("Calculate", () => { describe("Calculate", () => {
it("creates a view with the calculation statistics schema", () => { it("creates a view with the calculation statistics schema", () => {
expect(viewTemplate({ expect(
"name": "Calculate View", viewTemplate({
"field": "myField", name: "Calculate View",
"calculation": "stats", field: "myField",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0", calculation: "stats",
"filters": [] tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
})).toMatchSnapshot() filters: [],
})
).toMatchSnapshot()
}) })
}) })
describe("Group By", () => { describe("Group By", () => {
it("creates a view emitting the group by field", () => { it("creates a view emitting the group by field", () => {
expect(viewTemplate({ expect(
"name": "Test Scores Grouped By Age", viewTemplate({
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0", name: "Test Scores Grouped By Age",
"groupBy": "age", tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
"field": "score", groupBy: "age",
"filters": [], field: "score",
})).toMatchSnapshot() filters: [],
})
).toMatchSnapshot()
}) })
}) })
describe("Calculate and filter", () => { describe("Calculate and filter", () => {
it("creates a view with the calculation statistics and filter schema", () => { it("creates a view with the calculation statistics and filter schema", () => {
expect(viewTemplate({ expect(
"name": "Calculate View", viewTemplate({
"field": "myField", name: "Calculate View",
"calculation": "stats", field: "myField",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0", calculation: "stats",
"filters": [ tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [
{ {
"value": 17, value: 17,
"condition": "MT", condition: "MT",
"key": "age", key: "age",
} },
] ],
})).toMatchSnapshot() })
).toMatchSnapshot()
})
}) })
}) })
});

View File

@ -117,7 +117,7 @@ write.push(
* /applications/{appId}/publish: * /applications/{appId}/publish:
* post: * post:
* operationId: appPublish * operationId: appPublish
* summary: Unpublish an application * summary: Publish an application
* tags: * tags:
* - applications * - applications
* parameters: * parameters:

View File

@ -30,5 +30,4 @@ describe("/metrics", () => {
.expect(403) .expect(403)
}) })
}) })
}) })

View File

@ -40,7 +40,10 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(events.serve.servedAppPreview).toBeCalledTimes(1) expect(events.serve.servedAppPreview).toBeCalledTimes(1)
expect(events.serve.servedAppPreview).toBeCalledWith(config.getApp(), timezone) expect(events.serve.servedAppPreview).toBeCalledWith(
config.getApp(),
timezone
)
expect(events.serve.servedApp).not.toBeCalled() expect(events.serve.servedApp).not.toBeCalled()
}) })
@ -55,7 +58,11 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(events.serve.servedApp).toBeCalledTimes(1) expect(events.serve.servedApp).toBeCalledTimes(1)
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone, undefined) expect(events.serve.servedApp).toBeCalledWith(
config.getProdApp(),
timezone,
undefined
)
expect(events.serve.servedAppPreview).not.toBeCalled() expect(events.serve.servedAppPreview).not.toBeCalled()
}) })
@ -70,7 +77,11 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(events.serve.servedApp).toBeCalledTimes(1) expect(events.serve.servedApp).toBeCalledTimes(1)
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone, true) expect(events.serve.servedApp).toBeCalledWith(
config.getProdApp(),
timezone,
true
)
expect(events.serve.servedAppPreview).not.toBeCalled() expect(events.serve.servedAppPreview).not.toBeCalled()
}) })
}) })

View File

@ -38,7 +38,7 @@ describe("/api/keys", () => {
const res = await request const res = await request
.put(`/api/keys/TEST`) .put(`/api/keys/TEST`)
.send({ .send({
value: "test" value: "test",
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)

View File

@ -35,6 +35,17 @@ describe("/api/applications/:appId/sync", () => {
}) })
}) })
it("should reject an upload with a malicious uppercase file extension", async () => {
await config.withEnv({ SELF_HOSTED: undefined }, async () => {
let resp = (await config.api.attachment.process(
"OHNO.EXE",
Buffer.from([0]),
{ expectStatus: 400 }
)) as unknown as APIError
expect(resp.message).toContain("invalid extension")
})
})
it("should reject an upload with no file", async () => { it("should reject an upload with no file", async () => {
let resp = (await config.api.attachment.process( let resp = (await config.api.attachment.process(
undefined as any, undefined as any,

View File

@ -1,7 +1,6 @@
const setup = require("./utilities") const setup = require("./utilities")
const { events } = require("@budibase/backend-core") const { events } = require("@budibase/backend-core")
describe("/dev", () => { describe("/dev", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
@ -32,9 +31,9 @@ describe("/dev", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.version).toBe('0.0.0+jest') expect(res.body.version).toBe("0.0.0+jest")
expect(events.installation.versionChecked).toBeCalledTimes(1) expect(events.installation.versionChecked).toBeCalledTimes(1)
expect(events.installation.versionChecked).toBeCalledWith('0.0.0+jest') expect(events.installation.versionChecked).toBeCalledWith("0.0.0+jest")
}) })
}) })
}) })

View File

@ -14,7 +14,10 @@ describe("/metadata", () => {
automation = await config.createAutomation() automation = await config.createAutomation()
}) })
async function createMetadata(data, type = MetadataTypes.AUTOMATION_TEST_INPUT) { async function createMetadata(
data,
type = MetadataTypes.AUTOMATION_TEST_INPUT
) {
const res = await request const res = await request
.post(`/api/metadata/${type}/${automation._id}`) .post(`/api/metadata/${type}/${automation._id}`)
.send(data) .send(data)
@ -53,7 +56,9 @@ describe("/metadata", () => {
describe("destroy", () => { describe("destroy", () => {
it("should be able to delete some test inputs", async () => { it("should be able to delete some test inputs", async () => {
const res = await request const res = await request
.delete(`/api/metadata/${MetadataTypes.AUTOMATION_TEST_INPUT}/${automation._id}`) .delete(
`/api/metadata/${MetadataTypes.AUTOMATION_TEST_INPUT}/${automation._id}`
)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)

View File

@ -14,7 +14,7 @@ jest.mock("@budibase/backend-core", () => {
db: { db: {
...core.db, ...core.db,
isProdAppID: jest.fn(), isProdAppID: jest.fn(),
} },
} }
}) })
const setup = require("./utilities") const setup = require("./utilities")
@ -31,7 +31,6 @@ describe("/queries", () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
const setupTest = async () => { const setupTest = async () => {
await config.init() await config.init()
datasource = await config.createDatasource() datasource = await config.createDatasource()
query = await config.createQuery() query = await config.createQuery()
@ -52,7 +51,7 @@ describe("/queries", () => {
return { datasource, query } return { datasource, query }
} }
const createQuery = async (query) => { const createQuery = async query => {
return request return request
.post(`/api/queries`) .post(`/api/queries`)
.send(query) .send(query)
@ -76,7 +75,7 @@ describe("/queries", () => {
_id: res.body._id, _id: res.body._id,
...query, ...query,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString(),
}) })
expect(events.query.created).toBeCalledTimes(1) expect(events.query.created).toBeCalledTimes(1)
expect(events.query.updated).not.toBeCalled() expect(events.query.updated).not.toBeCalled()
@ -101,7 +100,7 @@ describe("/queries", () => {
_id: res.body._id, _id: res.body._id,
...query, ...query,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString(),
}) })
expect(events.query.created).not.toBeCalled() expect(events.query.created).not.toBeCalled()
expect(events.query.updated).toBeCalledTimes(1) expect(events.query.updated).toBeCalledTimes(1)
@ -237,8 +236,8 @@ describe("/queries", () => {
.expect(200) .expect(200)
// these responses come from the mock // these responses come from the mock
expect(res.body.schemaFields).toEqual({ expect(res.body.schemaFields).toEqual({
"a": "string", a: "string",
"b": "number", b: "number",
}) })
expect(res.body.rows.length).toEqual(1) expect(res.body.rows.length).toEqual(1)
expect(events.query.previewed).toBeCalledTimes(1) expect(events.query.previewed).toBeCalledTimes(1)
@ -302,9 +301,9 @@ describe("/queries", () => {
}) })
// these responses come from the mock // these responses come from the mock
expect(res.body.schemaFields).toEqual({ expect(res.body.schemaFields).toEqual({
"opts": "json", opts: "json",
"url": "string", url: "string",
"value": "string", value: "string",
}) })
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1") expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
}) })
@ -316,9 +315,9 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
expect(res.body.schemaFields).toEqual({ expect(res.body.schemaFields).toEqual({
"opts": "json", opts: "json",
"url": "string", url: "string",
"value": "string" value: "string",
}) })
expect(res.body.rows[0].url).toContain("doctype%20html") expect(res.body.rows[0].url).toContain("doctype%20html")
}) })
@ -339,9 +338,9 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
expect(res.body.schemaFields).toEqual({ expect(res.body.schemaFields).toEqual({
"fails": "number", fails: "number",
"opts": "json", opts: "json",
"url": "string" url: "string",
}) })
expect(res.body.rows[0].fails).toEqual(1) expect(res.body.rows[0].fails).toEqual(1)
}) })
@ -371,13 +370,19 @@ describe("/queries", () => {
}) })
describe("Current User Request Mapping", () => { describe("Current User Request Mapping", () => {
async function previewGet(datasource, fields, params) { async function previewGet(datasource, fields, params) {
return config.previewQuery(request, config, datasource, fields, params) return config.previewQuery(request, config, datasource, fields, params)
} }
async function previewPost(datasource, fields, params) { async function previewPost(datasource, fields, params) {
return config.previewQuery(request, config, datasource, fields, params, "create") return config.previewQuery(
request,
config,
datasource,
fields,
params,
"create"
)
} }
it("should parse global and query level header mappings", async () => { it("should parse global and query level header mappings", async () => {
@ -385,27 +390,29 @@ describe("/queries", () => {
const datasource = await config.restDatasource({ const datasource = await config.restDatasource({
defaultHeaders: { defaultHeaders: {
"test": "headerVal", test: "headerVal",
"emailHdr": "{{[user].[email]}}" emailHdr: "{{[user].[email]}}",
} },
}) })
const res = await previewGet(datasource, { const res = await previewGet(datasource, {
path: "www.google.com", path: "www.google.com",
queryString: "email={{[user].[email]}}", queryString: "email={{[user].[email]}}",
headers: { headers: {
queryHdr: "{{[user].[firstName]}}", queryHdr: "{{[user].[firstName]}}",
secondHdr : "1234" secondHdr: "1234",
} },
}) })
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.headers).toEqual({ expect(parsedRequest.opts.headers).toEqual({
"test": "headerVal", test: "headerVal",
"emailHdr": userDetails.email, emailHdr: userDetails.email,
"queryHdr": userDetails.firstName, queryHdr: userDetails.firstName,
"secondHdr" : "1234" secondHdr: "1234",
}) })
expect(res.body.rows[0].url).toEqual("http://www.google.com?email=" + userDetails.email.replace("@", "%40")) expect(res.body.rows[0].url).toEqual(
"http://www.google.com?email=" + userDetails.email.replace("@", "%40")
)
}) })
it("should bind the current user to query parameters", async () => { it("should bind the current user to query parameters", async () => {
@ -413,92 +420,130 @@ describe("/queries", () => {
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewGet(datasource, { const res = await previewGet(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "test={{myEmail}}&testName={{myName}}&testParam={{testParam}}", queryString:
}, { "test={{myEmail}}&testName={{myName}}&testParam={{testParam}}",
"myEmail" : "{{[user].[email]}}", },
"myName" : "{{[user].[firstName]}}", {
"testParam" : "1234" myEmail: "{{[user].[email]}}",
}) myName: "{{[user].[firstName]}}",
testParam: "1234",
}
)
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=" + userDetails.email.replace("@", "%40") + expect(res.body.rows[0].url).toEqual(
"&testName=" + userDetails.firstName + "&testParam=1234") "http://www.google.com?test=" +
userDetails.email.replace("@", "%40") +
"&testName=" +
userDetails.firstName +
"&testParam=1234"
)
}) })
it("should bind the current user the request body - plain text", async () => { it("should bind the current user the request body - plain text", async () => {
const userDetails = config.getUserDetails() const userDetails = config.getUserDetails()
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewPost(datasource, { const res = await previewPost(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "testParam={{testParam}}", queryString: "testParam={{testParam}}",
requestBody: "This is plain text and this is my email: {{[user].[email]}}. This is a test param: {{testParam}}", requestBody:
bodyType: "text" "This is plain text and this is my email: {{[user].[email]}}. This is a test param: {{testParam}}",
}, { bodyType: "text",
"testParam" : "1234" },
}) {
testParam: "1234",
}
)
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.body).toEqual(`This is plain text and this is my email: ${userDetails.email}. This is a test param: 1234`) expect(parsedRequest.opts.body).toEqual(
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234") `This is plain text and this is my email: ${userDetails.email}. This is a test param: 1234`
)
expect(res.body.rows[0].url).toEqual(
"http://www.google.com?testParam=1234"
)
}) })
it("should bind the current user the request body - json", async () => { it("should bind the current user the request body - json", async () => {
const userDetails = config.getUserDetails() const userDetails = config.getUserDetails()
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewPost(datasource, { const res = await previewPost(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "testParam={{testParam}}", queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}", requestBody:
bodyType: "json" '{"email":"{{[user].[email]}}","queryCode":{{testParam}},"userRef":"{{userRef}}"}',
}, { bodyType: "json",
"testParam" : "1234", },
"userRef" : "{{[user].[firstName]}}" {
}) testParam: "1234",
userRef: "{{[user].[firstName]}}",
}
)
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
const test = `{"email":"${userDetails.email}","queryCode":1234,"userRef":"${userDetails.firstName}"}` const test = `{"email":"${userDetails.email}","queryCode":1234,"userRef":"${userDetails.firstName}"}`
expect(parsedRequest.opts.body).toEqual(test) expect(parsedRequest.opts.body).toEqual(test)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234") expect(res.body.rows[0].url).toEqual(
"http://www.google.com?testParam=1234"
)
}) })
it("should bind the current user the request body - xml", async () => { it("should bind the current user the request body - xml", async () => {
const userDetails = config.getUserDetails() const userDetails = config.getUserDetails()
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewPost(datasource, { const res = await previewPost(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "testParam={{testParam}}", queryString: "testParam={{testParam}}",
requestBody: "<note> <email>{{[user].[email]}}</email> <code>{{testParam}}</code> " + requestBody:
"<note> <email>{{[user].[email]}}</email> <code>{{testParam}}</code> " +
"<ref>{{userId}}</ref> <somestring>testing</somestring> </note>", "<ref>{{userId}}</ref> <somestring>testing</somestring> </note>",
bodyType: "xml" bodyType: "xml",
}, { },
"testParam" : "1234", {
"userId" : "{{[user].[firstName]}}" testParam: "1234",
}) userId: "{{[user].[firstName]}}",
}
)
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
const test = `<note> <email>${userDetails.email}</email> <code>1234</code> <ref>${userDetails.firstName}</ref> <somestring>testing</somestring> </note>` const test = `<note> <email>${userDetails.email}</email> <code>1234</code> <ref>${userDetails.firstName}</ref> <somestring>testing</somestring> </note>`
expect(parsedRequest.opts.body).toEqual(test) expect(parsedRequest.opts.body).toEqual(test)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234") expect(res.body.rows[0].url).toEqual(
"http://www.google.com?testParam=1234"
)
}) })
it("should bind the current user the request body - form-data", async () => { it("should bind the current user the request body - form-data", async () => {
const userDetails = config.getUserDetails() const userDetails = config.getUserDetails()
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewPost(datasource, { const res = await previewPost(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "testParam={{testParam}}", queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}", requestBody:
bodyType: "form" '{"email":"{{[user].[email]}}","queryCode":{{testParam}},"userRef":"{{userRef}}"}',
}, { bodyType: "form",
"testParam" : "1234", },
"userRef" : "{{[user].[firstName]}}" {
}) testParam: "1234",
userRef: "{{[user].[firstName]}}",
}
)
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
@ -511,28 +556,34 @@ describe("/queries", () => {
const userRef = parsedRequest.opts.body._streams[7] const userRef = parsedRequest.opts.body._streams[7]
expect(userRef).toEqual(userDetails.firstName) expect(userRef).toEqual(userDetails.firstName)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234") expect(res.body.rows[0].url).toEqual(
"http://www.google.com?testParam=1234"
)
}) })
it("should bind the current user the request body - encoded", async () => { it("should bind the current user the request body - encoded", async () => {
const userDetails = config.getUserDetails() const userDetails = config.getUserDetails()
const datasource = await config.restDatasource() const datasource = await config.restDatasource()
const res = await previewPost(datasource, { const res = await previewPost(
datasource,
{
path: "www.google.com", path: "www.google.com",
queryString: "testParam={{testParam}}", queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}", requestBody:
bodyType: "encoded" '{"email":"{{[user].[email]}}","queryCode":{{testParam}},"userRef":"{{userRef}}"}',
}, { bodyType: "encoded",
"testParam" : "1234", },
"userRef" : "{{[user].[firstName]}}" {
}) testParam: "1234",
userRef: "{{[user].[firstName]}}",
}
)
const parsedRequest = JSON.parse(res.body.extra.raw) const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.body.email).toEqual(userDetails.email) expect(parsedRequest.opts.body.email).toEqual(userDetails.email)
expect(parsedRequest.opts.body.queryCode).toEqual("1234") expect(parsedRequest.opts.body.queryCode).toEqual("1234")
expect(parsedRequest.opts.body.userRef).toEqual(userDetails.firstName) expect(parsedRequest.opts.body.userRef).toEqual(userDetails.firstName)
}) })
})
});
}) })

View File

@ -170,7 +170,7 @@ describe("/roles", () => {
.get("/api/roles/accessible") .get("/api/roles/accessible")
.set({ .set({
...config.defaultHeaders(), ...config.defaultHeaders(),
"x-budibase-role": "CUSTOM_ROLE" "x-budibase-role": "CUSTOM_ROLE",
}) })
.expect(200) .expect(200)
expect(res.body.length).toBe(3) expect(res.body.length).toBe(3)

View File

@ -37,19 +37,23 @@ describe("/routing", () => {
await runInProd(async () => { await runInProd(async () => {
return request return request
.get(`/api/routing/client`) .get(`/api/routing/client`)
.set(await config.roleHeaders({ .set(
await config.roleHeaders({
roleId: BUILTIN_ROLE_IDS.BASIC, roleId: BUILTIN_ROLE_IDS.BASIC,
prodApp: false prodApp: false,
})) })
)
.expect(302) .expect(302)
}) })
}) })
it("returns the correct routing for basic user", async () => { it("returns the correct routing for basic user", async () => {
const res = await request const res = await request
.get(`/api/routing/client`) .get(`/api/routing/client`)
.set(await config.roleHeaders({ .set(
roleId: BUILTIN_ROLE_IDS.BASIC await config.roleHeaders({
})) roleId: BUILTIN_ROLE_IDS.BASIC,
})
)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.routes).toBeDefined() expect(res.body.routes).toBeDefined()
@ -57,18 +61,20 @@ describe("/routing", () => {
subpaths: { subpaths: {
[route]: { [route]: {
screenId: basic._id, screenId: basic._id,
roleId: basic.routing.roleId roleId: basic.routing.roleId,
} },
} },
}) })
}) })
it("returns the correct routing for power user", async () => { it("returns the correct routing for power user", async () => {
const res = await request const res = await request
.get(`/api/routing/client`) .get(`/api/routing/client`)
.set(await config.roleHeaders({ .set(
roleId: BUILTIN_ROLE_IDS.POWER await config.roleHeaders({
})) roleId: BUILTIN_ROLE_IDS.POWER,
})
)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.routes).toBeDefined() expect(res.body.routes).toBeDefined()
@ -76,9 +82,9 @@ describe("/routing", () => {
subpaths: { subpaths: {
[route]: { [route]: {
screenId: power._id, screenId: power._id,
roleId: power.routing.roleId roleId: power.routing.roleId,
} },
} },
}) })
}) })
}) })
@ -87,10 +93,12 @@ describe("/routing", () => {
it("should fetch all routes for builder", async () => { it("should fetch all routes for builder", async () => {
const res = await request const res = await request
.get(`/api/routing`) .get(`/api/routing`)
.set(await config.roleHeaders({ .set(
await config.roleHeaders({
roleId: BUILTIN_ROLE_IDS.POWER, roleId: BUILTIN_ROLE_IDS.POWER,
builder: true, builder: true,
})) })
)
.expect(200) .expect(200)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
expect(res.body.routes).toBeDefined() expect(res.body.routes).toBeDefined()

View File

@ -36,7 +36,7 @@ describe("/screens", () => {
}) })
describe("save", () => { describe("save", () => {
const saveScreen = async (screen) => { const saveScreen = async screen => {
const res = await request const res = await request
.post(`/api/screens`) .post(`/api/screens`)
.send(screen) .send(screen)

View File

@ -36,13 +36,13 @@ describe("/views", () => {
table = await config.createTable(priceTable()) table = await config.createTable(priceTable())
}) })
const saveView = async (view) => { const saveView = async view => {
const viewToSave = { const viewToSave = {
name: "TestView", name: "TestView",
field: "Price", field: "Price",
calculation: "stats", calculation: "stats",
tableId: table._id, tableId: table._id,
...view ...view,
} }
return request return request
.post(`/api/views`) .post(`/api/views`)
@ -53,7 +53,6 @@ describe("/views", () => {
} }
describe("create", () => { describe("create", () => {
it("returns a success message when the view is successfully created", async () => { it("returns a success message when the view is successfully created", async () => {
const res = await saveView() const res = await saveView()
expect(res.body.tableId).toBe(table._id) expect(res.body.tableId).toBe(table._id)
@ -81,11 +80,13 @@ describe("/views", () => {
const res = await saveView({ const res = await saveView({
calculation: null, calculation: null,
filters: [{ filters: [
{
value: "1", value: "1",
condition: "EQUALS", condition: "EQUALS",
key: "price" key: "price",
}], },
],
}) })
expect(res.body.tableId).toBe(table._id) expect(res.body.tableId).toBe(table._id)
@ -199,18 +200,26 @@ describe("/views", () => {
}) })
it("updates a view filter", async () => { it("updates a view filter", async () => {
await saveView({ filters: [{ await saveView({
filters: [
{
value: "1", value: "1",
condition: "EQUALS", condition: "EQUALS",
key: "price" key: "price",
}] }) },
],
})
jest.clearAllMocks() jest.clearAllMocks()
await saveView({ filters: [{ await saveView({
filters: [
{
value: "2", value: "2",
condition: "EQUALS", condition: "EQUALS",
key: "price" key: "price",
}] }) },
],
})
expect(events.view.created).not.toBeCalled() expect(events.view.created).not.toBeCalled()
expect(events.view.updated).toBeCalledTimes(1) expect(events.view.updated).toBeCalledTimes(1)
@ -223,11 +232,15 @@ describe("/views", () => {
}) })
it("deletes a view filter", async () => { it("deletes a view filter", async () => {
await saveView({ filters: [{ await saveView({
filters: [
{
value: "1", value: "1",
condition: "EQUALS", condition: "EQUALS",
key: "price" key: "price",
}] }) },
],
})
jest.clearAllMocks() jest.clearAllMocks()
await saveView({ filters: [] }) await saveView({ filters: [] })
@ -344,7 +357,6 @@ describe("/views", () => {
}) })
describe("exportView", () => { describe("exportView", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
@ -362,14 +374,14 @@ describe("/views", () => {
.expect(200) .expect(200)
} }
const assertJsonExport = (res) => { const assertJsonExport = res => {
const rows = JSON.parse(res.text) const rows = JSON.parse(res.text)
expect(rows.length).toBe(1) expect(rows.length).toBe(1)
expect(rows[0].name).toBe("test-name") expect(rows[0].name).toBe("test-name")
expect(rows[0].description).toBe("ùúûü") expect(rows[0].description).toBe("ùúûü")
} }
const assertCSVExport = (res) => { const assertCSVExport = res => {
expect(res.text).toBe(`"name","description"\n"test-name","ùúûü"`) expect(res.text).toBe(`"name","description"\n"test-name","ùúûü"`)
} }

View File

@ -9,26 +9,25 @@ describe("test the bash action", () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
it("should be able to execute a script", async () => { it("should be able to execute a script", async () => {
let res = await setup.runStep(
let res = await setup.runStep("EXECUTE_BASH", "EXECUTE_BASH",
inputs = { (inputs = {
code: "echo 'test'" code: "echo 'test'",
} })
) )
expect(res.stdout).toEqual("test\n") expect(res.stdout).toEqual("test\n")
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should handle a null value", async () => { it("should handle a null value", async () => {
let res = await setup.runStep(
let res = await setup.runStep("EXECUTE_BASH", "EXECUTE_BASH",
inputs = { (inputs = {
code: null code: null,
} })
)
expect(res.stdout).toEqual(
"Budibase bash automation failed: Invalid inputs"
) )
expect(res.stdout).toEqual("Budibase bash automation failed: Invalid inputs")
}) })
}) })

View File

@ -1,7 +1,7 @@
const setup = require("./utilities") const setup = require("./utilities")
// need real Date for this test // need real Date for this test
const tk = require('timekeeper'); const tk = require("timekeeper")
tk.reset() tk.reset()
describe("test the delay logic", () => { describe("test the delay logic", () => {

View File

@ -23,5 +23,4 @@ describe("test the outgoing webhook action", () => {
expect(res.response.method).toEqual("post") expect(res.response.method).toEqual("post")
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
}) })

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