diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte
index 11284b8917..bcd84e7112 100644
--- a/packages/bbui/src/Table/Table.svelte
+++ b/packages/bbui/src/Table/Table.svelte
@@ -27,6 +27,7 @@
export let selectedRows = []
export let editColumnTitle = "Edit"
export let customRenderers = []
+ export let disableSorting = false
const dispatch = createEventDispatcher()
@@ -63,7 +64,7 @@
)
// Reset state when data changes
- $: data.length, reset()
+ $: rows.length, reset()
const reset = () => {
nextScrollTop = 0
scrollTop = 0
@@ -107,7 +108,7 @@
}
const sortRows = (rows, sortColumn, sortOrder) => {
- if (!sortColumn || !sortOrder) {
+ if (!sortColumn || !sortOrder || disableSorting) {
return rows
}
return rows.slice().sort((a, b) => {
@@ -131,6 +132,7 @@
sortColumn = fieldSchema.name
sortOrder = "Descending"
}
+ dispatch("sort", { column: sortColumn, order: sortOrder })
}
const getDisplayName = schema => {
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index adc22e5daf..ff52c7d11a 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -24,7 +24,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
// need the client lucene builder to convert to the structure API expects
- import { buildLuceneQuery } from "../../../../../client/src/utils/lucene"
+ import { buildLuceneQuery } from "helpers/lucene"
export let block
export let webhookModal
diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte
index 3293c694b6..00b20d4a09 100644
--- a/packages/builder/src/components/backend/DataTable/DataTable.svelte
+++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte
@@ -1,6 +1,6 @@
-
- {#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}
+
+
+
+ {#key id}
+
+ {/key}
{/if}
-
-
-
- {/if}
-
+
+ {#key id}
+
+
+
+ {/key}
+
+
+
diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte
index fcb17a774d..78c3cc37f8 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/components/backend/DataTable/buttons/FilterButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/ViewFilterButton.svelte
similarity index 100%
rename from packages/builder/src/components/backend/DataTable/buttons/FilterButton.svelte
rename to packages/builder/src/components/backend/DataTable/buttons/ViewFilterButton.svelte
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index 1a7d75f28a..011c9bee43 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -10,6 +10,7 @@
ModalContent,
Context,
} from "@budibase/bbui"
+ import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp"
import { tables } from "stores/backend"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
@@ -30,8 +31,9 @@
const AUTO_TYPE = "auto"
const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type
- let fieldDefinitions = cloneDeep(FIELDS)
+ const dispatch = createEventDispatcher()
const { hide } = getContext(Context.Modal)
+ let fieldDefinitions = cloneDeep(FIELDS)
export let field = {
type: "string",
@@ -81,12 +83,13 @@
if (field.type === AUTO_TYPE) {
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
}
- tables.saveField({
+ await tables.saveField({
originalName,
field,
primaryDisplay,
indexes,
})
+ dispatch("updatecolumns")
}
function deleteColumn() {
@@ -99,6 +102,7 @@
hide()
deletion = false
}
+ dispatch("updatecolumns")
}
function handleTypeChange(event) {
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte
index ce52287c99..a60d9ecf31 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte
@@ -1,4 +1,5 @@
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte
index 32f369ce3d..f1de23fb97 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte
@@ -1,4 +1,5 @@
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
index 6ba8e4042f..3c6fa83c01 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
@@ -1,9 +1,9 @@
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
index 638fd44de6..9f0d5086f6 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
@@ -12,7 +12,7 @@
import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
- import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
+ import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
import { selectedComponent, store } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
index 4733fd4ffe..22363ff8f6 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
@@ -13,18 +13,20 @@
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { generate } from "shortid"
- import { getValidOperatorsForType, OperatorOptions } from "helpers/lucene"
+ import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
export let schemaFields
export let filters = []
export let bindings = []
export let panel = BindingPanel
+ export let allowBindings = true
const BannedTypes = ["link", "attachment", "formula"]
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
.map(field => field.name)
+ $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
const addFilter = () => {
filters = [
@@ -93,7 +95,7 @@
{#if !filters?.length}
- Add your first filter column.
+ Add your first filter expression.
{:else}
Results are filtered to only those which match all of the following
constraints.
@@ -117,7 +119,7 @@
/>
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte
index 58b767353b..2cb35a9cf5 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte
@@ -20,7 +20,6 @@
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
$: schemaFields = Object.values(schema || {})
- $: internalTable = dataSource?.type === "table"
const saveFilter = async () => {
dispatch("change", tempValue)
diff --git a/packages/builder/src/constants/lucene.js b/packages/builder/src/constants/lucene.js
new file mode 100644
index 0000000000..00da0c29bc
--- /dev/null
+++ b/packages/builder/src/constants/lucene.js
@@ -0,0 +1,97 @@
+/**
+ * Operator options for lucene queries
+ */
+export const OperatorOptions = {
+ Equals: {
+ value: "equal",
+ label: "Equals",
+ },
+ NotEquals: {
+ value: "notEqual",
+ label: "Not equals",
+ },
+ Empty: {
+ value: "empty",
+ label: "Is empty",
+ },
+ NotEmpty: {
+ value: "notEmpty",
+ label: "Is not empty",
+ },
+ StartsWith: {
+ value: "string",
+ label: "Starts with",
+ },
+ Like: {
+ value: "fuzzy",
+ label: "Like",
+ },
+ MoreThan: {
+ value: "rangeLow",
+ label: "More than",
+ },
+ LessThan: {
+ value: "rangeHigh",
+ label: "Less than",
+ },
+ Contains: {
+ value: "equal",
+ label: "Contains",
+ },
+ NotContains: {
+ value: "notEqual",
+ label: "Does Not Contain",
+ },
+}
+
+/**
+ * Returns the valid operator options for a certain data type
+ * @param type the data type
+ */
+export const getValidOperatorsForType = type => {
+ const Op = OperatorOptions
+ if (type === "string") {
+ return [
+ Op.Equals,
+ Op.NotEquals,
+ Op.StartsWith,
+ Op.Like,
+ Op.Empty,
+ Op.NotEmpty,
+ ]
+ } else if (type === "number") {
+ return [
+ Op.Equals,
+ Op.NotEquals,
+ Op.MoreThan,
+ Op.LessThan,
+ Op.Empty,
+ Op.NotEmpty,
+ ]
+ } else if (type === "options") {
+ return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
+ } else if (type === "array") {
+ return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
+ } else if (type === "boolean") {
+ return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
+ } else if (type === "longform") {
+ return [
+ Op.Equals,
+ Op.NotEquals,
+ Op.StartsWith,
+ Op.Like,
+ Op.Empty,
+ Op.NotEmpty,
+ ]
+ } else if (type === "datetime") {
+ return [
+ Op.Equals,
+ Op.NotEquals,
+ Op.MoreThan,
+ Op.LessThan,
+ Op.Empty,
+ Op.NotEmpty,
+ ]
+ }
+ return []
+}
diff --git a/packages/builder/src/helpers/fetchTableData.js b/packages/builder/src/helpers/fetchTableData.js
new file mode 100644
index 0000000000..3623426fbd
--- /dev/null
+++ b/packages/builder/src/helpers/fetchTableData.js
@@ -0,0 +1,206 @@
+// Do not use any aliased imports in common files, as these will be bundled
+// by multiple bundlers which may not be able to resolve them
+import { writable, derived, get } from "svelte/store"
+import * as API from "../builderStore/api"
+import { buildLuceneQuery } from "./lucene"
+
+const defaultOptions = {
+ tableId: null,
+ filters: 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
+ let lastBookmark
+
+ // 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 => {
+ lastBookmark = 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, filters } = 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(filters)
+
+ // 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 (state.loading || !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 (state.loading || !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()
+ }
+
+ // Loads the same page again
+ const refresh = async () => {
+ if (get(store).loading) {
+ return
+ }
+ const page = await fetchPage(lastBookmark)
+ store.update($store => ({ ...$store, rows: page.rows }))
+ }
+
+ // 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,
+ refresh,
+ }
+}
diff --git a/packages/builder/src/helpers/lucene.js b/packages/builder/src/helpers/lucene.js
index 18692359e4..03baa751cc 100644
--- a/packages/builder/src/helpers/lucene.js
+++ b/packages/builder/src/helpers/lucene.js
@@ -1,90 +1,179 @@
-export const OperatorOptions = {
- Equals: {
- value: "equal",
- label: "Equals",
- },
- NotEquals: {
- value: "notEqual",
- label: "Not equals",
- },
- Empty: {
- value: "empty",
- label: "Is empty",
- },
- NotEmpty: {
- value: "notEmpty",
- label: "Is not empty",
- },
- StartsWith: {
- value: "string",
- label: "Starts with",
- },
- Like: {
- value: "fuzzy",
- label: "Like",
- },
- MoreThan: {
- value: "rangeLow",
- label: "More than",
- },
- LessThan: {
- value: "rangeHigh",
- label: "Less than",
- },
- Contains: {
- value: "equal",
- label: "Contains",
- },
- NotContains: {
- value: "notEqual",
- label: "Does Not Contain",
- },
+/**
+ * Builds a lucene JSON query from the filter structure generated in the builder
+ * @param filter the builder filter structure
+ */
+export const buildLuceneQuery = filter => {
+ let query = {
+ string: {},
+ fuzzy: {},
+ range: {},
+ equal: {},
+ notEqual: {},
+ empty: {},
+ notEmpty: {},
+ contains: {},
+ notContains: {},
+ }
+ if (Array.isArray(filter)) {
+ filter.forEach(expression => {
+ let { operator, field, type, value } = expression
+ // Parse all values into correct types
+ if (type === "datetime" && value) {
+ value = new Date(value).toISOString()
+ }
+ if (type === "number") {
+ value = parseFloat(value)
+ }
+ if (type === "boolean") {
+ value = `${value}`?.toLowerCase() === "true"
+ }
+ if (operator.startsWith("range")) {
+ if (!query.range[field]) {
+ query.range[field] = {
+ low:
+ type === "number"
+ ? Number.MIN_SAFE_INTEGER
+ : "0000-00-00T00:00:00.000Z",
+ high:
+ type === "number"
+ ? Number.MAX_SAFE_INTEGER
+ : "9999-00-00T00:00:00.000Z",
+ }
+ }
+ if (operator === "rangeLow" && value != null && value !== "") {
+ query.range[field].low = value
+ } else if (operator === "rangeHigh" && value != null && value !== "") {
+ query.range[field].high = value
+ }
+ } else if (query[operator]) {
+ if (type === "boolean") {
+ // Transform boolean filters to cope with null.
+ // "equals false" needs to be "not equals true"
+ // "not equals false" needs to be "equals true"
+ if (operator === "equal" && value === false) {
+ query.notEqual[field] = true
+ } else if (operator === "notEqual" && value === false) {
+ query.equal[field] = true
+ } else {
+ query[operator][field] = value
+ }
+ } else {
+ query[operator][field] = value
+ }
+ }
+ })
+ }
+
+ return query
}
-export const getValidOperatorsForType = type => {
- const Op = OperatorOptions
- if (type === "string") {
- return [
- Op.Equals,
- Op.NotEquals,
- Op.StartsWith,
- Op.Like,
- Op.Empty,
- Op.NotEmpty,
- ]
- } else if (type === "number") {
- return [
- Op.Equals,
- Op.NotEquals,
- Op.MoreThan,
- Op.LessThan,
- Op.Empty,
- Op.NotEmpty,
- ]
- } else if (type === "options") {
- return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
- } else if (type === "array") {
- return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
- } else if (type === "boolean") {
- return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
- } else if (type === "longform") {
- return [
- Op.Equals,
- Op.NotEquals,
- Op.StartsWith,
- Op.Like,
- Op.Empty,
- Op.NotEmpty,
- ]
- } else if (type === "datetime") {
- return [
- Op.Equals,
- Op.NotEquals,
- Op.MoreThan,
- Op.LessThan,
- Op.Empty,
- Op.NotEmpty,
- ]
+/**
+ * Performs a client-side lucene search on an array of data
+ * @param docs the data
+ * @param query the JSON lucene query
+ */
+export const luceneQuery = (docs, query) => {
+ if (!query) {
+ return docs
}
- return []
+
+ // Iterates over a set of filters and evaluates a fail function against a doc
+ const match = (type, failFn) => doc => {
+ const filters = Object.entries(query[type] || {})
+ for (let i = 0; i < filters.length; i++) {
+ if (failFn(filters[i][0], filters[i][1], doc)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ // Process a string match (fails if the value does not start with the string)
+ const stringMatch = match("string", (key, value, doc) => {
+ return !doc[key] || !doc[key].startsWith(value)
+ })
+
+ // Process a fuzzy match (treat the same as starts with when running locally)
+ const fuzzyMatch = match("fuzzy", (key, value, doc) => {
+ return !doc[key] || !doc[key].startsWith(value)
+ })
+
+ // Process a range match
+ const rangeMatch = match("range", (key, value, doc) => {
+ return !doc[key] || doc[key] < value.low || doc[key] > value.high
+ })
+
+ // Process an equal match (fails if the value is different)
+ const equalMatch = match("equal", (key, value, doc) => {
+ return value != null && value !== "" && doc[key] !== value
+ })
+
+ // Process a not-equal match (fails if the value is the same)
+ const notEqualMatch = match("notEqual", (key, value, doc) => {
+ return value != null && value !== "" && doc[key] === value
+ })
+
+ // Process an empty match (fails if the value is not empty)
+ const emptyMatch = match("empty", (key, value, doc) => {
+ return doc[key] != null && doc[key] !== ""
+ })
+
+ // Process a not-empty match (fails is the value is empty)
+ const notEmptyMatch = match("notEmpty", (key, value, doc) => {
+ return doc[key] == null || doc[key] === ""
+ })
+
+ // Match a document against all criteria
+ const docMatch = doc => {
+ return (
+ stringMatch(doc) &&
+ fuzzyMatch(doc) &&
+ rangeMatch(doc) &&
+ equalMatch(doc) &&
+ notEqualMatch(doc) &&
+ emptyMatch(doc) &&
+ notEmptyMatch(doc)
+ )
+ }
+
+ // Process all docs
+ return docs.filter(docMatch)
+}
+
+/**
+ * Performs a client-side sort from the equivalent server-side lucene sort
+ * parameters.
+ * @param docs the data
+ * @param sort the sort column
+ * @param sortOrder the sort order ("ascending" or "descending")
+ * @param sortType the type of sort ("string" or "number")
+ */
+export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
+ if (!sort || !sortOrder || !sortType) {
+ return docs
+ }
+ const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
+ return docs.slice().sort((a, b) => {
+ const colA = parse(a[sort])
+ const colB = parse(b[sort])
+ if (sortOrder === "Descending") {
+ return colA > colB ? -1 : 1
+ } else {
+ return colA > colB ? 1 : -1
+ }
+ })
+}
+
+/**
+ * Limits the specified docs to the specified number of rows from the equivalent
+ * server-side lucene limit parameters.
+ * @param docs the data
+ * @param limit the number of docs to limit to
+ */
+export const luceneLimit = (docs, limit) => {
+ const numLimit = parseFloat(limit)
+ if (isNaN(numLimit)) {
+ return docs
+ }
+ return docs.slice(0, numLimit)
}
diff --git a/packages/builder/src/helpers/warnings.js b/packages/builder/src/helpers/warnings.js
new file mode 100644
index 0000000000..ad943a8578
--- /dev/null
+++ b/packages/builder/src/helpers/warnings.js
@@ -0,0 +1,16 @@
+export const suppressWarnings = warnings => {
+ if (!warnings?.length) {
+ return
+ }
+ const regex = new RegExp(warnings.map(x => `(${x})`).join("|"), "gi")
+ const warn = console.warn
+ console.warn = (...params) => {
+ const msg = params[0]
+ if (msg && typeof msg === "string") {
+ if (msg.match(regex)) {
+ return
+ }
+ }
+ warn(...params)
+ }
+}
diff --git a/packages/builder/src/main.js b/packages/builder/src/main.js
index f0fd0af178..bc5ec4f009 100644
--- a/packages/builder/src/main.js
+++ b/packages/builder/src/main.js
@@ -7,11 +7,19 @@ import "@spectrum-css/vars/dist/spectrum-light.css"
import "@spectrum-css/vars/dist/spectrum-lightest.css"
import "@spectrum-css/page/dist/index-vars.css"
import "./global.css"
-
+import { suppressWarnings } from "./helpers/warnings"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
+import App from "./App.svelte"
+
+// Init spectrum icons
loadSpectrumIcons()
-import App from "./App.svelte"
+// Suppress svelte runtime warnings
+suppressWarnings([
+ "was created with unknown prop",
+ "was created without expected prop",
+ "received an unexpected slot",
+])
export default new App({
target: document.getElementById("app"),
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte
index 4fa864ce7a..6d802df2e9 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte
@@ -1 +1,13 @@
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte
index 13f8719594..f48be08fd6 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte
@@ -2,7 +2,7 @@
import { params } from "@roxi/routify"
import { datasources } from "stores/backend"
- if ($params.selectedDatasource) {
+ if ($params.selectedDatasource && !$params.query) {
const datasource = $datasources.list.find(
m => m._id === $params.selectedDatasource
)
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte
index d05aa882ad..4fa864ce7a 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte
@@ -1,14 +1 @@
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/_layout.svelte
deleted file mode 100644
index 14f6303e5f..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/_layout.svelte
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/index.svelte
deleted file mode 100644
index a68c0dc651..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/index.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-{#if $database?._id && $tables?.selected?.name}
-
-{:else}Create your first table to start building{/if}
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte
deleted file mode 100644
index eddb5ab598..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/index.svelte
deleted file mode 100644
index 8e195ddb12..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/[selectedRow]/index.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/index.svelte
deleted file mode 100644
index 7d081b6976..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/[selectedTable]/relationship/index.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/_layout.svelte
deleted file mode 100644
index f957355c5c..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/_layout.svelte
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/index.svelte
deleted file mode 100644
index 6d61614145..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/table/index.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-{#if $tables.list.length === 0}
- Create your first table to start building
-{:else}Select a table to edit{/if}
-
-
diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js
index 2eeae29b9d..020a0c9420 100644
--- a/packages/builder/src/stores/backend/queries.js
+++ b/packages/builder/src/stores/backend/queries.js
@@ -1,5 +1,5 @@
import { writable, get } from "svelte/store"
-import { datasources, integrations, tables } from "./"
+import { datasources, integrations, tables, views } from "./"
import api from "builderStore/api"
export function createQueriesStore() {
@@ -55,10 +55,9 @@ export function createQueriesStore() {
},
select: query => {
update(state => ({ ...state, selected: query._id }))
- tables.update(state => ({
- ...state,
- selected: null,
- }))
+ views.unselect()
+ tables.unselect()
+ datasources.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js
index e0b614a63e..161877f660 100644
--- a/packages/builder/src/stores/backend/tables.js
+++ b/packages/builder/src/stores/backend/tables.js
@@ -95,7 +95,13 @@ export function createTablesStore() {
selected: {},
}))
},
- saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
+ saveField: async ({
+ originalName,
+ field,
+ primaryDisplay = false,
+ indexes,
+ }) => {
+ let promise
update(state => {
// delete the original if renaming
// need to handle if the column had no name, empty string
@@ -126,9 +132,12 @@ export function createTablesStore() {
...state.draft.schema,
[field.name]: cloneDeep(field),
}
- save(state.draft)
+ promise = save(state.draft)
return state
})
+ if (promise) {
+ await promise
+ }
},
deleteField: field => {
update(state => {
diff --git a/packages/builder/src/stores/backend/views.js b/packages/builder/src/stores/backend/views.js
index 0b15d18fa5..14c7bf92a4 100644
--- a/packages/builder/src/stores/backend/views.js
+++ b/packages/builder/src/stores/backend/views.js
@@ -16,6 +16,7 @@ export function createViewsStore() {
...state,
selected: view,
}))
+ tables.unselect()
queries.unselect()
datasources.unselect()
},
diff --git a/packages/client/rollup.config.js b/packages/client/rollup.config.js
index f404f93c4c..a814303069 100644
--- a/packages/client/rollup.config.js
+++ b/packages/client/rollup.config.js
@@ -58,6 +58,10 @@ export default {
find: "sdk",
replacement: path.resolve("./src/sdk"),
},
+ {
+ find: "builder",
+ replacement: path.resolve("../builder"),
+ },
],
}),
svelte({
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte
index 5890c0e2a9..c9c033caa3 100644
--- a/packages/client/src/components/ClientApp.svelte
+++ b/packages/client/src/components/ClientApp.svelte
@@ -23,7 +23,7 @@
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
import HoverIndicator from "components/preview/HoverIndicator.svelte"
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
- import ErrorSVG from "../../../builder/assets/error.svg"
+ import ErrorSVG from "builder/assets/error.svg"
// Provide contexts
setContext("sdk", SDK)
diff --git a/packages/client/src/components/app/DataProvider.svelte b/packages/client/src/components/app/DataProvider.svelte
index bdc9001445..991c41b77d 100644
--- a/packages/client/src/components/app/DataProvider.svelte
+++ b/packages/client/src/components/app/DataProvider.svelte
@@ -6,7 +6,7 @@
luceneQuery,
luceneSort,
luceneLimit,
- } from "utils/lucene"
+ } from "builder/src/helpers/lucene"
import Placeholder from "./Placeholder.svelte"
export let dataSource
diff --git a/packages/client/src/stores/state.js b/packages/client/src/stores/state.js
index cb20149de8..ce977c4333 100644
--- a/packages/client/src/stores/state.js
+++ b/packages/client/src/stores/state.js
@@ -1,5 +1,5 @@
import { writable, get, derived } from "svelte/store"
-import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
+import { localStorageStore } from "builder/src/builderStore/store/localStorage"
import { appStore } from "./app"
const createStateStore = () => {
diff --git a/packages/client/src/utils/conditions.js b/packages/client/src/utils/conditions.js
index 964a63d3fd..2791fa169e 100644
--- a/packages/client/src/utils/conditions.js
+++ b/packages/client/src/utils/conditions.js
@@ -1,4 +1,4 @@
-import { buildLuceneQuery, luceneQuery } from "./lucene"
+import { buildLuceneQuery, luceneQuery } from "builder/src/helpers/lucene"
export const getActiveConditions = conditions => {
if (!conditions?.length) {
diff --git a/packages/client/src/utils/lucene.js b/packages/client/src/utils/lucene.js
deleted file mode 100644
index 03baa751cc..0000000000
--- a/packages/client/src/utils/lucene.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * Builds a lucene JSON query from the filter structure generated in the builder
- * @param filter the builder filter structure
- */
-export const buildLuceneQuery = filter => {
- let query = {
- string: {},
- fuzzy: {},
- range: {},
- equal: {},
- notEqual: {},
- empty: {},
- notEmpty: {},
- contains: {},
- notContains: {},
- }
- if (Array.isArray(filter)) {
- filter.forEach(expression => {
- let { operator, field, type, value } = expression
- // Parse all values into correct types
- if (type === "datetime" && value) {
- value = new Date(value).toISOString()
- }
- if (type === "number") {
- value = parseFloat(value)
- }
- if (type === "boolean") {
- value = `${value}`?.toLowerCase() === "true"
- }
- if (operator.startsWith("range")) {
- if (!query.range[field]) {
- query.range[field] = {
- low:
- type === "number"
- ? Number.MIN_SAFE_INTEGER
- : "0000-00-00T00:00:00.000Z",
- high:
- type === "number"
- ? Number.MAX_SAFE_INTEGER
- : "9999-00-00T00:00:00.000Z",
- }
- }
- if (operator === "rangeLow" && value != null && value !== "") {
- query.range[field].low = value
- } else if (operator === "rangeHigh" && value != null && value !== "") {
- query.range[field].high = value
- }
- } else if (query[operator]) {
- if (type === "boolean") {
- // Transform boolean filters to cope with null.
- // "equals false" needs to be "not equals true"
- // "not equals false" needs to be "equals true"
- if (operator === "equal" && value === false) {
- query.notEqual[field] = true
- } else if (operator === "notEqual" && value === false) {
- query.equal[field] = true
- } else {
- query[operator][field] = value
- }
- } else {
- query[operator][field] = value
- }
- }
- })
- }
-
- return query
-}
-
-/**
- * Performs a client-side lucene search on an array of data
- * @param docs the data
- * @param query the JSON lucene query
- */
-export const luceneQuery = (docs, query) => {
- if (!query) {
- return docs
- }
-
- // Iterates over a set of filters and evaluates a fail function against a doc
- const match = (type, failFn) => doc => {
- const filters = Object.entries(query[type] || {})
- for (let i = 0; i < filters.length; i++) {
- if (failFn(filters[i][0], filters[i][1], doc)) {
- return false
- }
- }
- return true
- }
-
- // Process a string match (fails if the value does not start with the string)
- const stringMatch = match("string", (key, value, doc) => {
- return !doc[key] || !doc[key].startsWith(value)
- })
-
- // Process a fuzzy match (treat the same as starts with when running locally)
- const fuzzyMatch = match("fuzzy", (key, value, doc) => {
- return !doc[key] || !doc[key].startsWith(value)
- })
-
- // Process a range match
- const rangeMatch = match("range", (key, value, doc) => {
- return !doc[key] || doc[key] < value.low || doc[key] > value.high
- })
-
- // Process an equal match (fails if the value is different)
- const equalMatch = match("equal", (key, value, doc) => {
- return value != null && value !== "" && doc[key] !== value
- })
-
- // Process a not-equal match (fails if the value is the same)
- const notEqualMatch = match("notEqual", (key, value, doc) => {
- return value != null && value !== "" && doc[key] === value
- })
-
- // Process an empty match (fails if the value is not empty)
- const emptyMatch = match("empty", (key, value, doc) => {
- return doc[key] != null && doc[key] !== ""
- })
-
- // Process a not-empty match (fails is the value is empty)
- const notEmptyMatch = match("notEmpty", (key, value, doc) => {
- return doc[key] == null || doc[key] === ""
- })
-
- // Match a document against all criteria
- const docMatch = doc => {
- return (
- stringMatch(doc) &&
- fuzzyMatch(doc) &&
- rangeMatch(doc) &&
- equalMatch(doc) &&
- notEqualMatch(doc) &&
- emptyMatch(doc) &&
- notEmptyMatch(doc)
- )
- }
-
- // Process all docs
- return docs.filter(docMatch)
-}
-
-/**
- * Performs a client-side sort from the equivalent server-side lucene sort
- * parameters.
- * @param docs the data
- * @param sort the sort column
- * @param sortOrder the sort order ("ascending" or "descending")
- * @param sortType the type of sort ("string" or "number")
- */
-export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
- if (!sort || !sortOrder || !sortType) {
- return docs
- }
- const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
- return docs.slice().sort((a, b) => {
- const colA = parse(a[sort])
- const colB = parse(b[sort])
- if (sortOrder === "Descending") {
- return colA > colB ? -1 : 1
- } else {
- return colA > colB ? 1 : -1
- }
- })
-}
-
-/**
- * Limits the specified docs to the specified number of rows from the equivalent
- * server-side lucene limit parameters.
- * @param docs the data
- * @param limit the number of docs to limit to
- */
-export const luceneLimit = (docs, limit) => {
- const numLimit = parseFloat(limit)
- if (isNaN(numLimit)) {
- return docs
- }
- return docs.slice(0, numLimit)
-}
diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js
index d429c14cc7..02bfad85d2 100644
--- a/packages/server/src/api/controllers/row/internal.js
+++ b/packages/server/src/api/controllers/row/internal.js
@@ -351,6 +351,11 @@ exports.bulkDestroy = async ctx => {
}
exports.search = async ctx => {
+ // Fetch the whole table when running in cypress, as search doesn't work
+ if (env.isCypress()) {
+ return { rows: await exports.fetch(ctx) }
+ }
+
const appId = ctx.appId
const { tableId } = ctx.params
const db = new CouchDB(appId)
diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js
index 89e015b6f5..5012808950 100644
--- a/packages/server/src/environment.js
+++ b/packages/server/src/environment.js
@@ -13,6 +13,10 @@ function isDev() {
)
}
+function isCypress() {
+ return process.env.NODE_ENV === "cypress"
+}
+
let LOADED = false
if (!LOADED && isDev() && !isTest()) {
require("dotenv").config()
@@ -61,6 +65,7 @@ module.exports = {
module.exports[key] = value
},
isTest,
+ isCypress,
isDev,
isProd: () => {
return !isDev()