Add data provider component and refactor component setting types

This commit is contained in:
Andrew Kingston 2021-03-16 13:54:34 +00:00
parent 2685d46266
commit 6e29423d4d
18 changed files with 302 additions and 176 deletions

View File

@ -35,7 +35,7 @@ export const getDataProviderComponents = (asset, componentId) => {
// Filter by only data provider components // Filter by only data provider components
return path.filter(component => { return path.filter(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
return def?.dataProvider return def?.dataContext != null
}) })
} }
@ -101,15 +101,16 @@ const getContextBindings = (asset, componentId) => {
// Create bindings for each data provider // Create bindings for each data provider
dataProviders.forEach(component => { dataProviders.forEach(component => {
const isForm = component._component.endsWith("/form") const def = store.actions.components.getDefinition(component._component)
const datasource = getDatasourceForProvider(component) const contextDefinition = def.dataContext
let tableName, schema let schema
// Forms are an edge case which do not need table schemas // Forms are an edge case which do not need table schemas
if (isForm) { if (contextDefinition.type === "form") {
schema = buildFormSchema(component) schema = buildFormSchema(component)
tableName = "Fields" tableName = "Fields"
} else { } else {
const datasource = getDatasourceForProvider(component)
if (!datasource) { if (!datasource) {
return return
} }

View File

@ -1,5 +1,6 @@
[ [
"container", "container",
"dataprovider",
"datagrid", "datagrid",
"list", "list",
"button", "button",

View File

@ -0,0 +1,23 @@
<script>
import { Select } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils"
export let value
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
$: console.log(path)
$: providers = path.filter(
component =>
component._component === "@budibase/standard-components/dataprovider"
)
</script>
<Select thin secondary {value} on:change>
<option value="">Choose option</option>
{#if providers}
{#each providers as component}
<option value={component._id}>{component._instanceName}</option>
{/each}
{/if}
</Select>

View File

@ -1,7 +1,7 @@
<script> <script>
import DatasourceSelect from "./DatasourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }] const otherSources = [{ name: "Custom", label: "Custom" }]
</script> </script>
<DatasourceSelect on:change {...$$props} showAllQueries={true} {otherSources} /> <DataSourceSelect on:change {...$$props} showAllQueries={true} {otherSources} />

View File

@ -13,7 +13,8 @@
import OptionSelect from "./PropertyControls/OptionSelect.svelte" import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte" import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte" import TableSelect from "./PropertyControls/TableSelect.svelte"
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte" import DataSourceSelect from "./PropertyControls/DataSourceSelect.svelte"
import DataProviderSelect from "./PropertyControls/DataProviderSelect.svelte"
import FieldSelect from "./PropertyControls/FieldSelect.svelte" import FieldSelect from "./PropertyControls/FieldSelect.svelte"
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte" import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte" import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
@ -61,7 +62,8 @@
const controlMap = { const controlMap = {
text: Input, text: Input,
select: OptionSelect, select: OptionSelect,
datasource: DatasourceSelect, dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
detailScreen: DetailScreenSelect, detailScreen: DetailScreenSelect,
boolean: Checkbox, boolean: Checkbox,
number: Input, number: Input,

View File

@ -7,31 +7,31 @@ import { executeQuery } from "./queries"
/** /**
* Fetches all rows for a particular Budibase data source. * Fetches all rows for a particular Budibase data source.
*/ */
export const fetchDatasource = async datasource => { export const fetchDatasource = async dataSource => {
if (!datasource || !datasource.type) { if (!dataSource || !dataSource.type) {
return [] return []
} }
// Fetch all rows in data source // Fetch all rows in data source
const { type, tableId, fieldName } = datasource const { type, tableId, fieldName } = dataSource
let rows = [] let rows = []
if (type === "table") { if (type === "table") {
rows = await fetchTableData(tableId) rows = await fetchTableData(tableId)
} else if (type === "view") { } else if (type === "view") {
rows = await fetchViewData(datasource) rows = await fetchViewData(dataSource)
} else if (type === "query") { } else if (type === "query") {
// Set the default query params // Set the default query params
let parameters = cloneDeep(datasource.queryParams || {}) let parameters = cloneDeep(dataSource.queryParams || {})
for (let param of datasource.parameters) { for (let param of dataSource.parameters) {
if (!parameters[param.name]) { if (!parameters[param.name]) {
parameters[param.name] = param.default parameters[param.name] = param.default
} }
} }
rows = await executeQuery({ queryId: datasource._id, parameters }) rows = await executeQuery({ queryId: dataSource._id, parameters })
} else if (type === "link") { } else if (type === "link") {
rows = await fetchRelationshipData({ rows = await fetchRelationshipData({
rowId: datasource.rowId, rowId: dataSource.rowId,
tableId: datasource.rowTableId, tableId: dataSource.rowTableId,
fieldName, fieldName,
}) })
} }

View File

@ -1,4 +1,4 @@
import { notificationStore, datasourceStore } from "../store" import { notificationStore, dataSourceStore } from "../store"
import API from "./api" import API from "./api"
/** /**
@ -20,7 +20,7 @@ export const executeQuery = async ({ queryId, parameters }) => {
notificationStore.danger("An error has occurred") notificationStore.danger("An error has occurred")
} else if (!query.readable) { } else if (!query.readable) {
notificationStore.success("Query executed successfully") notificationStore.success("Query executed successfully")
datasourceStore.actions.invalidateDatasource(query.datasourceId) dataSourceStore.actions.invalidateDataSource(query.datasourceId)
} }
return res return res
} }

View File

@ -1,4 +1,4 @@
import { notificationStore, datasourceStore } from "../store" import { notificationStore, dataSourceStore } from "../store"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
@ -31,7 +31,7 @@ export const saveRow = async row => {
: notificationStore.success("Row saved") : notificationStore.success("Row saved")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
return res return res
} }
@ -52,7 +52,7 @@ export const updateRow = async row => {
: notificationStore.success("Row updated") : notificationStore.success("Row updated")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
return res return res
} }
@ -72,7 +72,7 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
: notificationStore.success("Row deleted") : notificationStore.success("Row deleted")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)
return res return res
} }
@ -96,7 +96,7 @@ export const deleteRows = async ({ tableId, rows }) => {
: notificationStore.success(`${rows.length} row(s) deleted`) : notificationStore.success(`${rows.length} row(s) deleted`)
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)
return res return res
} }

View File

@ -33,7 +33,7 @@
{ {
type: ActionTypes.RefreshDatasource, type: ActionTypes.RefreshDatasource,
callback: () => authStore.actions.fetchUser(), callback: () => authStore.actions.fetchUser(),
metadata: { datasource: { type: "table", tableId: TableNames.USERS } }, metadata: { dataSource: { type: "table", tableId: TableNames.USERS } },
}, },
] ]
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, setContext, onMount } from "svelte" import { getContext, setContext, onMount } from "svelte"
import { datasourceStore, createContextStore } from "../store" import { dataSourceStore, createContextStore } from "../store"
import { ActionTypes } from "../constants" import { ActionTypes } from "../constants"
import { generate } from "shortid" import { generate } from "shortid"
@ -31,9 +31,9 @@
// Register any "refresh datasource" actions with a singleton store // Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource // so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) { if (type === ActionTypes.RefreshDatasource) {
const { datasource } = metadata || {} const { dataSource } = metadata || {}
datasourceStore.actions.registerDatasource( dataSourceStore.actions.registerDataSource(
datasource, dataSource,
instanceId, instanceId,
callback callback
) )
@ -48,7 +48,7 @@
instanceId = generate() instanceId = generate()
// Unregister all datasource instances when unmounting this provider // Unregister all datasource instances when unmounting this provider
return () => datasourceStore.actions.unregisterInstance(instanceId) return () => dataSourceStore.actions.unregisterInstance(instanceId)
}) })
</script> </script>

View File

@ -0,0 +1,84 @@
import { writable, get } from "svelte/store"
import { notificationStore } from "./notification"
export const createDataSourceStore = () => {
const store = writable([])
// Registers a new dataSource instance
const registerDataSource = (dataSource, instanceId, refresh) => {
if (!dataSource || !instanceId || !refresh) {
return
}
// Create a list of all relevant dataSource IDs which would require that
// this dataSource is refreshed
let dataSourceIds = []
// Extract table ID
if (dataSource.type === "table" || dataSource.type === "view") {
if (dataSource.tableId) {
dataSourceIds.push(dataSource.tableId)
}
}
// Extract both table IDs from both sides of the relationship
else if (dataSource.type === "link") {
if (dataSource.rowTableId) {
dataSourceIds.push(dataSource.rowTableId)
}
if (dataSource.tableId) {
dataSourceIds.push(dataSource.tableId)
}
}
// Extract the dataSource ID (not the query ID) for queries
else if (dataSource.type === "query") {
if (dataSource.dataSourceId) {
dataSourceIds.push(dataSource.dataSourceId)
}
}
// Store configs for each relevant dataSource ID
if (dataSourceIds.length) {
store.update(state => {
dataSourceIds.forEach(id => {
state.push({
dataSourceId: id,
instanceId,
refresh,
})
})
return state
})
}
}
// Removes all registered dataSource instances belonging to a particular
// instance ID
const unregisterInstance = instanceId => {
store.update(state => {
return state.filter(instance => instance.instanceId !== instanceId)
})
}
// Invalidates a specific dataSource ID by refreshing all instances
// which depend on data from that dataSource
const invalidateDataSource = dataSourceId => {
const relatedInstances = get(store).filter(instance => {
return instance.dataSourceId === dataSourceId
})
if (relatedInstances?.length) {
notificationStore.blockNotifications(1000)
}
relatedInstances?.forEach(instance => {
instance.refresh()
})
}
return {
subscribe: store.subscribe,
actions: { registerDataSource, unregisterInstance, invalidateDataSource },
}
}
export const dataSourceStore = createDataSourceStore()

View File

@ -1,84 +0,0 @@
import { writable, get } from "svelte/store"
import { notificationStore } from "./notification"
export const createDatasourceStore = () => {
const store = writable([])
// Registers a new datasource instance
const registerDatasource = (datasource, instanceId, refresh) => {
if (!datasource || !instanceId || !refresh) {
return
}
// Create a list of all relevant datasource IDs which would require that
// this datasource is refreshed
let datasourceIds = []
// Extract table ID
if (datasource.type === "table" || datasource.type === "view") {
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract both table IDs from both sides of the relationship
else if (datasource.type === "link") {
if (datasource.rowTableId) {
datasourceIds.push(datasource.rowTableId)
}
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract the datasource ID (not the query ID) for queries
else if (datasource.type === "query") {
if (datasource.datasourceId) {
datasourceIds.push(datasource.datasourceId)
}
}
// Store configs for each relevant datasource ID
if (datasourceIds.length) {
store.update(state => {
datasourceIds.forEach(id => {
state.push({
datasourceId: id,
instanceId,
refresh,
})
})
return state
})
}
}
// Removes all registered datasource instances belonging to a particular
// instance ID
const unregisterInstance = instanceId => {
store.update(state => {
return state.filter(instance => instance.instanceId !== instanceId)
})
}
// Invalidates a specific datasource ID by refreshing all instances
// which depend on data from that datasource
const invalidateDatasource = datasourceId => {
const relatedInstances = get(store).filter(instance => {
return instance.datasourceId === datasourceId
})
if (relatedInstances?.length) {
notificationStore.blockNotifications(1000)
}
relatedInstances?.forEach(instance => {
instance.refresh()
})
}
return {
subscribe: store.subscribe,
actions: { registerDatasource, unregisterInstance, invalidateDatasource },
}
}
export const datasourceStore = createDatasourceStore()

View File

@ -3,7 +3,7 @@ export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { datasourceStore } from "./datasource" export { dataSourceStore } from "./dataSource"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -84,13 +84,11 @@
"icon": "ri-list-check-2", "icon": "ri-list-check-2",
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true,
"actions": ["RefreshDatasource"],
"settings": [ "settings": [
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProviderId"
}, },
{ {
"type": "text", "type": "text",
@ -103,7 +101,11 @@
"label": "Filtering", "label": "Filtering",
"key": "filter" "key": "filter"
} }
] ],
"dataContext": {
"type": "schema",
"dataProviderSetting": "dataProviderId"
}
}, },
"search": { "search": {
"name": "Search", "name": "Search",
@ -1472,5 +1474,44 @@
"defaultValue": false "defaultValue": false
} }
] ]
},
"dataprovider": {
"name": "Data Provider",
"icon": "ri-database-2-line",
"styleable": false,
"hasChildren": true,
"settings": [
{
"type": "dataSource",
"label": "Data",
"key": "dataSource"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
}
],
"dataContext": {
"type": "static",
"values": [
{
"label": "Rows",
"key": "rows"
},
{
"label": "Rows Length",
"key": "rowsLength"
},
{
"label": "Loading",
"key": "loading"
},
{
"label": "Loaded",
"key": "loaded"
}
]
}
} }
} }

View File

@ -0,0 +1,93 @@
<script>
import { getContext } from "svelte"
export let dataSource
export let filter
export let sortColumn
export let sortOrder
export let limit
const { API, styleable, Provider, ActionTypes } = getContext("sdk")
const component = getContext("component")
// Loading flag every time data is being fetched
let loading = false
// Loading flag for the initial load
let loaded = false
let allRows = []
$: fetchData(dataSource)
$: filteredRows = filterRows(allRows, filter)
$: sortedRows = sortRows(filteredRows, sortColumn, sortOrder)
$: rows = limitRows(sortedRows, limit)
$: {
console.log(allRows)
console.log(filteredRows)
console.log(sortedRows)
console.log(rows)
}
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(dataSource),
metadata: { dataSource },
},
]
$: dataContext = {
rows,
rowsLength: rows.length,
loading,
loaded,
}
const fetchData = async dataSource => {
loading = true
allRows = await API.fetchDatasource(dataSource)
loading = false
loaded = false
}
const filterRows = (rows, filter) => {
if (!Object.keys(filter || {}).length) {
return rows
}
let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => {
if (value != null && value !== "") {
filteredData = filteredData.filter(row => {
return row[field] === value
})
}
})
return filteredData
}
const sortRows = (rows, sortColumn, sortOrder) => {
if (!sortColumn || !sortOrder) {
return rows
}
return rows.slice().sort((a, b) => {
const colA = a[sortColumn]
const colB = b[sortColumn]
if (sortOrder === "descending") {
return colA > colB ? -1 : 1
} else {
return colA > colB ? 1 : -1
}
})
}
const limitRows = (rows, limit) => {
const numLimit = parseFloat(limit)
if (isNaN(numLimit)) {
return rows
}
return rows.slice(0, numLimit)
}
</script>
<Provider {actions} data={dataContext}>
<slot />
</Provider>

View File

@ -1,68 +1,32 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { isEmpty } from "lodash/fp"
export let datasource export let dataProviderId
export let noRowsMessage export let noRowsMessage
export let filter
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( const { API, styleable, builderStore, Provider } = getContext("sdk")
"sdk"
)
const component = getContext("component") const component = getContext("component")
let rows = [] const context = getContext("context")
let loaded = false
$: fetchData(datasource) $: data = context[dataProviderId]?.rows ?? []
$: filteredRows = filterRows(rows, filter) $: loaded = context[dataProviderId]?.loaded ?? false
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(datasource),
metadata: { datasource },
},
]
const fetchData = async datasource => {
if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource)
}
loaded = true
}
const filterRows = (rows, filter) => {
if (!Object.keys(filter || {}).length) {
return rows
}
let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => {
if (value != null && value !== "") {
filteredData = filteredData.filter(row => {
return row[field] === value
})
}
})
return filteredData
}
</script> </script>
<Provider {actions}> <div use:styleable={$component.styles}>
<div use:styleable={$component.styles}> {#if data.length > 0}
{#if filteredRows.length > 0} {#if $component.children === 0 && $builderStore.inBuilder}
{#if $component.children === 0 && $builderStore.inBuilder} <p><i class="ri-image-line" />Add some components to display.</p>
<p><i class="ri-image-line" />Add some components to display.</p> {:else}
{:else} {#each data as row}
{#each filteredRows as row} <Provider data={row}>
<Provider data={row}> <slot />
<slot /> </Provider>
</Provider> {/each}
{/each}
{/if}
{:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
{/if} {/if}
</div> {:else if loaded && noRowsMessage}
</Provider> <p><i class="ri-list-check-2" />{noRowsMessage}</p>
{/if}
</div>
<style> <style>
p { p {

View File

@ -13,6 +13,7 @@ import { loadSpectrumIcons } from "./spectrum-icons"
loadSpectrumIcons() loadSpectrumIcons()
export { default as container } from "./Container.svelte" export { default as container } from "./Container.svelte"
export { default as dataprovider } from "./DataProvider.svelte"
export { default as datagrid } from "./grid/Component.svelte" export { default as datagrid } from "./grid/Component.svelte"
export { default as screenslot } from "./ScreenSlot.svelte" export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte" export { default as button } from "./Button.svelte"