279 lines
6.7 KiB
Svelte
279 lines
6.7 KiB
Svelte
<script>
|
|
import { getContext } from "svelte"
|
|
import { ProgressCircle, Pagination } from "@budibase/bbui"
|
|
import {
|
|
buildLuceneQuery,
|
|
luceneQuery,
|
|
luceneSort,
|
|
luceneLimit,
|
|
} from "./lucene"
|
|
import Placeholder from "./Placeholder.svelte"
|
|
|
|
export let dataSource
|
|
export let filter
|
|
export let sortColumn
|
|
export let sortOrder
|
|
export let limit
|
|
export let paginate
|
|
|
|
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
|
|
// Mark as loaded if we have no datasource so we don't stall forever
|
|
let loaded = !dataSource
|
|
let schemaLoaded = false
|
|
|
|
// Provider state
|
|
let rows = []
|
|
let allRows = []
|
|
let schema = {}
|
|
let bookmarks = [null]
|
|
let pageNumber = 0
|
|
let query = null
|
|
|
|
$: query = buildLuceneQuery(filter)
|
|
$: internalTable = dataSource?.type === "table"
|
|
$: nestedProvider = dataSource?.type === "provider"
|
|
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
|
$: hasPrevPage = pageNumber > 0
|
|
$: getSchema(dataSource)
|
|
$: sortType = getSortType(schema, sortColumn)
|
|
$: {
|
|
// Wait until schema loads before loading data, so that we can determine
|
|
// the correct sort type first time
|
|
if (schemaLoaded) {
|
|
fetchData(
|
|
dataSource,
|
|
query,
|
|
limit,
|
|
sortColumn,
|
|
sortOrder,
|
|
sortType,
|
|
paginate
|
|
)
|
|
}
|
|
}
|
|
$: {
|
|
if (internalTable) {
|
|
// Internal tables are already processed server-side
|
|
rows = allRows
|
|
} else {
|
|
// For anything else we use client-side implementations to filter, sort
|
|
// and limit
|
|
const filtered = luceneQuery(allRows, query)
|
|
const sorted = luceneSort(filtered, sortColumn, sortOrder, sortType)
|
|
rows = luceneLimit(sorted, limit)
|
|
}
|
|
}
|
|
$: actions = [
|
|
{
|
|
type: ActionTypes.RefreshDatasource,
|
|
callback: () => refresh(),
|
|
metadata: { dataSource },
|
|
},
|
|
{
|
|
type: ActionTypes.SetDataProviderQuery,
|
|
callback: newQuery => (query = newQuery),
|
|
},
|
|
]
|
|
$: dataContext = {
|
|
rows,
|
|
schema,
|
|
rowsLength: rows.length,
|
|
|
|
// Undocumented properties. These aren't supposed to be used in builder
|
|
// bindings, but are used internally by other components
|
|
id: $component?.id,
|
|
state: { query },
|
|
loaded,
|
|
}
|
|
|
|
const getSortType = (schema, sortColumn) => {
|
|
if (!schema || !sortColumn || !schema[sortColumn]) {
|
|
return "string"
|
|
}
|
|
const type = schema?.[sortColumn]?.type
|
|
return type === "number" ? "number" : "string"
|
|
}
|
|
|
|
const refresh = async () => {
|
|
if (schemaLoaded && !nestedProvider) {
|
|
fetchData(
|
|
dataSource,
|
|
query,
|
|
limit,
|
|
sortColumn,
|
|
sortOrder,
|
|
sortType,
|
|
paginate
|
|
)
|
|
}
|
|
}
|
|
|
|
const fetchData = async (
|
|
dataSource,
|
|
query,
|
|
limit,
|
|
sortColumn,
|
|
sortOrder,
|
|
sortType,
|
|
paginate
|
|
) => {
|
|
loading = true
|
|
if (dataSource?.type === "table") {
|
|
// For internal tables we use server-side processing
|
|
const res = await API.searchTable({
|
|
tableId: dataSource.tableId,
|
|
query,
|
|
limit,
|
|
sort: sortColumn,
|
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
|
sortType,
|
|
paginate,
|
|
})
|
|
pageNumber = 0
|
|
allRows = res.rows
|
|
if (res.hasNextPage) {
|
|
bookmarks = [null, res.bookmark]
|
|
} else {
|
|
bookmarks = [null]
|
|
}
|
|
} else if (dataSource?.type === "provider") {
|
|
// For providers referencing another provider, just use the rows it
|
|
// provides
|
|
allRows = dataSource?.value?.rows ?? []
|
|
} else {
|
|
// For other data sources like queries or views, fetch all rows from the
|
|
// server
|
|
allRows = await API.fetchDatasource(dataSource)
|
|
}
|
|
loading = false
|
|
loaded = true
|
|
}
|
|
|
|
const getSchema = async dataSource => {
|
|
if (dataSource?.schema) {
|
|
schema = dataSource.schema
|
|
} else if (dataSource?.tableId) {
|
|
const definition = await API.fetchTableDefinition(dataSource.tableId)
|
|
schema = definition?.schema ?? {}
|
|
} else if (dataSource?.type === "provider") {
|
|
schema = dataSource.value?.schema ?? {}
|
|
} else {
|
|
schema = {}
|
|
}
|
|
|
|
// Ensure there are "name" properties for all fields and that field schema
|
|
// are objects
|
|
let fixedSchema = {}
|
|
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
|
if (typeof fieldSchema === "string") {
|
|
fixedSchema[fieldName] = {
|
|
type: fieldSchema,
|
|
name: fieldName,
|
|
}
|
|
} else {
|
|
fixedSchema[fieldName] = {
|
|
...fieldSchema,
|
|
name: fieldName,
|
|
}
|
|
}
|
|
})
|
|
schema = fixedSchema
|
|
schemaLoaded = true
|
|
}
|
|
|
|
const nextPage = async () => {
|
|
if (!hasNextPage || !internalTable) {
|
|
return
|
|
}
|
|
const res = await API.searchTable({
|
|
tableId: dataSource?.tableId,
|
|
query,
|
|
bookmark: bookmarks[pageNumber + 1],
|
|
limit,
|
|
sort: sortColumn,
|
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
|
sortType,
|
|
paginate: true,
|
|
})
|
|
pageNumber++
|
|
allRows = res.rows
|
|
if (res.hasNextPage) {
|
|
bookmarks[pageNumber + 1] = res.bookmark
|
|
}
|
|
}
|
|
|
|
const prevPage = async () => {
|
|
if (!hasPrevPage || !internalTable) {
|
|
return
|
|
}
|
|
const res = await API.searchTable({
|
|
tableId: dataSource?.tableId,
|
|
query,
|
|
bookmark: bookmarks[pageNumber - 1],
|
|
limit,
|
|
sort: sortColumn,
|
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
|
sortType,
|
|
paginate: true,
|
|
})
|
|
pageNumber--
|
|
allRows = res.rows
|
|
}
|
|
</script>
|
|
|
|
<div use:styleable={$component.styles} class="container">
|
|
<Provider {actions} data={dataContext}>
|
|
{#if !loaded}
|
|
<div class="loading">
|
|
<ProgressCircle />
|
|
</div>
|
|
{:else}
|
|
{#if !$component.children}
|
|
<Placeholder />
|
|
{:else}
|
|
<slot />
|
|
{/if}
|
|
{#if paginate && internalTable}
|
|
<div class="pagination">
|
|
<Pagination
|
|
page={pageNumber + 1}
|
|
{hasPrevPage}
|
|
{hasNextPage}
|
|
goToPrevPage={prevPage}
|
|
goToNextPage={nextPage}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</Provider>
|
|
</div>
|
|
|
|
<style>
|
|
.container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
align-items: stretch;
|
|
}
|
|
.loading {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100px;
|
|
}
|
|
.pagination {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
</style>
|