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:
parent
9c45b16ced
commit
757ca6a166
|
@ -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 }}
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(",")
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue