diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte
index 3293c694b6..88f754b254 100644
--- a/packages/builder/src/components/backend/DataTable/DataTable.svelte
+++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte
@@ -1,6 +1,5 @@
-
- {#if isInternal}
-
- {/if}
- {#if schema && Object.keys(schema).length > 0}
- {#if !isUsersTable}
-
- {/if}
+
+
{#if isInternal}
-
+
{/if}
-
- {#if isUsersTable}
-
+ {#if schema && Object.keys(schema).length > 0}
+ {#if !isUsersTable}
+
+ {/if}
+ {#if isInternal}
+
+ {/if}
+
+ {#if isUsersTable}
+
+ {/if}
+
+
+
{/if}
-
-
-
- {/if}
-
+
+
+
+
+
diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte
index fcb17a774d..38d580e273 100644
--- a/packages/builder/src/components/backend/DataTable/Table.svelte
+++ b/packages/builder/src/components/backend/DataTable/Table.svelte
@@ -1,8 +1,7 @@
-
-
- {#if title}
-
{title}
- {/if}
- {#if loading}
-
-
-
- {/if}
+
+
+
+ {#if title}
+
{title}
+ {/if}
+ {#if loading}
+
+
+
+ {/if}
+
+
+
+ {#if !isUsersTable && selectedRows.length > 0}
+
+ {/if}
+
-
-
- {#if !isUsersTable && selectedRows.length > 0}
-
- {/if}
-
-
-{#key tableId}
-
editColumn(e.detail)}
- on:editrow={e => editRow(e.detail)}
- on:clickrelationship={e => selectRelationship(e.detail)}
- />
-{/key}
+ {#key tableId}
+ editColumn(e.detail)}
+ on:editrow={e => editRow(e.detail)}
+ on:clickrelationship={e => selectRelationship(e.detail)}
+ on:sort
+ />
+ {/key}
+
diff --git a/packages/builder/src/helpers/fetchTableData.js b/packages/builder/src/helpers/fetchTableData.js
new file mode 100644
index 0000000000..ff7d61519f
--- /dev/null
+++ b/packages/builder/src/helpers/fetchTableData.js
@@ -0,0 +1,192 @@
+import { writable, derived, get } from "svelte/store"
+import * as API from "builderStore/api"
+import { buildLuceneQuery } from "../../../client/src/utils/lucene"
+
+const defaultOptions = {
+ tableId: null,
+ filter: null,
+ limit: 10,
+ sortColumn: null,
+ sortOrder: "ascending",
+ paginate: true,
+ schema: null,
+}
+
+export const fetchTableData = opts => {
+ // Save option set so we can override it later rather than relying on params
+ let options = {
+ ...defaultOptions,
+ ...opts,
+ }
+
+ // Local non-observable state
+ let query
+ let sortType
+
+ // Local observable state
+ const store = writable({
+ rows: [],
+ schema: null,
+ loading: false,
+ loaded: false,
+ bookmarks: [],
+ pageNumber: 0,
+ })
+
+ // Derive certain properties to return
+ const derivedStore = derived(store, $store => {
+ return {
+ ...$store,
+ hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null,
+ hasPrevPage: $store.pageNumber > 0,
+ }
+ })
+
+ const fetchPage = async bookmark => {
+ const { tableId, limit, sortColumn, sortOrder, paginate } = options
+ store.update($store => ({ ...$store, loading: true }))
+ const res = await API.post(`/api/${options.tableId}/search`, {
+ tableId,
+ query,
+ limit,
+ sort: sortColumn,
+ sortOrder: sortOrder?.toLowerCase() ?? "ascending",
+ sortType,
+ paginate,
+ bookmark,
+ })
+ store.update($store => ({ ...$store, loading: false, loaded: true }))
+ return await res.json()
+ }
+
+ // Fetches a fresh set of results from the server
+ const fetchData = async () => {
+ const { tableId, schema, sortColumn, filter } = options
+
+ // Ensure table ID exists
+ if (!tableId) {
+ return
+ }
+
+ // Get and enrich schema.
+ // Ensure there are "name" properties for all fields and that field schema
+ // are objects
+ let enrichedSchema = schema
+ if (!enrichedSchema) {
+ const definition = await API.get(`/api/tables/${tableId}`)
+ enrichedSchema = definition?.schema ?? null
+ }
+ if (enrichedSchema) {
+ Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
+ if (typeof fieldSchema === "string") {
+ enrichedSchema[fieldName] = {
+ type: fieldSchema,
+ name: fieldName,
+ }
+ } else {
+ enrichedSchema[fieldName] = {
+ ...fieldSchema,
+ name: fieldName,
+ }
+ }
+ })
+
+ // Save fixed schema so we can provide it later
+ options.schema = enrichedSchema
+ }
+
+ // Ensure schema exists
+ if (!schema) {
+ return
+ }
+ store.update($store => ({ ...$store, schema }))
+
+ // Work out what sort type to use
+ if (!sortColumn || !schema[sortColumn]) {
+ sortType = "string"
+ }
+ const type = schema?.[sortColumn]?.type
+ sortType = type === "number" ? "number" : "string"
+
+ // Build the lucene query
+ query = buildLuceneQuery(filter)
+
+ // Actually fetch data
+ const page = await fetchPage()
+ store.update($store => ({
+ ...$store,
+ loading: false,
+ loaded: true,
+ pageNumber: 0,
+ rows: page.rows,
+ bookmarks: page.hasNextPage ? [null, page.bookmark] : [null],
+ }))
+ }
+
+ // Fetches the next page of data
+ const nextPage = async () => {
+ const state = get(derivedStore)
+ if (!options.paginate || !state.hasNextPage) {
+ return
+ }
+
+ // Fetch next page
+ const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
+
+ // Update state
+ store.update($store => {
+ let { bookmarks, pageNumber } = $store
+ if (page.hasNextPage) {
+ bookmarks[pageNumber + 2] = page.bookmark
+ }
+ return {
+ ...$store,
+ pageNumber: pageNumber + 1,
+ rows: page.rows,
+ bookmarks,
+ }
+ })
+ }
+
+ // Fetches the previous page of data
+ const prevPage = async () => {
+ const state = get(derivedStore)
+ if (!options.paginate || !state.hasPrevPage) {
+ return
+ }
+
+ // Fetch previous page
+ const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
+
+ // Update state
+ store.update($store => {
+ return {
+ ...$store,
+ pageNumber: $store.pageNumber - 1,
+ rows: page.rows,
+ }
+ })
+ }
+
+ // Resets the data set and updates options
+ const update = async newOptions => {
+ if (newOptions) {
+ options = {
+ ...options,
+ ...newOptions,
+ }
+ }
+ await fetchData()
+ }
+
+ // Initially fetch data but don't bother waiting for the result
+ fetchData()
+
+ // Return our derived store which will be updated over time
+ return {
+ subscribe: derivedStore.subscribe,
+ nextPage,
+ prevPage,
+ update,
+ }
+}