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
pull_request:
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:
BRANCH: ${{ github.event.pull_request.head.ref }}
@ -19,7 +26,7 @@ env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
NX_BASE_BRANCH: origin/${{ github.base_ref }}
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:
lint:
@ -200,6 +207,9 @@ jobs:
- run: yarn --frozen-lockfile
- name: Build packages
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
run: |
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:
release:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
const _ = require('lodash/fp')
const {structures} = require("../../../tests")
const _ = require("lodash/fp")
const { structures } = require("../../../tests")
jest.mock("../../../src/context")
jest.mock("../../../src/db")
@ -7,10 +7,9 @@ jest.mock("../../../src/db")
const context = require("../../../src/context")
const db = require("../../../src/db")
const {getCreatorCount} = require('../../../src/users/users')
const { getCreatorCount } = require("../../../src/users/users")
describe("Users", () => {
let getGlobalDBMock
let getGlobalUserParamsMock
let paginationMock
@ -26,26 +25,26 @@ describe("Users", () => {
it("Retrieves the number of creators", async () => {
const getUsers = (offset, limit, creators = false) => {
const range = _.range(offset, limit)
const opts = creators ? {builder: {global: true}} : undefined
const opts = creators ? { builder: { global: true } } : undefined
return range.map(() => structures.users.user(opts))
}
const page1Data = getUsers(0, 8)
const page2Data = getUsers(8, 12, true)
getGlobalDBMock.mockImplementation(() => ({
name : "fake-db",
name: "fake-db",
allDocs: () => ({
rows: [...page1Data, ...page2Data]
})
rows: [...page1Data, ...page2Data],
}),
}))
paginationMock.mockImplementationOnce(() => ({
data: page1Data,
hasNextPage: true,
nextPage: "1"
nextPage: "1",
}))
paginationMock.mockImplementation(() => ({
data: page2Data,
hasNextPage: false,
nextPage: undefined
nextPage: undefined,
}))
const creatorsCount = await getCreatorCount()
expect(creatorsCount).toBe(4)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@
export let disabled = false
export let readonly = false
export let updateOnChange = true
export let error = null
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
@ -111,27 +110,12 @@
}
</script>
<div
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div class="spectrum-InputGroup" class:is-disabled={disabled}>
<div
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
{id}
on:click

View File

@ -6,7 +6,6 @@
export let id = null
export let placeholder = null
export let disabled = false
export let error = null
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
@ -84,7 +83,6 @@
<Picker
on:loadMore
{id}
{error}
{disabled}
{readonly}
{fieldText}

View File

@ -14,7 +14,6 @@
export let id = null
export let disabled = false
export let error = null
export let fieldText = ""
export let fieldIcon = ""
export let fieldColour = ""
@ -113,7 +112,6 @@
class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled}
class:is-invalid={!!error}
class:is-open={open}
aria-haspopup="listbox"
on:click={onClick}
@ -142,16 +140,6 @@
>
{fieldText}
</span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"

View File

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

View File

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

View File

@ -5,14 +5,13 @@
export let placeholder = null
export let disabled = false
export let readonly = false
export let error = null
export let height = null
export let id = null
export let fullScreenOffset = null
export let easyMDEOptions = null
</script>
<div class:error>
<div>
<MarkdownEditor
{value}
{placeholder}
@ -27,18 +26,4 @@
</div>
<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>

View File

@ -6,7 +6,6 @@
export let id = null
export let placeholder = "Choose an option"
export let disabled = false
export let error = null
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
@ -71,7 +70,6 @@
on:loadMore
{quiet}
{id}
{error}
{disabled}
{readonly}
{fieldText}

View File

@ -7,7 +7,6 @@
export let value = null
export let placeholder = null
export let disabled = false
export let error = null
export let id = null
export let readonly = false
export let updateOnChange = true
@ -98,20 +97,9 @@
<div
class="spectrum-Stepper"
class:spectrum-Stepper--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<div class="spectrum-Textfield spectrum-Stepper-textfield">
<input
{disabled}

View File

@ -6,7 +6,6 @@
export let placeholder = null
export let disabled = false
export let readonly = false
export let error = null
export let id = null
export let height = null
export let minHeight = null
@ -41,20 +40,9 @@
<div
style={`${heightString}${minHeightString}`}
class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM
spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<!-- prettier-ignore -->
<textarea
bind:this={textarea}

View File

@ -6,7 +6,6 @@
export let placeholder = null
export let type = "text"
export let disabled = false
export let error = null
export let id = null
export let readonly = false
export let updateOnChange = true
@ -78,19 +77,9 @@
<div
class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled}
class:is-focused={focus}
>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
bind:this={field}
{disabled}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
<script>
import "@spectrum-css/fieldlabel/dist/index-vars.css"
import FieldLabel from "./FieldLabel.svelte"
import Icon from "../Icon/Icon.svelte"
export let id = null
export let label = null
export let labelPosition = "above"
export let error = null
export let helpText = null
export let tooltip = ""
</script>
@ -17,6 +19,10 @@
<slot />
{#if error}
<div class="error">{error}</div>
{:else if helpText}
<div class="helpText">
<Icon name="HelpOutline" /> <span>{helpText}</span>
</div>
{/if}
</div>
</div>
@ -39,4 +45,21 @@
font-size: var(--spectrum-global-dimension-font-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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,11 +9,33 @@
export let bindings
export let anchor
export let removeButton
export let canRemove
export let nested
$: readableText = isJSBinding(item.text)
? "(JavaScript function)"
: 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>
<div class="list-item-body">
@ -24,12 +46,12 @@
{componentBindings}
{bindings}
on:change
parseSettings={updatedNestedFlags}
/>
<div class="field-label">{readableText || "Button"}</div>
</div>
<div class="list-item-right">
<Icon
disabled={!canRemove}
size="S"
name="Close"
hoverable

View File

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

View File

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

View File

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

View File

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

View File

@ -179,6 +179,7 @@
highlighted={$store.highlightedSettingKey === setting.key}
propertyFocus={$store.propertyFocus === setting.key}
info={setting.info}
disableBindings={setting.disableBindings}
props={{
// Generic settings
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>
import { params, goto } from "@roxi/routify"
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 { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
$: app = $apps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
@ -14,6 +16,18 @@
}
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>
<div class="container">
@ -45,6 +59,7 @@
</ActionButton>
{/if}
<ActionButton
disabled={noScreens}
quiet
icon="LinkOut"
on:click={() => window.open(iframeUrl, "_blank")}
@ -52,7 +67,19 @@
Fullscreen
</ActionButton>
</div>
<iframe src={iframeUrl} title={app.name} />
{#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} />
{/if}
</div>
<style>
@ -64,6 +91,7 @@
align-items: stretch;
padding: 0 var(--spacing-l) var(--spacing-l) var(--spacing-l);
}
.header {
display: flex;
justify-content: flex-start;
@ -71,9 +99,27 @@
gap: var(--spacing-xs);
flex: 0 0 50px;
}
iframe {
flex: 1 1 auto;
border-radius: var(--spacing-s);
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>

View File

@ -270,7 +270,6 @@
{
"type": "buttonConfiguration",
"key": "buttons",
"nested": true,
"defaultValue": [
{
"type": "cta",
@ -2732,6 +2731,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -2863,6 +2867,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -2960,6 +2969,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -3176,6 +3190,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "boolean",
"label": "Autocomplete",
@ -3336,6 +3355,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -3560,6 +3584,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -3658,6 +3687,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -3800,6 +3834,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -3895,6 +3934,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "boolean",
"label": "Disabled",
@ -4145,6 +4189,11 @@
"label": "Label",
"key": "label"
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "text",
"label": "Extensions",
@ -4248,6 +4297,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -4356,6 +4410,11 @@
"key": "defaultValue",
"supportsConditions": false
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"label": "On change",
@ -6339,8 +6398,29 @@
"label": "High contrast",
"key": "stripeRows",
"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": {
"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",
"key": "defaultValue"
},
{
"type": "text",
"label": "Help text",
"key": "helpText"
},
{
"type": "event",
"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,
// because it functions similarly to one
import { getContext } from "svelte"
import { get } from "svelte/store"
import { Grid } from "@budibase/frontend-core"
// table is actually any datasource, but called table for legacy compatibility
@ -16,12 +17,21 @@
export let fixedRowHeight = null
export let columns = null
export let onRowClick = null
export let buttons = null
const context = getContext("context")
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)
$: schemaOverrides = getSchemaOverrides(columns)
$: enrichedButtons = enrichButtons(buttons)
const getSchemaOverrides = columns => {
let overrides = {}
@ -33,6 +43,25 @@
})
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>
<div
@ -58,6 +87,7 @@
showControls={false}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
buttons={enrichedButtons}
on:rowclick={e => onRowClick?.({ row: e.detail })}
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<script>
import Placeholder from "../Placeholder.svelte"
import { getContext, onDestroy } from "svelte"
import { Icon } from "@budibase/bbui"
export let label
export let field
@ -13,6 +14,7 @@
export let readonly = false
export let validation
export let span = 6
export let helpText = null
// Get contexts
const formContext = getContext("form")
@ -97,7 +99,14 @@
{:else}
<slot />
{#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}
</div>
@ -127,13 +136,45 @@
position: relative;
width: 100%;
}
.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(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
);
font-size: var(--spectrum-global-dimension-font-size-75);
}
.helpText {
display: flex;
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--left {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
$: style = 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) {
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 notifySuccess = null
export let notifyError = null
export let buttons = null
// Unique identifier for DOM nodes inside this instance
const rand = Math.random()
@ -99,6 +100,7 @@
fixedRowHeight,
notifySuccess,
notifyError,
buttons,
})
// Set context for children to consume

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
const { Curl } = require("../../curl")
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")
}
describe("Curl Import", () => {
let curl
let curl
beforeEach(() => {
curl = new Curl()
@ -28,7 +28,7 @@ describe("Curl Import", () => {
expect(supported).toBe(false)
})
const init = async (file) => {
const init = async file => {
await curl.isSupported(getData(file))
}
@ -37,10 +37,9 @@ describe("Curl Import", () => {
const info = await curl.getInfo()
expect(info.name).toBe("example.com")
})
describe("Returns queries", () => {
const getQueries = async (file) => {
const getQueries = async file => {
await init(file)
const queries = await curl.getQueries()
expect(queries.length).toBe(1)
@ -58,7 +57,7 @@ describe("Curl Import", () => {
await testVerb("put", "update")
await testVerb("delete", "delete")
await testVerb("patch", "patch")
})
})
const testPath = async (file, urlPath) => {
const queries = await getQueries(file)
@ -66,8 +65,8 @@ describe("Curl Import", () => {
}
it("populates path", async () => {
await testPath("get", "http://example.com/")
await testPath("path", "http://example.com/paths/abc")
await testPath("get", "http://example.com/")
await testPath("path", "http://example.com/paths/abc")
})
const testHeaders = async (file, headers) => {
@ -77,7 +76,10 @@ describe("Curl Import", () => {
it("populates headers", async () => {
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) => {
@ -91,13 +93,15 @@ describe("Curl Import", () => {
const testBody = async (file, body) => {
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 () => {
await testBody("get", undefined)
await testBody("post", { "key" : "val" })
await testBody("post", { key: "val" })
await testBody("empty-body", {})
})
})
})
})

View File

@ -1,13 +1,16 @@
const { OpenAPI2 } = require("../../openapi2")
const fs = require("fs")
const path = require('path')
const path = require("path")
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", () => {
let openapi2
let openapi2
beforeEach(() => {
openapi2 = new OpenAPI2()
@ -47,9 +50,9 @@ describe("OpenAPI2 Import", () => {
it("returns import info", async () => {
await runTests("petstore", testImportInfo)
})
describe("Returns queries", () => {
const indexQueries = (queries) => {
const indexQueries = queries => {
return queries.reduce((acc, query) => {
acc[query.name] = query
return acc
@ -72,15 +75,15 @@ describe("OpenAPI2 Import", () => {
it("populates verb", async () => {
const assertions = {
"createEntity" : "create",
"getEntities" : "read",
"getEntity" : "read",
"updateEntity" : "update",
"patchEntity" : "patch",
"deleteEntity" : "delete"
createEntity: "create",
getEntities: "read",
getEntity: "read",
updateEntity: "update",
patchEntity: "patch",
deleteEntity: "delete",
}
await runTests("crud", testVerb, assertions)
})
})
const testPath = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
@ -91,12 +94,12 @@ describe("OpenAPI2 Import", () => {
it("populates path", async () => {
const assertions = {
"createEntity" : "http://example.com/entities",
"getEntities" : "http://example.com/entities",
"getEntity" : "http://example.com/entities/{{entityId}}",
"updateEntity" : "http://example.com/entities/{{entityId}}",
"patchEntity" : "http://example.com/entities/{{entityId}}",
"deleteEntity" : "http://example.com/entities/{{entityId}}"
createEntity: "http://example.com/entities",
getEntities: "http://example.com/entities",
getEntity: "http://example.com/entities/{{entityId}}",
updateEntity: "http://example.com/entities/{{entityId}}",
patchEntity: "http://example.com/entities/{{entityId}}",
deleteEntity: "http://example.com/entities/{{entityId}}",
}
await runTests("crud", testPath, assertions)
})
@ -109,27 +112,25 @@ describe("OpenAPI2 Import", () => {
}
const contentTypeHeader = {
"Content-Type" : "application/json",
"Content-Type": "application/json",
}
it("populates headers", async () => {
const assertions = {
"createEntity" : {
...contentTypeHeader
createEntity: {
...contentTypeHeader,
},
"getEntities" : {
getEntities: {},
getEntity: {},
updateEntity: {
...contentTypeHeader,
},
"getEntity" : {
patchEntity: {
...contentTypeHeader,
},
"updateEntity" : {
...contentTypeHeader
deleteEntity: {
"x-api-key": "{{x-api-key}}",
},
"patchEntity" : {
...contentTypeHeader
},
"deleteEntity" : {
"x-api-key" : "{{x-api-key}}",
}
}
await runTests("crud", testHeaders, assertions)
@ -138,18 +139,20 @@ describe("OpenAPI2 Import", () => {
const testQuery = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
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 () => {
const assertions = {
"createEntity" : "",
"getEntities" : "page={{page}}&size={{size}}",
"getEntity" : "",
"updateEntity" : "",
"patchEntity" : "",
"deleteEntity" : ""
createEntity: "",
getEntities: "page={{page}}&size={{size}}",
getEntity: "",
updateEntity: "",
patchEntity: "",
deleteEntity: "",
}
await runTests("crud", testQuery, assertions)
})
@ -163,45 +166,45 @@ describe("OpenAPI2 Import", () => {
it("populates parameters", async () => {
const assertions = {
"createEntity" : [],
"getEntities" : [
createEntity: [],
getEntities: [
{
"name" : "page",
"default" : "",
name: "page",
default: "",
},
{
"name" : "size",
"default" : "",
}
name: "size",
default: "",
},
],
"getEntity" : [
getEntity: [
{
"name" : "entityId",
"default" : "",
}
name: "entityId",
default: "",
},
],
"updateEntity" : [
updateEntity: [
{
"name" : "entityId",
"default" : "",
}
name: "entityId",
default: "",
},
],
"patchEntity" : [
patchEntity: [
{
"name" : "entityId",
"default" : "",
}
name: "entityId",
default: "",
},
],
"deleteEntity" : [
deleteEntity: [
{
"name" : "entityId",
"default" : "",
name: "entityId",
default: "",
},
{
"name" : "x-api-key",
"default" : "",
}
]
name: "x-api-key",
default: "",
},
],
}
await runTests("crud", testParameters, assertions)
})
@ -209,30 +212,32 @@ describe("OpenAPI2 Import", () => {
const testBody = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
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 () => {
const assertions = {
"createEntity" : {
"name" : "name",
"type" : "type",
createEntity: {
name: "name",
type: "type",
},
"getEntities" : undefined,
"getEntity" : undefined,
"updateEntity" : {
"id": 1,
"name" : "name",
"type" : "type",
getEntities: undefined,
getEntity: undefined,
updateEntity: {
id: 1,
name: "name",
type: "type",
},
"patchEntity" : {
"id": 1,
"name" : "name",
"type" : "type",
patchEntity: {
id: 1,
name: "name",
type: "type",
},
"deleteEntity" : undefined
deleteEntity: undefined,
}
await runTests("crud", testBody, assertions)
})
})
})
})

View File

@ -1,11 +1,14 @@
const TestConfig = require("../../../../../tests/utilities/TestConfiguration")
const { RestImporter } = require("../index")
const fs = require("fs")
const path = require('path')
const { events} = require("@budibase/backend-core")
const path = require("path")
const { events } = require("@budibase/backend-core")
const getData = (file) => {
return fs.readFileSync(path.join(__dirname, `../sources/tests/${file}`), "utf8")
const getData = file => {
return fs.readFileSync(
path.join(__dirname, `../sources/tests/${file}`),
"utf8"
)
}
// openapi2 (swagger)
@ -14,7 +17,7 @@ const oapi2CrudYaml = getData("openapi2/data/crud/crud.json")
const oapi2PetstoreJson = getData("openapi2/data/petstore/petstore.json")
const oapi2PetstoreYaml = getData("openapi2/data/petstore/petstore.json")
// openapi3
// openapi3
const oapi3CrudJson = getData("openapi3/data/crud/crud.json")
const oapi3CrudYaml = getData("openapi3/data/crud/crud.json")
const oapi3PetstoreJson = getData("openapi3/data/petstore/petstore.json")
@ -35,7 +38,7 @@ const datasets = {
oapi3PetstoreJson,
oapi3PetstoreYaml,
// curl
curl
curl,
}
describe("Rest Importer", () => {
@ -45,9 +48,9 @@ describe("Rest Importer", () => {
await config.init()
})
let restImporter
let restImporter
const init = async (data) => {
const init = async data => {
restImporter = new RestImporter(data)
await restImporter.init()
}
@ -69,35 +72,35 @@ describe("Rest Importer", () => {
it("gets info", async () => {
const assertions = {
// openapi2 (swagger)
"oapi2CrudJson" : {
oapi2CrudJson: {
name: "CRUD",
},
"oapi2CrudYaml" : {
oapi2CrudYaml: {
name: "CRUD",
},
"oapi2PetstoreJson" : {
oapi2PetstoreJson: {
name: "Swagger Petstore",
},
"oapi2PetstoreYaml" :{
oapi2PetstoreYaml: {
name: "Swagger Petstore",
},
// openapi3
"oapi3CrudJson" : {
oapi3CrudJson: {
name: "CRUD",
},
"oapi3CrudYaml" : {
oapi3CrudYaml: {
name: "CRUD",
},
"oapi3PetstoreJson" : {
oapi3PetstoreJson: {
name: "Swagger Petstore - OpenAPI 3.0",
},
"oapi3PetstoreYaml" :{
oapi3PetstoreYaml: {
name: "Swagger Petstore - OpenAPI 3.0",
},
// curl
"curl": {
curl: {
name: "example.com",
}
},
}
await runTest(testGetInfo, assertions)
})
@ -109,54 +112,58 @@ describe("Rest Importer", () => {
expect(importResult.errorQueries.length).toBe(0)
expect(importResult.queries.length).toBe(assertions[key].count)
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()
}
it("imports queries", async () => {
// simple sanity assertions that the whole dataset
// simple sanity assertions that the whole dataset
// makes it through the importer
const assertions = {
// openapi2 (swagger)
"oapi2CrudJson" : {
oapi2CrudJson: {
count: 6,
source: "openapi2.0",
},
"oapi2CrudYaml" :{
oapi2CrudYaml: {
count: 6,
source: "openapi2.0"
source: "openapi2.0",
},
"oapi2PetstoreJson" : {
oapi2PetstoreJson: {
count: 20,
source: "openapi2.0"
source: "openapi2.0",
},
"oapi2PetstoreYaml" :{
oapi2PetstoreYaml: {
count: 20,
source: "openapi2.0"
source: "openapi2.0",
},
// openapi3
"oapi3CrudJson" : {
oapi3CrudJson: {
count: 6,
source: "openapi3.0"
source: "openapi3.0",
},
"oapi3CrudYaml" :{
oapi3CrudYaml: {
count: 6,
source: "openapi3.0"
source: "openapi3.0",
},
"oapi3PetstoreJson" : {
oapi3PetstoreJson: {
count: 19,
source: "openapi3.0"
source: "openapi3.0",
},
"oapi3PetstoreYaml" :{
oapi3PetstoreYaml: {
count: 19,
source: "openapi3.0"
source: "openapi3.0",
},
// curl
"curl": {
curl: {
count: 1,
source: "curl"
}
source: "curl",
},
}
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")
@ -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(
`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("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => {
expect(viewTemplate({
"name": "Test View",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [{
"value": "Test",
"condition": "EQUALS",
"key": "Name"
}, {
"value": "Value",
"condition": "MT",
"key": "Yes",
"conjunction": "OR"
}]
})).toMatchSnapshot()
expect(
viewTemplate({
name: "Test View",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [
{
value: "Test",
condition: "EQUALS",
key: "Name",
},
{
value: "Value",
condition: "MT",
key: "Yes",
conjunction: "OR",
},
],
})
).toMatchSnapshot()
})
})
describe("Calculate", () => {
it("creates a view with the calculation statistics schema", () => {
expect(viewTemplate({
"name": "Calculate View",
"field": "myField",
"calculation": "stats",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": []
})).toMatchSnapshot()
expect(
viewTemplate({
name: "Calculate View",
field: "myField",
calculation: "stats",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [],
})
).toMatchSnapshot()
})
})
describe("Group By", () => {
it("creates a view emitting the group by field", () => {
expect(viewTemplate({
"name": "Test Scores Grouped By Age",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"groupBy": "age",
"field": "score",
"filters": [],
})).toMatchSnapshot()
expect(
viewTemplate({
name: "Test Scores Grouped By Age",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
groupBy: "age",
field: "score",
filters: [],
})
).toMatchSnapshot()
})
})
describe("Calculate and filter", () => {
it("creates a view with the calculation statistics and filter schema", () => {
expect(viewTemplate({
"name": "Calculate View",
"field": "myField",
"calculation": "stats",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [
{
"value": 17,
"condition": "MT",
"key": "age",
}
]
})).toMatchSnapshot()
expect(
viewTemplate({
name: "Calculate View",
field: "myField",
calculation: "stats",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [
{
value: 17,
condition: "MT",
key: "age",
},
],
})
).toMatchSnapshot()
})
})
});
})

View File

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

View File

@ -25,10 +25,9 @@ describe("/metrics", () => {
it("endpoint should not be publicly exposed", async () => {
await request
.get(`/api/public/v1/metrics`)
.set(config.publicHeaders())
.expect(403)
.get(`/api/public/v1/metrics`)
.set(config.publicHeaders())
.expect(403)
})
})
})

View File

@ -13,8 +13,8 @@ describe("/static", () => {
beforeAll(async () => {
app = await config.init()
})
beforeEach(()=>{
beforeEach(() => {
jest.clearAllMocks()
})
@ -22,7 +22,7 @@ describe("/static", () => {
it("should ping from builder", async () => {
await request
.post("/api/bbtel/ping")
.send({source: "builder", timezone})
.send({ source: "builder", timezone })
.set(config.defaultHeaders())
.expect(200)
@ -35,12 +35,15 @@ describe("/static", () => {
it("should ping from app preview", async () => {
await request
.post("/api/bbtel/ping")
.send({source: "app", timezone})
.send({ source: "app", timezone })
.set(config.defaultHeaders())
.expect(200)
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()
})
@ -50,12 +53,16 @@ describe("/static", () => {
await request
.post("/api/bbtel/ping")
.send({source: "app", timezone})
.send({ source: "app", timezone })
.set(headers)
.expect(200)
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()
})
@ -65,12 +72,16 @@ describe("/static", () => {
await request
.post("/api/bbtel/ping")
.send({source: "app", timezone, embedded: true})
.send({ source: "app", timezone, embedded: true })
.set(headers)
.expect(200)
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()
})
})

View File

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

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 () => {
let resp = (await config.api.attachment.process(
undefined as any,

View File

@ -21,4 +21,4 @@ describe("/authenticate", () => {
expect(res.body._id).toEqual(generateUserMetadataID(config.user._id))
})
})
})
})

View File

@ -29,4 +29,4 @@ describe("/component", () => {
})
})
})
})
})

View File

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

View File

@ -49,4 +49,4 @@ describe("/integrations", () => {
})
})
})
})
})

View File

@ -56,4 +56,4 @@ describe("/layouts", () => {
})
})
})
})
})

View File

@ -14,7 +14,10 @@ describe("/metadata", () => {
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
.post(`/api/metadata/${type}/${automation._id}`)
.send(data)
@ -53,7 +56,9 @@ describe("/metadata", () => {
describe("destroy", () => {
it("should be able to delete some test inputs", async () => {
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())
.expect("Content-Type", /json/)
.expect(200)

View File

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

View File

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

View File

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

View File

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

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