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 5bbb2b388d
commit a51f5c73c4
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
return path.filter(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
dataProviders.forEach(component => {
const isForm = component._component.endsWith("/form")
const datasource = getDatasourceForProvider(component)
let tableName, schema
const def = store.actions.components.getDefinition(component._component)
const contextDefinition = def.dataContext
let schema
// Forms are an edge case which do not need table schemas
if (isForm) {
if (contextDefinition.type === "form") {
schema = buildFormSchema(component)
tableName = "Fields"
} else {
const datasource = getDatasourceForProvider(component)
if (!datasource) {
return
}

View File

@ -1,5 +1,6 @@
[
"container",
"dataprovider",
"datagrid",
"list",
"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>
import DatasourceSelect from "./DatasourceSelect.svelte"
import DataSourceSelect from "./DataSourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }]
</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 Checkbox from "./PropertyControls/Checkbox.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 MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
@ -61,7 +62,8 @@
const controlMap = {
text: Input,
select: OptionSelect,
datasource: DatasourceSelect,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script>
import { getContext, setContext, onMount } from "svelte"
import { datasourceStore, createContextStore } from "../store"
import { dataSourceStore, createContextStore } from "../store"
import { ActionTypes } from "../constants"
import { generate } from "shortid"
@ -31,9 +31,9 @@
// Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) {
const { datasource } = metadata || {}
datasourceStore.actions.registerDatasource(
datasource,
const { dataSource } = metadata || {}
dataSourceStore.actions.registerDataSource(
dataSource,
instanceId,
callback
)
@ -48,7 +48,7 @@
instanceId = generate()
// Unregister all datasource instances when unmounting this provider
return () => datasourceStore.actions.unregisterInstance(instanceId)
return () => dataSourceStore.actions.unregisterInstance(instanceId)
})
</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 { screenStore } from "./screens"
export { builderStore } from "./builder"
export { datasourceStore } from "./datasource"
export { dataSourceStore } from "./dataSource"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

View File

@ -84,13 +84,11 @@
"icon": "ri-list-check-2",
"styleable": true,
"hasChildren": true,
"dataProvider": true,
"actions": ["RefreshDatasource"],
"settings": [
{
"type": "datasource",
"type": "dataProvider",
"label": "Data",
"key": "datasource"
"key": "dataProviderId"
},
{
"type": "text",
@ -103,7 +101,11 @@
"label": "Filtering",
"key": "filter"
}
]
],
"dataContext": {
"type": "schema",
"dataProviderSetting": "dataProviderId"
}
},
"search": {
"name": "Search",
@ -1472,5 +1474,44 @@
"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,58 +1,23 @@
<script>
import { getContext } from "svelte"
import { isEmpty } from "lodash/fp"
export let datasource
export let dataProviderId
export let noRowsMessage
export let filter
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
"sdk"
)
const { API, styleable, builderStore, Provider } = getContext("sdk")
const component = getContext("component")
let rows = []
let loaded = false
const context = getContext("context")
$: fetchData(datasource)
$: filteredRows = filterRows(rows, filter)
$: 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
}
$: data = context[dataProviderId]?.rows ?? []
$: loaded = context[dataProviderId]?.loaded ?? false
</script>
<Provider {actions}>
<div use:styleable={$component.styles}>
{#if filteredRows.length > 0}
<div use:styleable={$component.styles}>
{#if data.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder}
<p><i class="ri-image-line" />Add some components to display.</p>
{:else}
{#each filteredRows as row}
{#each data as row}
<Provider data={row}>
<slot />
</Provider>
@ -61,8 +26,7 @@
{:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
{/if}
</div>
</Provider>
</div>
<style>
p {

View File

@ -13,6 +13,7 @@ import { loadSpectrumIcons } from "./spectrum-icons"
loadSpectrumIcons()
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 screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte"