Add initial support for query datasources in grids

This commit is contained in:
Andrew Kingston 2023-10-03 17:35:00 +01:00
parent b9ae3d0e23
commit d03fdb6df9
13 changed files with 178 additions and 34 deletions

View File

@ -5545,12 +5545,11 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"info": "Grid Blocks are only compatible with internal or SQL tables",
"settings": [ "settings": [
{ {
"type": "table", "type": "dataSource",
"label": "Data", "label": "Data",
"key": "table", "key": "datasource",
"required": true "required": true
}, },
{ {

View File

@ -4,7 +4,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
export let table export let datasource
export let allowAddRows = true export let allowAddRows = true
export let allowEditRows = true export let allowEditRows = true
export let allowDeleteRows = true export let allowDeleteRows = true
@ -15,6 +15,9 @@
export let fixedRowHeight = null export let fixedRowHeight = null
export let columns = null export let columns = null
// Legacy settings
export let table
const component = getContext("component") const component = getContext("component")
const { styleable, API, builderStore, notificationStore } = getContext("sdk") const { styleable, API, builderStore, notificationStore } = getContext("sdk")
@ -38,7 +41,7 @@
class:in-builder={$builderStore.inBuilder} class:in-builder={$builderStore.inBuilder}
> >
<Grid <Grid
datasource={table} datasource={datasource || table}
{API} {API}
{stripeRows} {stripeRows}
{initialFilter} {initialFilter}

View File

@ -34,7 +34,7 @@
column.schema.autocolumn || column.schema.autocolumn ||
column.schema.disabled || column.schema.disabled ||
column.schema.type === "formula" || column.schema.type === "formula" ||
(!$config.canEditRows && row._id) (!$config.canEditRows && !row._isNewRow)
// Register this cell API if the row is focused // Register this cell API if the row is focused
$: { $: {

View File

@ -33,7 +33,7 @@
let visible = false let visible = false
let isAdding = false let isAdding = false
let newRow = {} let newRow = { _isNewRow: true }
let offset = 0 let offset = 0
$: firstColumn = $stickyColumn || $renderedColumns[0] $: firstColumn = $stickyColumn || $renderedColumns[0]
@ -58,7 +58,9 @@
// Create row // Create row
const newRowIndex = offset ? undefined : 0 const newRowIndex = offset ? undefined : 0
const savedRow = await rows.actions.addRow(newRow, newRowIndex) let rowToCreate = { ...newRow }
delete rowToCreate._isNewRow
const savedRow = await rows.actions.addRow(rowToCreate, newRowIndex)
if (savedRow) { if (savedRow) {
// Reset state // Reset state
clear() clear()

View File

@ -37,9 +37,10 @@ export const deriveStores = context => {
[props, hasNonAutoColumn], [props, hasNonAutoColumn],
([$props, $hasNonAutoColumn]) => { ([$props, $hasNonAutoColumn]) => {
let config = { ...$props } let config = { ...$props }
const type = $props.datasource?.type
// Disable some features if we're editing a view // Disable some features if we're editing a view
if ($props.datasource?.type === "viewV2") { if (type === "viewV2") {
config.canEditColumns = false config.canEditColumns = false
} }
@ -48,6 +49,14 @@ export const deriveStores = context => {
config.canAddRows = false config.canAddRows = false
} }
// Disable features for non DS+
if (!["table", "viewV2"].includes(type)) {
config.canAddRows = false
config.canEditRows = false
config.canDeleteRows = false
config.canExpandRows = false
}
return config return config
} }
) )

View File

@ -1,4 +1,5 @@
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { getDatasourceDefinition } from "../../../fetch"
export const createStores = () => { export const createStores = () => {
const definition = writable(null) const definition = writable(null)
@ -19,6 +20,14 @@ export const deriveStores = context => {
} }
let newSchema = { ...$definition?.schema } let newSchema = { ...$definition?.schema }
// Ensure schema is configured as objects.
// Certain datasources like queries use primitives.
Object.keys(newSchema).forEach(key => {
if (typeof newSchema[key] !== "object") {
newSchema[key] = { type: newSchema[key] }
}
})
// Apply schema overrides // Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => { Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) { if (newSchema[field]) {
@ -48,7 +57,16 @@ export const deriveStores = context => {
} }
export const createActions = context => { export const createActions = context => {
const { datasource, definition, config, dispatch, table, viewV2 } = context const {
API,
datasource,
definition,
config,
dispatch,
table,
viewV2,
query,
} = context
// Gets the appropriate API for the configured datasource type // Gets the appropriate API for the configured datasource type
const getAPI = () => { const getAPI = () => {
@ -58,6 +76,8 @@ export const createActions = context => {
return table return table
case "viewV2": case "viewV2":
return viewV2 return viewV2
case "query":
return query
default: default:
return null return null
} }
@ -65,7 +85,8 @@ export const createActions = context => {
// Refreshes the datasource definition // Refreshes the datasource definition
const refreshDefinition = async () => { const refreshDefinition = async () => {
return await getAPI()?.actions.refreshDefinition() const def = await getDatasourceDefinition({ API, datasource })
definition.set(def)
} }
// Saves the datasource definition // Saves the datasource definition

View File

@ -17,6 +17,7 @@ import * as Filter from "./filter"
import * as Notifications from "./notifications" import * as Notifications from "./notifications"
import * as Table from "./table" import * as Table from "./table"
import * as ViewV2 from "./viewV2" import * as ViewV2 from "./viewV2"
import * as Query from "./query"
import * as Datasource from "./datasource" import * as Datasource from "./datasource"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
@ -26,6 +27,7 @@ const DependencyOrderedStores = [
Scroll, Scroll,
Table, Table,
ViewV2, ViewV2,
Query,
Datasource, Datasource,
Columns, Columns,
Rows, Rows,

View File

@ -0,0 +1,108 @@
import { get } from "svelte/store"
export const createActions = context => {
const { API, columns, stickyColumn } = context
const saveDefinition = async newDefinition => {
await API.saveQuery(newDefinition)
}
const saveRow = async () => {
throw "Rows cannot be updated through queries"
}
const deleteRows = async () => {
throw "Rows cannot be deleted through queries"
}
const getRow = () => {
throw "Queries don't support fetching individual rows"
}
const isDatasourceValid = datasource => {
return datasource?.type === "query" && datasource?._id
}
const canUseColumn = name => {
const $columns = get(columns)
const $sticky = get(stickyColumn)
return $columns.some(col => col.name === name) || $sticky?.name === name
}
return {
query: {
actions: {
saveDefinition,
addRow: saveRow,
updateRow: saveRow,
deleteRows,
getRow,
isDatasourceValid,
canUseColumn,
},
},
}
}
export const initialise = context => {
const {
datasource,
sort,
filter,
query,
initialFilter,
initialSortColumn,
initialSortOrder,
fetch,
} = context
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
// Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => {
// Clear previous subscriptions
unsubscribers?.forEach(unsubscribe => unsubscribe())
unsubscribers = []
if (!query.actions.isDatasourceValid($datasource)) {
return
}
// Wipe state
filter.set(get(initialFilter))
sort.set({
column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending",
})
// Update fetch when filter changes
unsubscribers.push(
filter.subscribe($filter => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?._id !== $datasource._id) {
return
}
$fetch.update({
filter: $filter,
})
})
)
// Update fetch when sorting changes
unsubscribers.push(
sort.subscribe($sort => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?._id !== $datasource._id) {
return
}
$fetch.update({
sortOrder: $sort.order || "ascending",
sortColumn: $sort.column,
})
})
)
})
}

View File

@ -1,7 +1,8 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData" import { fetchData } from "../../../fetch"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { tick } from "svelte" import { tick } from "svelte"
import { Helpers } from "@budibase/bbui"
export const createStores = () => { export const createStores = () => {
const rows = writable([]) const rows = writable([])
@ -413,6 +414,13 @@ export const createActions = context => {
let newRow let newRow
for (let i = 0; i < newRows.length; i++) { for (let i = 0; i < newRows.length; i++) {
newRow = newRows[i] newRow = newRows[i]
// Ensure we have a unique _id.
// This means generating one for non DS+.
if (!newRow._id) {
newRow._id = Helpers.hashString(JSON.stringify(newRow))
}
if (!rowCacheMap[newRow._id]) { if (!rowCacheMap[newRow._id]) {
rowCacheMap[newRow._id] = true rowCacheMap[newRow._id] = true
rowsToAppend.push(newRow) rowsToAppend.push(newRow)

View File

@ -3,11 +3,7 @@ import { get } from "svelte/store"
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context const { API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
definition.set(await API.fetchTableDefinition(get(datasource).tableId))
}
const saveDefinition = async newDefinition => { const saveDefinition = async newDefinition => {
await API.saveTable(newDefinition) await API.saveTable(newDefinition)
@ -52,7 +48,6 @@ export const createActions = context => {
return { return {
table: { table: {
actions: { actions: {
refreshDefinition,
saveDefinition, saveDefinition,
addRow: saveRow, addRow: saveRow,
updateRow: saveRow, updateRow: saveRow,

View File

@ -3,20 +3,7 @@ import { get } from "svelte/store"
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context const { API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
const $datasource = get(datasource)
if (!$datasource) {
definition.set(null)
return
}
const table = await API.fetchTableDefinition($datasource.tableId)
const view = Object.values(table?.views || {}).find(
view => view.id === $datasource.id
)
definition.set(view)
}
const saveDefinition = async newDefinition => { const saveDefinition = async newDefinition => {
await API.viewV2.update(newDefinition) await API.viewV2.update(newDefinition)
@ -61,7 +48,6 @@ export const createActions = context => {
return { return {
viewV2: { viewV2: {
actions: { actions: {
refreshDefinition,
saveDefinition, saveDefinition,
addRow: saveRow, addRow: saveRow,
updateRow: saveRow, updateRow: saveRow,

View File

@ -24,7 +24,18 @@ const DataFetchMap = {
jsonarray: JSONArrayFetch, jsonarray: JSONArrayFetch,
} }
// Constructs a new fetch model for a certain datasource
export const fetchData = ({ API, datasource, options }) => { export const fetchData = ({ API, datasource, options }) => {
const Fetch = DataFetchMap[datasource?.type] || TableFetch const Fetch = DataFetchMap[datasource?.type] || TableFetch
return new Fetch({ API, datasource, ...options }) return new Fetch({ API, datasource, ...options })
} }
// Fetches the definition of any type of datasource
export const getDatasourceDefinition = async ({ API, datasource }) => {
const handler = DataFetchMap[datasource?.type]
if (!handler) {
return null
}
const instance = new handler({ API })
return await instance.getDefinition(datasource)
}

View File

@ -1,5 +1,5 @@
export { createAPIClient } from "./api" export { createAPIClient } from "./api"
export { fetchData } from "./fetch/fetchData" export { fetchData } from "./fetch"
export { Utils } from "./utils" export { Utils } from "./utils"
export * as Constants from "./constants" export * as Constants from "./constants"
export * from "./stores" export * from "./stores"