Merge branch 'master' of github.com:Budibase/budibase into labday/sqs
This commit is contained in:
commit
0144a5b844
|
@ -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
|
||||
|
|
|
@ -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 }}`
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.13.10",
|
||||
"version": "2.13.14",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,10 +16,9 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onClick = e => {
|
||||
const onClick = () => {
|
||||
if (!disabled) {
|
||||
dispatch("click")
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
|
@ -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={() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
$: schemaComponents = getContextProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"schema"
|
||||
"schema",
|
||||
{ includeSelf: nested }
|
||||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let sanitisedFields
|
||||
let fieldList
|
||||
let schema
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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};`
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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", {})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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}"`
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
|
|
|
@ -117,7 +117,7 @@ write.push(
|
|||
* /applications/{appId}/publish:
|
||||
* post:
|
||||
* operationId: appPublish
|
||||
* summary: Unpublish an application
|
||||
* summary: Publish an application
|
||||
* tags:
|
||||
* - applications
|
||||
* parameters:
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -21,4 +21,4 @@ describe("/authenticate", () => {
|
|||
expect(res.body._id).toEqual(generateUserMetadataID(config.user._id))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -29,4 +29,4 @@ describe("/component", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -49,4 +49,4 @@ describe("/integrations", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -56,4 +56,4 @@ describe("/layouts", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue