Add new custom datasource type

This commit is contained in:
Andrew Kingston 2023-10-05 14:42:26 +01:00
parent b75c78dae5
commit 43c30d877b
5 changed files with 162 additions and 22 deletions

View File

@ -9,6 +9,7 @@
Heading, Heading,
Drawer, Drawer,
DrawerContent, DrawerContent,
Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
@ -22,6 +23,7 @@
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
export let value = {} export let value = {}
export let otherSources export let otherSources
@ -31,9 +33,12 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const arrayTypes = ["attachment", "array"] const arrayTypes = ["attachment", "array"]
let anchorRight, dropdownRight let anchorRight, dropdownRight
let drawer let drawer
let tmpQueryParams let tmpQueryParams
let tmpCustomData
let customDataValid = true
$: text = value?.label ?? "Choose an option" $: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({ $: tables = $tablesStore.list.map(m => ({
@ -125,6 +130,10 @@
value: `{{ literal ${runtimeBinding} }}`, value: `{{ literal ${runtimeBinding} }}`,
} }
}) })
$: custom = {
type: "custom",
label: "Custom",
}
const handleSelected = selected => { const handleSelected = selected => {
dispatch("change", selected) dispatch("change", selected)
@ -151,6 +160,11 @@
drawer.show() drawer.show()
} }
const openCustomDrawer = () => {
tmpCustomData = value.data || ""
drawer.show()
}
const getQueryValue = queries => { const getQueryValue = queries => {
return queries.find(q => q._id === value._id) || value return queries.find(q => q._id === value._id) || value
} }
@ -162,6 +176,14 @@
}) })
drawer.hide() drawer.hide()
} }
const saveCustomData = () => {
handleSelected({
...value,
data: tmpCustomData,
})
drawer.hide()
}
</script> </script>
<div class="container" bind:this={anchorRight}> <div class="container" bind:this={anchorRight}>
@ -172,7 +194,9 @@
on:click={dropdownRight.show} on:click={dropdownRight.show}
/> />
{#if value?.type === "query"} {#if value?.type === "query"}
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} /> <div class="icon">
<Icon hoverable name="Settings" on:click={openQueryParamsDrawer} />
</div>
<Drawer title={"Query Bindings"} bind:this={drawer}> <Drawer title={"Query Bindings"} bind:this={drawer}>
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button> <Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
@ -198,6 +222,31 @@
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
{/if} {/if}
{#if value?.type === "custom"}
<div class="icon">
<Icon hoverable name="Settings" on:click={openCustomDrawer} />
</div>
<Drawer title="Custom data" bind:this={drawer}>
<Button
slot="buttons"
cta
on:click={saveCustomData}
disabled={!customDataValid}>Save</Button
>
<div slot="description">
Provide a JavaScript or JSON array to use as data
</div>
<ClientBindingPanel
slot="body"
bind:valid={customDataValid}
value={tmpCustomData}
on:change={event => (tmpCustomData = event.detail)}
{bindings}
allowJS
allowHelpers
/>
</Drawer>
{/if}
</div> </div>
<Popover bind:this={dropdownRight} anchor={anchorRight}> <Popover bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
@ -285,17 +334,18 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if otherSources?.length}
<Divider /> <Divider />
<div class="title"> <div class="title">
<Heading size="XS">Other</Heading> <Heading size="XS">Other</Heading>
</div> </div>
<ul> <ul>
<li on:click={() => handleSelected(custom)}>{custom.label}</li>
{#if otherSources?.length}
{#each otherSources as source} {#each otherSources as source}
<li on:click={() => handleSelected(source)}>{source.label}</li> <li on:click={() => handleSelected(source)}>{source.label}</li>
{/each} {/each}
</ul>
{/if} {/if}
</ul>
</div> </div>
</Popover> </Popover>
@ -340,16 +390,7 @@
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
} }
i { .icon {
margin-left: 5px; margin-left: 8px;
display: flex;
align-items: center;
transition: all 0.2s;
}
i:hover {
transform: scale(1.1);
font-weight: 600;
cursor: pointer;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
export const getColor = (idx, opacity = 0.3) => { export const getColor = (idx, opacity = 0.3) => {
if (idx == null || idx === -1) { if (idx == null || idx === -1) {
return null idx = 0
} }
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})` return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
} }

View File

@ -420,7 +420,7 @@ export const createActions = context => {
// Ensure we have a unique _id. // Ensure we have a unique _id.
// This means generating one for non DS+. // This means generating one for non DS+.
if (!newRow._id) { if (!newRow._id) {
newRow._id = `fake-${Helpers.hashString(JSON.stringify(newRow))}` newRow._id = `fake-${Math.random()}`
} }
if (!rowCacheMap[newRow._id]) { if (!rowCacheMap[newRow._id]) {

View File

@ -0,0 +1,97 @@
import DataFetch from "./DataFetch.js"
export default class CustomFetch extends DataFetch {
getType(value) {
if (value == null) {
return "string"
}
const type = typeof value
if (type === "object") {
if (Array.isArray(value)) {
return "array"
}
return "json"
} else {
return type
}
}
// Parses the custom data into an array format
parseCustomData(data) {
if (!data) {
return []
}
// Happy path - already an array
if (Array.isArray(data)) {
return data
}
// Handle string cases
if (typeof data === "string") {
// Try JSON parsing
try {
data = JSON.parse(data)
if (Array.isArray(data)) {
return data
}
} catch (error) {
// Ignore
}
// Try a simple CSV
return data.split(",").map(x => x.trim())
}
// Other cases we just assume it's a single object and wrap it
return [data]
}
// Enriches the custom data to ensure the structure and format is usable
enrichCustomData(data) {
if (!data?.length) {
return []
}
// Filter out any invalid values
data = data.filter(x => x != null && !Array.isArray(x))
// Ensure all values are packed into objects
return data.map(x => (typeof x === "object" ? x : { value: x }))
}
getCustomData(datasource) {
return this.enrichCustomData(this.parseCustomData(datasource?.data))
}
async getDefinition(datasource) {
// Try and work out the schema from the array provided
let schema = {}
const data = this.getCustomData(datasource)
// Go through every object and extract all valid keys
for (let datum of data) {
for (let key of Object.keys(datum)) {
if (key === "_id") {
continue
}
if (!schema[key]) {
schema[key] = { type: this.getType(datum[key]) }
}
}
}
return {
schema,
}
}
async getData() {
const { datasource } = this.options
return {
rows: this.getCustomData(datasource),
hasNextPage: false,
cursor: null,
}
}
}

View File

@ -8,6 +8,7 @@ import FieldFetch from "./FieldFetch.js"
import JSONArrayFetch from "./JSONArrayFetch.js" import JSONArrayFetch from "./JSONArrayFetch.js"
import UserFetch from "./UserFetch.js" import UserFetch from "./UserFetch.js"
import GroupUserFetch from "./GroupUserFetch.js" import GroupUserFetch from "./GroupUserFetch.js"
import CustomFetch from "./CustomFetch.js"
const DataFetchMap = { const DataFetchMap = {
table: TableFetch, table: TableFetch,
@ -17,6 +18,7 @@ const DataFetchMap = {
link: RelationshipFetch, link: RelationshipFetch,
user: UserFetch, user: UserFetch,
groupUser: GroupUserFetch, groupUser: GroupUserFetch,
custom: CustomFetch,
// Client specific datasource types // Client specific datasource types
provider: NestedProviderFetch, provider: NestedProviderFetch,