Merge master develop (#10897)

* Binding drawer fixes

* Added missing headless flag for the bindable combobox

* Fix for QueryEditor width

* Fix svelte transitions in grid new row component breaking routify

* Bump version to 2.7.7

* fix REST connector failure to save

* Bump version to 2.7.8

* Bump version to 2.7.9

* Unexpected token when export data (#10721)

* Tidy ID string for JSON parse

* Display error for composite keys

* Unit test WIP

* directly assign the mock function on the datasource

* Unit tests for exportRows ID handling

---------

Co-authored-by: Martin McKeaveney <martinmckeaveney@gmail.com>

* Return all rows if oneOf value is falsey (#10638)

* Bump version to 2.7.10

* Make sure divider fields are left-most (#10627)

* Make sure divider fields are left most

* Refactor

* Bump version to 2.7.11

* Temporarily remove the focus store update as it triggers a full redraw of the component settings

* Linting

* Removed commented out code

* Bump version to 2.7.12

* Bump version to 2.7.13

* Do not show Business tag for Email action (#10867)

* Bump version to 2.7.14

* new deploy trigger (#10892)

* point the deploys at the new env

* Bump version to 2.7.15

---------

Co-authored-by: Dean <deanhannigan@gmail.com>
Co-authored-by: Andrew Kingston <andrew@kingston.dev>
Co-authored-by: Budibase Staging Release Bot <>
Co-authored-by: Martin McKeaveney <martinmckeaveney@gmail.com>
Co-authored-by: Martin McKeaveney <martin@budibase.com>
Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com>
Co-authored-by: Adria Navarro <adria@budibase.com>
This commit is contained in:
Jonny McCullagh 2023-06-14 16:18:09 +01:00 committed by GitHub
parent 9c45b16ced
commit 757ca6a166
20 changed files with 234 additions and 136 deletions

View File

@ -12,31 +12,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# - name: Fail if not a tag - name: Fail if not a tag
# run: |
# if [[ $GITHUB_REF != refs/tags/* ]]; then
# echo "Workflow Dispatch can only be run on tags"
# exit 1
# fi
- uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: Fail if tag is not in master
# run: |
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
# exit 1
# fi
- name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \ if [[ $GITHUB_REF != refs/tags/* ]]; then
-H 'Accept: application/vnd.github.v3.raw' \ echo "Workflow Dispatch can only be run on tags"
-o values.production.yaml \ exit 1
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml fi
wc -l values.production.yaml - uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Get the latest budibase release version - name: Get the latest budibase release version
id: version id: version
@ -48,29 +39,10 @@ jobs:
fi fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials - uses: passeidireto/trigger-external-workflow-action@main
uses: aws-actions/configure-aws-credentials@v1 env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} repository: budibase/budibase-deploys
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} event: budicloud-prod-deploy
aws-region: eu-west-1 github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Deploy to EKS
uses: craftech-io/eks-helm-deploy-action@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
cluster-name: budibase-eks-production
config-files: values.production.yaml
chart-path: charts/budibase
namespace: budibase
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
name: budibase-prod
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -25,50 +25,17 @@ jobs:
exit 1 exit 1
fi fi
- name: Get the latest budibase release version - name: Get the latest budibase release version
id: version id: version
run: | run: |
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Pull values.yaml from budibase-infra - uses: passeidireto/trigger-external-workflow-action@main
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.preprod.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
wc -l values.preprod.yaml
- name: Deploy to Preprod Environment
uses: budibase/helm@v1.8.0
with:
release: budibase-preprod
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
helm: helm3
values: |
globals:
appVersion: v${{ env.RELEASE_VERSION }}
ingress:
enabled: true
nginx: true
value-files: >-
[
"values.preprod.yaml"
]
env: env:
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}' PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with: with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} repository: budibase/budibase-deploys
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod." event: budicloud-preprod-deploy
embed-title: ${{ env.RELEASE_VERSION }} github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -1,5 +1,5 @@
{ {
"version": "2.7.7-alpha.9", "version": "2.7.15",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "packages/backend-core",

View File

@ -343,6 +343,9 @@ export class QueryBuilder<T> {
} }
const oneOf = (key: string, value: any) => { const oneOf = (key: string, value: any) => {
if (!value) {
return `*:*`
}
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
if (typeof value === "string") { if (typeof value === "string") {
value = value.split(",") value = value.split(",")

View File

@ -114,6 +114,25 @@ describe("lucene", () => {
expect(resp.rows.length).toBe(2) expect(resp.rows.length).toBe(2)
}) })
it("should return all rows when doing a one of search against falsey value", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addOneOf("property", null)
let resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", undefined)
resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", "")
resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", [])
resp = await builder.run()
expect(resp.rows.length).toBe(0)
})
it("should be able to perform a contains search", async () => { it("should be able to perform a contains search", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME) const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addContains("property", ["word"]) builder.addContains("property", ["word"])

View File

@ -204,6 +204,12 @@
}) })
return columns return columns
.sort((a, b) => { .sort((a, b) => {
if (a.divider) {
return a
}
if (b.divider) {
return b
}
const orderA = a.order || Number.MAX_SAFE_INTEGER const orderA = a.order || Number.MAX_SAFE_INTEGER
const orderB = b.order || Number.MAX_SAFE_INTEGER const orderB = b.order || Number.MAX_SAFE_INTEGER
const nameA = getDisplayName(a) const nameA = getDisplayName(a)

View File

@ -12,7 +12,7 @@
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
import { admin, licensing } from "stores/portal" import { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
import { TriggerStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { checkForCollectStep } from "builderStore/utils" import { checkForCollectStep } from "builderStore/utils"
export let blockIdx export let blockIdx
@ -149,7 +149,7 @@
<div class="item-body"> <div class="item-body">
<Icon name={action.icon} /> <Icon name={action.icon} />
<Body size="XS">{action.name}</Body> <Body size="XS">{action.name}</Body>
{#if isDisabled && !syncAutomationsEnabled} {#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
<div class="tag-color"> <div class="tag-color">
<Tags> <Tags>
<Tag icon="LockClosed">Business</Tag> <Tag icon="LockClosed">Business</Tag>

View File

@ -76,6 +76,10 @@ export function getBindings({
// will be replaced by the main array binding // will be replaced by the main array binding
readableBinding: label, readableBinding: label,
runtimeBinding: binding, runtimeBinding: binding,
display: {
name: label,
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
},
}) })
} }
return bindings return bindings

View File

@ -57,6 +57,12 @@
} }
async function saveDatasource() { async function saveDatasource() {
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig()
if (!valid) {
return false
}
}
try { try {
if (!datasource.name) { if (!datasource.name) {
datasource.name = name datasource.name = name

View File

@ -19,7 +19,7 @@
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { store } from "builderStore"
import { convertToJS } from "@budibase/string-templates" import { convertToJS } from "@budibase/string-templates"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import CodeEditor from "../CodeEditor/CodeEditor.svelte" import CodeEditor from "../CodeEditor/CodeEditor.svelte"
@ -339,25 +339,28 @@
</Tab> </Tab>
{/if} {/if}
<div class="drawer-actions"> <div class="drawer-actions">
<Button {#if drawerActions?.hide}
secondary <Button
quiet secondary
on:click={() => { quiet
store.actions.settings.propertyFocus(null) on:click={() => {
drawerActions.hide() drawerActions.hide()
}} }}
> >
Cancel Cancel
</Button> </Button>
<Button {/if}
cta {#if bindingDrawerActions?.save}
disabled={!valid} <Button
on:click={() => { cta
bindingDrawerActions.save() disabled={!valid}
}} on:click={() => {
> bindingDrawerActions.save()
Save }}
</Button> >
Save
</Button>
{/if}
</div> </div>
</Tabs> </Tabs>
</div> </div>

View File

@ -36,7 +36,7 @@
.map(([name, categoryBindings]) => ({ .map(([name, categoryBindings]) => ({
name, name,
bindings: categoryBindings?.filter(binding => { bindings: categoryBindings?.filter(binding => {
return binding.readableBinding.match(searchRgx) return !search || binding.readableBinding.match(searchRgx)
}), }),
})) }))
.filter(category => { .filter(category => {
@ -46,7 +46,11 @@
) )
}) })
$: filteredHelpers = helpers?.filter(helper => { $: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx) return (
!search ||
helper.label.match(searchRgx) ||
helper.description.match(searchRgx)
)
}) })
const getHelperExample = (helper, js) => { const getHelperExample = (helper, js) => {
@ -124,9 +128,6 @@
<span <span
class="search-input-icon" class="search-input-icon"
on:click={() => { on:click={() => {
if (!search) {
return
}
search = null search = null
}} }}
class:searching={search} class:searching={search}

View File

@ -76,7 +76,7 @@
{/if} {/if}
</div> </div>
<Drawer bind:this={bindingDrawer} {title}> <Drawer bind:this={bindingDrawer} {title} headless>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Add the objects on the left to enrich your text. Add the objects on the left to enrich your text.
</svelte:fragment> </svelte:fragment>

View File

@ -5,8 +5,6 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { store } from "builderStore"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte" import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates" import { isJSBinding } from "@budibase/string-templates"
@ -36,7 +34,6 @@
const saveBinding = () => { const saveBinding = () => {
onChange(tempValue) onChange(tempValue)
store.actions.settings.propertyFocus(null)
onBlur() onBlur()
bindingDrawer.hide() bindingDrawer.hide()
} }
@ -70,7 +67,6 @@
<div <div
class="icon" class="icon"
on:click={() => { on:click={() => {
store.actions.settings.propertyFocus(key)
bindingDrawer.show() bindingDrawer.show()
}} }}
> >

View File

@ -73,10 +73,6 @@
if (highlighted) { if (highlighted) {
store.actions.settings.highlight(null) store.actions.settings.highlight(null)
} }
// To fix focus 'affect' when property is target of a drawer other actions in the builder.
if (propertyFocus) {
store.actions.settings.propertyFocus(null)
}
}) })
</script> </script>

View File

@ -186,7 +186,6 @@
} }
div :global(.CodeMirror) { div :global(.CodeMirror) {
width: var(--code-mirror-width) !important;
height: var(--code-mirror-height) !important; height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
font-family: var(--font-mono); font-family: var(--font-mono);

View File

@ -65,7 +65,7 @@
} }
const saveDatasource = async () => { const saveDatasource = async () => {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) { if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig() const valid = await validateConfig()
if (!valid) { if (!valid) {
return false return false

View File

@ -148,9 +148,9 @@
class:floating={offset > 0} class:floating={offset > 0}
style="--offset:{offset}px; --sticky-width:{width}px;" style="--offset:{offset}px; --sticky-width:{width}px;"
> >
<div class="underlay sticky" transition:fade={{ duration: 130 }} /> <div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
<div class="underlay" transition:fade={{ duration: 130 }} /> <div class="underlay" transition:fade|local={{ duration: 130 }} />
<div class="sticky-column" transition:fade={{ duration: 130 }}> <div class="sticky-column" transition:fade|local={{ duration: 130 }}>
<GutterCell on:expand={addViaModal} rowHovered> <GutterCell on:expand={addViaModal} rowHovered>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" /> <Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
{#if isAdding} {#if isAdding}
@ -179,7 +179,7 @@
</DataCell> </DataCell>
{/if} {/if}
</div> </div>
<div class="normal-columns" transition:fade={{ duration: 130 }}> <div class="normal-columns" transition:fade|local={{ duration: 130 }}>
<GridScrollWrapper scrollHorizontally wheelInteractive> <GridScrollWrapper scrollHorizontally wheelInteractive>
<div class="row"> <div class="row">
{#each $renderedColumns as column, columnIdx} {#each $renderedColumns as column, columnIdx}
@ -209,7 +209,7 @@
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
</div> </div>
<div class="buttons" transition:fade={{ duration: 130 }}> <div class="buttons" transition:fade|local={{ duration: 130 }}>
<Button size="M" cta on:click={addRow} disabled={isAdding}> <Button size="M" cta on:click={addRow} disabled={isAdding}>
<div class="button-with-keys"> <div class="button-with-keys">
Save Save

View File

@ -237,9 +237,15 @@ export async function exportRows(ctx: UserCtx) {
ctx.request.body = { ctx.request.body = {
query: { query: {
oneOf: { oneOf: {
_id: ctx.request.body.rows.map( _id: ctx.request.body.rows.map((row: string) => {
(row: string) => JSON.parse(decodeURI(row))[0] const ids = JSON.parse(
), decodeURI(row).replace(/'/g, `"`).replace(/%2C/g, ",")
)
if (ids.length > 1) {
ctx.throw(400, "Export data does not support composite keys.")
}
return ids[0]
}),
}, },
}, },
} }

View File

@ -0,0 +1,120 @@
import { exportRows } from "../row/external"
import sdk from "../../../sdk"
import { ExternalRequest } from "../row/ExternalRequest"
// @ts-ignore
sdk.datasources = {
get: jest.fn(),
}
jest.mock("../row/ExternalRequest")
jest.mock("../view/exporters", () => ({
csv: jest.fn(),
Format: {
CSV: "csv",
},
}))
jest.mock("../../../utilities/fileSystem")
function getUserCtx() {
return {
params: {
tableId: "datasource__tablename",
},
query: {
format: "csv",
},
request: {
body: {},
},
throw: jest.fn(() => {
throw "Err"
}),
attachment: jest.fn(),
}
}
describe("external row controller", () => {
describe("exportRows", () => {
beforeAll(() => {
//@ts-ignore
jest.spyOn(ExternalRequest.prototype, "run").mockImplementation(() => [])
})
afterEach(() => {
jest.clearAllMocks()
})
it("should throw a 400 if no datasource entities are present", async () => {
let userCtx = getUserCtx()
try {
//@ts-ignore
await exportRows(userCtx)
} catch (e) {
expect(userCtx.throw).toHaveBeenCalledWith(
400,
"Datasource has not been configured for plus API."
)
}
})
it("should handle single quotes from a row ID", async () => {
//@ts-ignore
sdk.datasources.get.mockImplementation(() => ({
entities: {
tablename: {
schema: {},
},
},
}))
let userCtx = getUserCtx()
userCtx.request.body = {
rows: ["['d001']"],
}
//@ts-ignore
await exportRows(userCtx)
expect(userCtx.request.body).toEqual({
query: {
oneOf: {
_id: ["d001"],
},
},
})
})
it("should throw a 400 if any composite keys are present", async () => {
let userCtx = getUserCtx()
userCtx.request.body = {
rows: ["[123]", "['d001'%2C'10111']"],
}
try {
//@ts-ignore
await exportRows(userCtx)
} catch (e) {
expect(userCtx.throw).toHaveBeenCalledWith(
400,
"Export data does not support composite keys."
)
}
})
it("should throw a 400 if no table name was found", async () => {
let userCtx = getUserCtx()
userCtx.params.tableId = "datasource__"
userCtx.request.body = {
rows: ["[123]"],
}
try {
//@ts-ignore
await exportRows(userCtx)
} catch (e) {
expect(userCtx.throw).toHaveBeenCalledWith(
400,
"Could not find table name."
)
}
})
})
})

View File

@ -135,7 +135,7 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
// specific to REST datasources, fix the auth configs again if required // specific to REST datasources, fix the auth configs again if required
if (hasAuthConfigs(update)) { if (hasAuthConfigs(update)) {
const configs = update.config.authConfigs as RestAuthConfig[] const configs = update.config.authConfigs as RestAuthConfig[]
const oldConfigs = old.config?.authConfigs as RestAuthConfig[] const oldConfigs = (old.config?.authConfigs as RestAuthConfig[]) || []
for (let config of configs) { for (let config of configs) {
if (config.type !== RestAuthType.BASIC) { if (config.type !== RestAuthType.BASIC) {
continue continue