Merge branch 'master' into feature/sql-attachments

This commit is contained in:
Michael Drury 2025-03-14 11:54:51 +00:00 committed by GitHub
commit 14506c3b75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 75 deletions

View File

@ -9,6 +9,8 @@
export let type = "label" export let type = "label"
export let size = "M" export let size = "M"
export const disableEditingState = () => setEditing(false)
let editing = false let editing = false
function setEditing(state) { function setEditing(state) {

View File

@ -13,7 +13,7 @@
import { lowercase } from "@/helpers" import { lowercase } from "@/helpers"
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
let dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let defaults export let defaults
export let object = defaults || {} export let object = defaults || {}
@ -47,10 +47,17 @@
})) }))
let fieldActivity = buildFieldActivity(activity) let fieldActivity = buildFieldActivity(activity)
$: object = fields.reduce( $: fullObject = fields.reduce((acc, next) => {
(acc, next) => ({ ...acc, [next.name]: next.value }), acc[next.name] = next.value
{} return acc
) }, {})
$: object = Object.entries(fullObject).reduce((acc, [key, next]) => {
if (key) {
acc[key] = next
}
return acc
}, {})
function buildFieldActivity(obj) { function buildFieldActivity(obj) {
if (!obj || typeof obj !== "object") { if (!obj || typeof obj !== "object") {
@ -78,16 +85,19 @@
} }
function changed() { function changed() {
// Required for reactivity
fields = fields fields = fields
const newActivity = {} const newActivity = {}
const trimmedFields = []
for (let idx = 0; idx < fields.length; idx++) { for (let idx = 0; idx < fields.length; idx++) {
const fieldName = fields[idx].name const fieldName = fields[idx].name
if (fieldName) { if (fieldName) {
newActivity[fieldName] = fieldActivity[idx] newActivity[fieldName] = fieldActivity[idx]
trimmedFields.push(fields[idx])
} }
} }
activity = newActivity activity = newActivity
dispatch("change", fields) dispatch("change", trimmedFields)
} }
function isJsonArray(value) { function isJsonArray(value) {
@ -102,7 +112,7 @@
</script> </script>
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. --> <!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
{#if Object.keys(object || {}).length > 0} {#if Object.keys(fullObject || {}).length > 0}
{#if headings} {#if headings}
<div class="container" class:container-active={toggle}> <div class="container" class:container-active={toggle}>
<Label {tooltip}>{keyHeading || keyPlaceholder}</Label> <Label {tooltip}>{keyHeading || keyPlaceholder}</Label>

View File

@ -56,12 +56,13 @@
let query, datasource let query, datasource
let breakQs = {}, let breakQs = {},
requestBindings = {} requestBindings = {}
let saveId, url let saveId
let response, schema, enabledHeaders let response, schema, enabledHeaders
let authConfigId
let dynamicVariables, addVariableModal, varBinding, globalDynamicBindings let dynamicVariables, addVariableModal, varBinding, globalDynamicBindings
let restBindings = getRestBindings() let restBindings = getRestBindings()
let nestedSchemaFields = {} let nestedSchemaFields = {}
let saving
let queryNameLabel
$: staticVariables = datasource?.config?.staticVariables || {} $: staticVariables = datasource?.config?.staticVariables || {}
@ -91,7 +92,7 @@
$: datasourceType = datasource?.source $: datasourceType = datasource?.source
$: integrationInfo = $integrations[datasourceType] $: integrationInfo = $integrations[datasourceType]
$: queryConfig = integrationInfo?.query $: queryConfig = integrationInfo?.query
$: url = buildUrl(url, breakQs) $: url = buildUrl(query?.fields?.path, breakQs)
$: checkQueryName(url) $: checkQueryName(url)
$: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400 $: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400
$: isGet = query?.queryVerb === "read" $: isGet = query?.queryVerb === "read"
@ -103,6 +104,10 @@
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs) $: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
$: originalQuery = originalQuery ?? cloneDeep(query)
$: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
$: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
function getSelectedQuery() { function getSelectedQuery() {
return cloneDeep( return cloneDeep(
$queries.list.find(q => q._id === queryId) || { $queries.list.find(q => q._id === queryId) || {
@ -126,7 +131,8 @@
?.trim() || inputUrl ?.trim() || inputUrl
function checkQueryName(inputUrl = null) { function checkQueryName(inputUrl = null) {
if (query && (!query.name || query.flags.urlName)) { if (query && (!query.name || query.flags?.urlName)) {
query.flags ??= {}
query.flags.urlName = true query.flags.urlName = true
query.name = cleanUrl(inputUrl) query.name = cleanUrl(inputUrl)
} }
@ -147,9 +153,12 @@
return qs.length === 0 ? newUrl : `${newUrl}?${qs}` return qs.length === 0 ? newUrl : `${newUrl}?${qs}`
} }
function buildQuery() { function buildQuery(fromQuery, urlQueries, requestBindings) {
const newQuery = cloneDeep(query) if (!fromQuery) {
const queryString = restUtils.buildQueryString(runtimeUrlQueries) return
}
const newQuery = cloneDeep(fromQuery)
const queryString = restUtils.buildQueryString(urlQueries)
newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings) newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings)
newQuery.fields.requestBody = newQuery.fields.requestBody =
@ -157,9 +166,8 @@
? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody) ? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody)
: readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody) : readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody)
newQuery.fields.path = url.split("?")[0] newQuery.fields.path = url?.split("?")[0]
newQuery.fields.queryString = queryString newQuery.fields.queryString = queryString
newQuery.fields.authConfigId = authConfigId
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders) newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
newQuery.schema = schema || {} newQuery.schema = schema || {}
newQuery.nestedSchemaFields = nestedSchemaFields || {} newQuery.nestedSchemaFields = nestedSchemaFields || {}
@ -168,13 +176,12 @@
} }
async function saveQuery() { async function saveQuery() {
const toSave = buildQuery() const toSave = builtQuery
saving = true
try { try {
const isNew = !query._rev const isNew = !query._rev
const { _id } = await queries.save(toSave.datasourceId, toSave) const { _id } = await queries.save(toSave.datasourceId, toSave)
saveId = _id saveId = _id
query = getSelectedQuery()
notifications.success(`Request saved successfully`)
if (dynamicVariables) { if (dynamicVariables) {
datasource.config.dynamicVariables = rebuildVariables(saveId) datasource.config.dynamicVariables = rebuildVariables(saveId)
datasource = await datasources.save({ datasource = await datasources.save({
@ -182,6 +189,13 @@
datasource, datasource,
}) })
} }
notifications.success(`Request saved successfully`)
if (isNew) {
$goto(`../../${_id}`)
}
query = getSelectedQuery()
prettifyQueryRequestBody( prettifyQueryRequestBody(
query, query,
requestBindings, requestBindings,
@ -189,11 +203,15 @@
staticVariables, staticVariables,
restBindings restBindings
) )
if (isNew) {
$goto(`../../${_id}`) // Force rebuilding original query
} originalQuery = null
queryNameLabel.disableEditingState()
} catch (err) { } catch (err) {
notifications.error(`Error saving query`) notifications.error(`Error saving query`)
} finally {
saving = false
} }
} }
@ -227,7 +245,7 @@
async function runQuery() { async function runQuery() {
try { try {
await validateQuery() await validateQuery()
response = await queries.preview(buildQuery()) response = await queries.preview(builtQuery)
if (response.rows.length === 0) { if (response.rows.length === 0) {
notifications.info("Request did not return any data") notifications.info("Request did not return any data")
} else { } else {
@ -249,22 +267,6 @@
} }
} }
const getAuthConfigId = () => {
let id = query.fields.authConfigId
if (id) {
// find the matching config on the datasource
const matchedConfig = datasource?.config?.authConfigs?.filter(
c => c._id === id
)[0]
// clear the id if the config is not found (deleted)
// i.e. just show 'None' in the dropdown
if (!matchedConfig) {
id = undefined
}
}
return id
}
const buildAuthConfigs = datasource => { const buildAuthConfigs = datasource => {
if (datasource?.config?.authConfigs) { if (datasource?.config?.authConfigs) {
return datasource.config.authConfigs.map(c => ({ return datasource.config.authConfigs.map(c => ({
@ -375,13 +377,6 @@
} }
} }
const paramsChanged = evt => {
breakQs = {}
for (let param of evt.detail) {
breakQs[param.name] = param.value
}
}
const urlChanged = evt => { const urlChanged = evt => {
breakQs = {} breakQs = {}
const qs = evt.target.value.split("?")[1] const qs = evt.target.value.split("?")[1]
@ -426,9 +421,7 @@
) { ) {
query.fields.path = `${datasource.config.url}/${path ? path : ""}` query.fields.path = `${datasource.config.url}/${path ? path : ""}`
} }
url = buildUrl(query.fields.path, breakQs)
requestBindings = restUtils.queryParametersToKeyValue(query.parameters) requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
authConfigId = getAuthConfigId()
if (!query.fields.disabledHeaders) { if (!query.fields.disabledHeaders) {
query.fields.disabledHeaders = {} query.fields.disabledHeaders = {}
} }
@ -497,6 +490,7 @@
<Layout gap="S"> <Layout gap="S">
<div class="top-bar"> <div class="top-bar">
<EditableLabel <EditableLabel
bind:this={queryNameLabel}
type="heading" type="heading"
bind:value={query.name} bind:value={query.name}
defaultValue="Untitled" defaultValue="Untitled"
@ -504,7 +498,9 @@
on:save={saveQuery} on:save={saveQuery}
/> />
<div class="controls"> <div class="controls">
<ConnectedQueryScreens sourceId={query._id} /> {#if query._id}
<ConnectedQueryScreens sourceId={query._id} />
{/if}
<div class="access"> <div class="access">
<Label>Access</Label> <Label>Access</Label>
<AccessLevelSelect {query} {saveId} /> <AccessLevelSelect {query} {saveId} />
@ -524,13 +520,13 @@
<div class="url"> <div class="url">
<Input <Input
on:blur={urlChanged} on:blur={urlChanged}
bind:value={url} bind:value={query.fields.path}
placeholder="http://www.api.com/endpoint" placeholder="http://www.api.com/endpoint"
/> />
</div> </div>
<Button primary disabled={!url} on:click={runQuery}>Send</Button> <Button primary disabled={!url} on:click={runQuery}>Send</Button>
<Button <Button
disabled={!query.name} disabled={!query.name || !isModified || saving}
cta cta
on:click={saveQuery} on:click={saveQuery}
tooltip={!hasSchema tooltip={!hasSchema
@ -557,16 +553,13 @@
/> />
</Tab> </Tab>
<Tab title="Params"> <Tab title="Params">
{#key breakQs} <KeyValueBuilder
<KeyValueBuilder bind:object={breakQs}
on:change={paramsChanged} name="param"
object={breakQs} headings
name="param" bindings={mergedBindings}
headings bindingDrawerLeft="260px"
bindings={mergedBindings} />
bindingDrawerLeft="260px"
/>
{/key}
</Tab> </Tab>
<Tab title="Headers"> <Tab title="Headers">
<KeyValueBuilder <KeyValueBuilder
@ -654,7 +647,7 @@
label="Auth" label="Auth"
labelPosition="left" labelPosition="left"
placeholder="None" placeholder="None"
bind:value={authConfigId} bind:value={query.fields.authConfigId}
options={authConfigs} options={authConfigs}
/> />
</div> </div>

View File

@ -1,20 +1,29 @@
<script> <script>
import RestAuthenticationBuilder from "./RestAuthenticationBuilder.svelte" import RestAuthenticationBuilder from "./RestAuthenticationBuilder.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
import Panel from "../Panel.svelte" import Panel from "../Panel.svelte"
import Tooltip from "../Tooltip.svelte" import Tooltip from "../Tooltip.svelte"
import { integrations } from "@/stores/builder"
import { notifications } from "@budibase/bbui"
export let datasource export let datasource
$: updatedDatasource = cloneDeep(datasource) $: updatedDatasource = cloneDeep(datasource)
const updateAuthConfigs = newAuthConfigs => { const updateAuthConfigs = async newAuthConfigs => {
updatedDatasource.config.authConfigs = newAuthConfigs updatedDatasource.config.authConfigs = newAuthConfigs
try {
await integrations.saveDatasource(updatedDatasource)
notifications.success(
`Datasource ${updatedDatasource.name} updated successfully`
)
} catch (error) {
notifications.error(`Error saving datasource: ${error.message}`)
}
} }
</script> </script>
<Panel> <Panel>
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
<Tooltip <Tooltip
slot="tooltip" slot="tooltip"
title="REST Authentication" title="REST Authentication"

View File

@ -1,8 +1,6 @@
<script> <script>
import { get } from "svelte/store"
import { isEqual } from "lodash" import { isEqual } from "lodash"
import { integrationForDatasource } from "@/stores/selectors" import { integrations } from "@/stores/builder"
import { integrations, datasources } from "@/stores/builder"
import { notifications, Button } from "@budibase/bbui" import { notifications, Button } from "@budibase/bbui"
export let datasource export let datasource
@ -12,11 +10,7 @@
const save = async () => { const save = async () => {
try { try {
const integration = integrationForDatasource( await integrations.saveDatasource(updatedDatasource)
get(integrations),
updatedDatasource
)
await datasources.save({ datasource: updatedDatasource, integration })
notifications.success( notifications.success(
`Datasource ${updatedDatasource.name} updated successfully` `Datasource ${updatedDatasource.name} updated successfully`
) )

View File

@ -1,6 +1,8 @@
import { writable, type Writable } from "svelte/store" import { get, writable, type Writable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { Integration } from "@budibase/types" import { Datasource, Integration } from "@budibase/types"
import { integrationForDatasource } from "@/stores/selectors"
import { datasources } from "./datasources"
type IntegrationsState = Record<string, Integration> type IntegrationsState = Record<string, Integration>
@ -25,9 +27,15 @@ const createIntegrationsStore = () => {
store.set(integrations) store.set(integrations)
} }
const saveDatasource = async (datasource: Datasource) => {
const integration = integrationForDatasource(get(store), datasource)
await datasources.save({ datasource, integration })
}
return { return {
...store, ...store,
init, init,
saveDatasource,
} }
} }