From 35f9dcf3f6d1aa6e505e27380c0929162179e9f7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 29 Apr 2021 14:55:33 +0100 Subject: [PATCH 01/47] Expose search API endpoint --- .../PropertyControls/FilterEditor/FilterBuilder.svelte | 0 .../PropertyControls/{ => FilterEditor}/FilterEditor.svelte | 0 packages/server/src/api/routes/index.js | 2 ++ 3 files changed, 2 insertions(+) create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte rename packages/builder/src/components/design/PropertiesPanel/PropertyControls/{ => FilterEditor}/FilterEditor.svelte (100%) diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte similarity index 100% rename from packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor.svelte rename to packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index 0b09a78bb8..5ea3ddacef 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -23,6 +23,7 @@ const queryRoutes = require("./query") const hostingRoutes = require("./hosting") const backupRoutes = require("./backup") const devRoutes = require("./dev") +const searchRoutes = require("./search") exports.mainRoutes = [ authRoutes, @@ -51,6 +52,7 @@ exports.mainRoutes = [ // this could be breaking as koa may recognise other routes as this tableRoutes, rowRoutes, + searchRoutes, ] exports.staticRoutes = staticRoutes From 3a601d76ce23d77a37f77fb18bb982f39be15abc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 29 Apr 2021 14:55:51 +0100 Subject: [PATCH 02/47] Support NOT lucene queries and escape whitespace --- packages/server/src/api/controllers/search/utils.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index d3ffb26be7..e8e26c813b 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -34,6 +34,7 @@ class QueryBuilder { fuzzy: {}, range: {}, equal: {}, + notEqual: {}, ...base, } this.limit = 50 @@ -73,6 +74,11 @@ class QueryBuilder { return this } + addNotEqual(key, value) { + this.query.notEqual[key] = value + return this + } + addTable(tableId) { this.query.equal.tableId = tableId return this @@ -85,7 +91,7 @@ class QueryBuilder { if (output.length !== 0) { output += " AND " } - output += queryFn(key, value) + output += queryFn(key, value).replace(/ /, "\\ ") } } @@ -104,6 +110,9 @@ class QueryBuilder { if (this.query.equal) { build(this.query.equal, (key, value) => `${key}:${value}`) } + if (this.query.notEqual) { + build(this.query.notEqual, (key, value) => `!${key}:${value}`) + } if (rawQuery) { output = output.length === 0 ? rawQuery : `&${rawQuery}` } From 0c457e4eaf3a9bbb38f0bc8f63b2b359c01bf847 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 29 Apr 2021 15:24:59 +0100 Subject: [PATCH 03/47] Fix combobox disabled state and remove unnecessary props --- packages/bbui/src/Form/Checkbox.svelte | 2 +- packages/bbui/src/Form/Combobox.svelte | 4 ++-- packages/bbui/src/Form/Core/Combobox.svelte | 21 ++++++++++++++++----- packages/bbui/src/Form/DatePicker.svelte | 2 +- packages/bbui/src/Form/Dropzone.svelte | 2 +- packages/bbui/src/Form/Field.svelte | 1 - packages/bbui/src/Form/Input.svelte | 2 +- packages/bbui/src/Form/Multiselect.svelte | 2 +- packages/bbui/src/Form/RadioGroup.svelte | 2 +- packages/bbui/src/Form/Search.svelte | 2 +- packages/bbui/src/Form/Select.svelte | 2 +- packages/bbui/src/Form/TextArea.svelte | 2 +- packages/bbui/src/Form/Toggle.svelte | 2 +- 13 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/bbui/src/Form/Checkbox.svelte b/packages/bbui/src/Form/Checkbox.svelte index 1f3e439c2a..90a2cddda5 100644 --- a/packages/bbui/src/Form/Checkbox.svelte +++ b/packages/bbui/src/Form/Checkbox.svelte @@ -17,6 +17,6 @@ } - + diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 6b1e67a299..380465792b 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -8,7 +8,7 @@ export let disabled = false export let labelPosition = "above" export let error = null - export let placeholder = "Choose an option" + export let placeholder = "Choose an option or type" export let options = [] export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") @@ -26,7 +26,7 @@ } - + { // Always use placeholder if no value if (value == null || value === "") { - return placeholder || "Choose an option" + return placeholder || "Choose an option or type" } // Wait for options to load if there is a value but no options @@ -43,12 +43,19 @@ const onChange = e => { selectOption(e.target.value) } + + $: console.log(disabled) -
+
(focus = false)} on:change={onChange} {value} + {disabled} {placeholder} class="spectrum-Textfield-input spectrum-InputGroup-input" />
@@ -63,7 +71,7 @@ class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button" tabindex="-1" aria-haspopup="true" - disabled={!!error} + {disabled} on:click={() => (open = true)}> - + - + diff --git a/packages/bbui/src/Form/Input.svelte b/packages/bbui/src/Form/Input.svelte index e4a155d342..5059eb2543 100644 --- a/packages/bbui/src/Form/Input.svelte +++ b/packages/bbui/src/Form/Input.svelte @@ -19,7 +19,7 @@ } - + - + - + - + - +
onChange(event.detail)} {placeholder} /> -
- -
+ {#if !disabled} +
+ +
+ {/if}
diff --git a/packages/builder/src/components/common/ValuesList.svelte b/packages/builder/src/components/common/ValuesList.svelte index ff8cdb5479..eba81dac7c 100644 --- a/packages/builder/src/components/common/ValuesList.svelte +++ b/packages/builder/src/components/common/ValuesList.svelte @@ -7,7 +7,7 @@ const inputChanged = ev => { try { - values = ev.target.value.split("\n") + values = ev.detail.split("\n") } catch (_) { values = [] } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte new file mode 100644 index 0000000000..a96e1bd3d5 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte @@ -0,0 +1,38 @@ + + +
+ + x._instanceName} + getOptionValue={(x) => x._id} + /> +
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js index 4700ea5c8f..95c92a3f6d 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js @@ -6,6 +6,8 @@ import TriggerAutomation from "./TriggerAutomation.svelte" import ValidateForm from "./ValidateForm.svelte" import LogIn from "./LogIn.svelte" import LogOut from "./LogOut.svelte" +import NextPage from "./NextPage.svelte" +import PrevPage from "./PrevPage.svelte" // defines what actions are available, when adding a new one // the component is the setup panel for the action @@ -45,4 +47,12 @@ export default [ name: "Log Out", component: LogOut, }, + { + name: "Next Page", + component: NextPage, + }, + { + name: "Previous Page", + component: PrevPage, + }, ] diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte index e69de29bb2..a5c192cace 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte @@ -0,0 +1,217 @@ + + +{#if value?.length} +
+ {#each value as expression, idx} + onOperatorChange(expression, e.detail)} + placeholder={null} /> + {#if ['string', 'longform', 'number'].includes(expression.type)} + (expression.value = event.detail)} /> + {:else if expression.type === 'options'} + + {:else if expression.type === 'boolean'} + + {:else if expression.type === 'datetime'} + + {:else} + + {/if} + + removeField(expression.id)} /> + {/each} +
+{/if} +
+ +
+ + 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 06b4930d53..9336417b08 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte @@ -1,12 +1,19 @@ @@ -48,24 +51,7 @@ constaints. {/if} -
- -
+
- - diff --git a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte index af81ea3d24..59fb549d09 100644 --- a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte @@ -16,7 +16,7 @@ import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte" import SchemaSelect from "./PropertyControls/SchemaSelect.svelte" import EventsEditor from "./PropertyControls/EventsEditor" - import FilterEditor from "./PropertyControls/FilterEditor.svelte" + import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte" import { IconSelect } from "./PropertyControls/IconSelect" import ColorPicker from "./PropertyControls/ColorPicker.svelte" import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte" diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js index 248e1516c2..632f24e4a4 100644 --- a/packages/client/src/api/tables.js +++ b/packages/client/src/api/tables.js @@ -5,14 +5,14 @@ import { enrichRows } from "./rows" * Fetches a table definition. * Since definitions cannot change at runtime, the result is cached. */ -export const fetchTableDefinition = async tableId => { +export const fetchTableDefinition = async (tableId) => { return await API.get({ url: `/api/tables/${tableId}`, cache: true }) } /** * Fetches all rows from a table. */ -export const fetchTableData = async tableId => { +export const fetchTableData = async (tableId) => { const rows = await API.get({ url: `/api/${tableId}/rows` }) return await enrichRows(rows, tableId) } @@ -34,3 +34,35 @@ export const searchTableData = async ({ tableId, search, pagination }) => { output.rows = await enrichRows(output.rows, tableId) return output } + +/** + * Searches a table using Lucene. + */ +export const searchTable = async ({ + tableId, + query, + raw, + bookmark, + limit, + sort, + sortOrder, +}) => { + if (!tableId || (!query && !raw)) { + return + } + const res = await API.post({ + url: `/api/search/${tableId}/rows`, + body: { + query, + raw, + bookmark, + limit, + sort, + sortOrder, + }, + }) + return { + rows: await enrichRows(res?.rows, tableId), + bookmark: res.bookmark, + } +} diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js index 3aa302bec9..fc69ac212c 100644 --- a/packages/client/src/constants.js +++ b/packages/client/src/constants.js @@ -5,4 +5,6 @@ export const TableNames = { export const ActionTypes = { ValidateForm: "ValidateForm", RefreshDatasource: "RefreshDatasource", + NextPage: "NextPage", + PrevPage: "PrevPage", } diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 4d2865d586..f78ac1773c 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -16,27 +16,27 @@ const saveRowHandler = async (action, context) => { } } -const deleteRowHandler = async action => { +const deleteRowHandler = async (action) => { const { tableId, revId, rowId } = action.parameters if (tableId && revId && rowId) { await deleteRow({ tableId, rowId, revId }) } } -const triggerAutomationHandler = async action => { +const triggerAutomationHandler = async (action) => { const { fields } = action.parameters if (fields) { await triggerAutomation(action.parameters.automationId, fields) } } -const navigationHandler = action => { +const navigationHandler = (action) => { if (action.parameters.url) { routeStore.actions.navigate(action.parameters.url) } } -const queryExecutionHandler = async action => { +const queryExecutionHandler = async (action) => { const { datasourceId, queryId, queryParams } = action.parameters await executeQuery({ datasourceId, @@ -68,7 +68,23 @@ const refreshDatasourceHandler = async (action, context) => { ) } -const loginHandler = async action => { +const nextPageHandler = async (action, context) => { + return await executeActionHandler( + context, + action.parameters.componentId, + ActionTypes.NextPage + ) +} + +const prevPageHandler = async (action, context) => { + return await executeActionHandler( + context, + action.parameters.componentId, + ActionTypes.PrevPage + ) +} + +const loginHandler = async (action) => { const { email, password } = action.parameters await authStore.actions.logIn({ email, password }) } @@ -87,6 +103,8 @@ const handlerMap = { ["Refresh Datasource"]: refreshDatasourceHandler, ["Log In"]: loginHandler, ["Log Out"]: logoutHandler, + ["Next Page"]: nextPageHandler, + ["Previous Page"]: prevPageHandler, } /** @@ -96,9 +114,10 @@ const handlerMap = { export const enrichButtonActions = (actions, context) => { // Prevent button actions in the builder preview if (get(builderStore).inBuilder) { - return () => {} + // TODO uncomment + // return () => {} } - const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) + const handlers = actions.map((def) => handlerMap[def["##eventHandlerType"]]) return async () => { for (let i = 0; i < handlers.length; i++) { try { diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js index 234c7eb258..648a66c742 100644 --- a/packages/server/src/api/controllers/search/index.js +++ b/packages/server/src/api/controllers/search/index.js @@ -1,12 +1,14 @@ const { QueryBuilder, buildSearchUrl, search } = require("./utils") -exports.rowSearch = async ctx => { +exports.rowSearch = async (ctx) => { const appId = ctx.appId const { tableId } = ctx.params - const { bookmark, query, raw } = ctx.request.body + const { bookmark, query, raw, limit, sort, sortOrder } = ctx.request.body let url if (query) { - url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete() + url = new QueryBuilder(appId, query, bookmark, limit, sort, sortOrder) + .addTable(tableId) + .complete() } else if (raw) { url = buildSearchUrl({ appId, diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index e8e26c813b..d18f48533e 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -10,24 +10,43 @@ const fetch = require("node-fetch") * @param {string|null} bookmark If there were more than the limit specified can send the bookmark that was * returned with query for next set of search results. * @param {number} limit The number of entries to return per query. + * @param {string} sort The column to sort by. + * @param {string} sortOrder The order to sort by. "ascending" or "descending". * @param {boolean} excludeDocs By default full rows are returned, if required this can be disabled. * @return {string} The URL which a GET can be performed on to receive results. */ -function buildSearchUrl({ appId, query, bookmark, excludeDocs, limit = 50 }) { +function buildSearchUrl({ + appId, + query, + bookmark, + sort, + sortOrder, + excludeDocs, + limit = 50, +}) { let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search` url += `/${SearchIndexes.ROWS}?q=${query}` url += `&limit=${limit}` if (!excludeDocs) { url += "&include_docs=true" } + if (sort) { + const orderChar = sortOrder === "descending" ? "-" : "" + url += `&sort="${orderChar}${sort.replace(/ /, "_")}"` + } if (bookmark) { url += `&bookmark=${bookmark}` } + console.log(url) return checkSlashesInUrl(url) } +const luceneEscape = (value) => { + return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") +} + class QueryBuilder { - constructor(appId, base) { + constructor(appId, base, bookmark, limit, sort, sortOrder) { this.appId = appId this.query = { string: {}, @@ -35,10 +54,14 @@ class QueryBuilder { range: {}, equal: {}, notEqual: {}, + empty: {}, + notEmpty: {}, ...base, } - this.limit = 50 - this.bookmark = null + this.bookmark = bookmark + this.limit = limit || 50 + this.sort = sort + this.sortOrder = sortOrder || "ascending" } setLimit(limit) { @@ -79,39 +102,73 @@ class QueryBuilder { return this } + addEmpty(key, value) { + this.query.empty[key] = value + return this + } + + addNotEmpty(key, value) { + this.query.notEmpty[key] = value + return this + } + addTable(tableId) { this.query.equal.tableId = tableId return this } complete(rawQuery = null) { - let output = "" + let output = "*:*" function build(structure, queryFn) { for (let [key, value] of Object.entries(structure)) { - if (output.length !== 0) { - output += " AND " + const expression = queryFn(luceneEscape(key.replace(/ /, "_")), value) + if (expression == null) { + continue } - output += queryFn(key, value).replace(/ /, "\\ ") + output += ` AND ${expression}` } } if (this.query.string) { - build(this.query.string, (key, value) => `${key}:${value}*`) + build(this.query.string, (key, value) => { + return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null + }) } if (this.query.range) { - build( - this.query.range, - (key, value) => `${key}:[${value.low} TO ${value.high}]` - ) + build(this.query.range, (key, value) => { + if (!value) { + return null + } + if (isNaN(value.low) || value.low == null || value.low === "") { + return null + } + if (isNaN(value.high) || value.high == null || value.high === "") { + return null + } + console.log(value) + return `${key}:[${value.low} TO ${value.high}]` + }) } if (this.query.fuzzy) { - build(this.query.fuzzy, (key, value) => `${key}:${value}~`) + build(this.query.fuzzy, (key, value) => { + return value ? `${key}:${luceneEscape(value.toLowerCase())}~` : null + }) } if (this.query.equal) { - build(this.query.equal, (key, value) => `${key}:${value}`) + build(this.query.equal, (key, value) => { + return value ? `${key}:${luceneEscape(value.toLowerCase())}` : null + }) } if (this.query.notEqual) { - build(this.query.notEqual, (key, value) => `!${key}:${value}`) + build(this.query.notEqual, (key, value) => { + return value ? `!${key}:${luceneEscape(value.toLowerCase())}` : null + }) + } + if (this.query.empty) { + build(this.query.empty, (key) => `!${key}:["" TO *]`) + } + if (this.query.notEmpty) { + build(this.query.notEmpty, (key) => `${key}:["" TO *]`) } if (rawQuery) { output = output.length === 0 ? rawQuery : `&${rawQuery}` @@ -121,11 +178,13 @@ class QueryBuilder { query: output, bookmark: this.bookmark, limit: this.limit, + sort: this.sort, + sortOrder: this.sortOrder, }) } } -exports.search = async query => { +exports.search = async (query) => { const response = await fetch(query, { method: "GET", }) @@ -134,7 +193,7 @@ exports.search = async query => { rows: [], } if (json.rows != null && json.rows.length > 0) { - output.rows = json.rows.map(row => row.doc) + output.rows = json.rows.map((row) => row.doc) } if (json.bookmark) { output.bookmark = json.bookmark diff --git a/packages/server/src/db/views/staticViews.js b/packages/server/src/db/views/staticViews.js index 305d042217..62bf58a745 100644 --- a/packages/server/src/db/views/staticViews.js +++ b/packages/server/src/db/views/staticViews.js @@ -25,11 +25,11 @@ const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR * @returns {Promise} The view now exists, please note that the next view of this query will actually build it, * so it may be slow. */ -exports.createLinkView = async appId => { +exports.createLinkView = async (appId) => { const db = new CouchDB(appId) const designDoc = await db.get("_design/database") const view = { - map: function(doc) { + map: function (doc) { // everything in this must remain constant as its going to Pouch, no external variables if (doc.type === "link") { let doc1 = doc.doc1 @@ -57,7 +57,7 @@ exports.createLinkView = async appId => { await db.put(designDoc) } -exports.createRoutingView = async appId => { +exports.createRoutingView = async (appId) => { const db = new CouchDB(appId) const designDoc = await db.get("_design/database") const view = { @@ -84,23 +84,28 @@ async function searchIndex(appId, indexName, fnString) { designDoc.indexes = { [indexName]: { index: fnString, + analyzer: "keyword", }, } await db.put(designDoc) } -exports.createAllSearchIndex = async appId => { +exports.createAllSearchIndex = async (appId) => { await searchIndex( appId, SearchIndexes.ROWS, - function(doc) { + function (doc) { function idx(input, prev) { for (let key of Object.keys(input)) { - const idxKey = prev != null ? `${prev}.${key}` : key - if (key === "_id" || key === "_rev") { + let idxKey = prev != null ? `${prev}.${key}` : key + idxKey = idxKey.replace(/ /, "_") + if (key === "_id" || key === "_rev" || input[key] == null) { continue } - if (typeof input[key] !== "object") { + if (typeof input[key] === "string") { + // eslint-disable-next-line no-undef + index(idxKey, input[key].toLowerCase(), { store: true }) + } else if (typeof input[key] !== "object") { // eslint-disable-next-line no-undef index(idxKey, input[key], { store: true }) } else { diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 163caa3dbc..c20744a1a2 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1419,6 +1419,7 @@ "icon": "Data", "styleable": false, "hasChildren": true, + "actions": ["NextPage", "PrevPage"], "settings": [ { "type": "dataSource", @@ -1470,6 +1471,10 @@ { "label": "Loaded", "key": "loaded" + }, + { + "label": "Page Number", + "key": "pageNumber" } ] } diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 83de1ceb66..cedc153917 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -5,7 +5,7 @@ export let filter export let sortColumn export let sortOrder - export let limit + export let limit = 50 const { API, styleable, Provider, ActionTypes } = getContext("sdk") const component = getContext("component") @@ -16,13 +16,18 @@ // Loading flag for the initial load let loaded = false - let allRows = [] + // Provider state + let rows = [] let schema = {} + let bookmarks = [null] + let pageNumber = 0 - $: fetchData(dataSource) - $: filteredRows = filterRows(allRows, filter) - $: sortedRows = sortRows(filteredRows, sortColumn, sortOrder) - $: rows = limitRows(sortedRows, limit) + $: query = dataSource?.type === "table" ? buildLuceneQuery(filter) : null + $: hasNextPage = bookmarks[pageNumber + 1] != null + $: hasPrevPage = pageNumber > 0 + $: fetchData(dataSource, query, limit, sortColumn, sortOrder) + // $: sortedRows = sortRows(filteredRows, sortColumn, sortOrder) + // $: rows = limitRows(sortedRows, limit) $: getSchema(dataSource) $: actions = [ { @@ -30,6 +35,14 @@ callback: () => fetchData(dataSource), metadata: { dataSource }, }, + { + type: ActionTypes.NextPage, + callback: () => nextPage(), + }, + { + type: ActionTypes.PrevPage, + callback: () => prevPage(), + }, ] $: dataContext = { rows, @@ -37,23 +50,82 @@ rowsLength: rows.length, loading, loaded, + pageNumber: pageNumber + 1, + hasNextPage, + hasPrevPage, } - const fetchData = async dataSource => { + const buildLuceneQuery = (filter) => { + let query = { + string: {}, + fuzzy: {}, + range: {}, + equal: {}, + notEqual: {}, + empty: {}, + notEmpty: {}, + } + if (Array.isArray(filter)) { + filter.forEach((expression) => { + if (expression.operator.startsWith("range")) { + let range = { + low: Number.MIN_SAFE_INTEGER, + high: Number.MAX_SAFE_INTEGER, + } + if (expression.operator === "rangeLow") { + range.low = expression.value + } else if (expression.operator === "rangeHigh") { + range.high = expression.value + } + query.range[expression.field] = range + } else if (query[expression.operator]) { + query[expression.operator][expression.field] = expression.value + } + }) + } + return query + } + + const fetchData = async (dataSource, query, limit, sortColumn, sortOrder) => { loading = true - allRows = await API.fetchDatasource(dataSource) + if (dataSource?.type === "table") { + const res = await API.searchTable({ + tableId: dataSource.tableId, + query, + limit, + sort: sortColumn, + sortOrder: sortOrder?.toLowerCase() ?? "ascending", + }) + pageNumber = 0 + rows = res.rows + + // Check we have next data + const next = await API.searchTable({ + tableId: dataSource.tableId, + query, + limit: 1, + bookmark: res.bookmark, + sort: sortColumn, + sortOrder: sortOrder?.toLowerCase() ?? "ascending", + }) + if (next.rows?.length) { + bookmarks = [null, res.bookmark] + } else { + bookmarks = [null] + } + } else { + const rows = await API.fetchDatasource(dataSource) + rows = inMemoryFilterRows(rows, filter) + } loading = false loaded = true } - const filterRows = (rows, filter) => { - if (!Object.keys(filter || {}).length) { - return rows - } + const inMemoryFilterRows = (rows, filter) => { let filteredData = [...rows] Object.entries(filter).forEach(([field, value]) => { if (value != null && value !== "") { - filteredData = filteredData.filter(row => { + filteredData = filteredData.filter((row) => { return row[field] === value }) } @@ -84,7 +156,7 @@ return rows.slice(0, numLimit) } - const getSchema = async dataSource => { + const getSchema = async (dataSource) => { if (dataSource?.schema) { schema = dataSource.schema } else if (dataSource?.tableId) { @@ -101,6 +173,51 @@ } }) } + + const nextPage = async () => { + if (!hasNextPage) { + return + } + const res = await API.searchTable({ + tableId: dataSource?.tableId, + query, + bookmark: bookmarks[pageNumber + 1], + limit, + sort: sortColumn, + sortOrder: sortOrder?.toLowerCase() ?? "ascending", + }) + pageNumber++ + rows = res.rows + + // Check we have next data + const next = await API.searchTable({ + tableId: dataSource.tableId, + query, + limit: 1, + bookmark: res.bookmark, + sort: sortColumn, + sortOrder: sortOrder?.toLowerCase() ?? "ascending", + }) + if (next.rows?.length) { + bookmarks[pageNumber + 1] = res.bookmark + } + } + + const prevPage = async () => { + if (!hasPrevPage) { + return + } + const res = await API.searchTable({ + tableId: dataSource?.tableId, + query, + bookmark: bookmarks[pageNumber - 1], + limit, + sort: sortColumn, + sortOrder: sortOrder?.toLowerCase() ?? "ascending", + }) + pageNumber-- + rows = res.rows + }
From 1a2e17ff17af6fa154ff238daefd6dd6c37dae40 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 11 May 2021 11:24:16 +0100 Subject: [PATCH 05/47] Add support for numerical sorting --- .../popovers/EditTablePopover.svelte | 2 - .../FilterEditor/FilterBuilder.svelte | 36 +++++++----- .../FilterEditor/FilterEditor.svelte | 1 - packages/client/src/api/tables.js | 6 +- .../src/api/controllers/search/index.js | 22 +++++++- .../src/api/controllers/search/utils.js | 20 ++++--- .../src/DataProvider.svelte | 55 ++++++++++++++----- yarn.lock | 2 +- 8 files changed, 101 insertions(+), 43 deletions(-) diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte index 2d419f18aa..6cae231be9 100644 --- a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte +++ b/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte @@ -40,13 +40,11 @@ if (wasSelectedTable._id === table._id) { $goto("./table") } - editorModal.hide() } async function save() { await tables.save(table) notifications.success("Table renamed successfully") - editorModal.hide() } function checkValid(evt) { diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte index a5c192cace..7afbaa5b7d 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte @@ -9,7 +9,7 @@ import { store, currentAsset } from "builderStore" import { getBindableProperties } from "builderStore/dataBinding" import { createEventDispatcher } from "svelte" - import DrawerBindableInput from "components/common/DrawerBindableInput.svelte" + import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import { generate } from "shortid" const dispatch = createEventDispatcher() @@ -159,35 +159,44 @@ bind:value={expression.field} options={fieldOptions} on:change={e => onFieldChange(expression, e.detail)} - placeholder="Column" /> + placeholder="Column" + /> + {#if expression.valueType === "Binding"} (expression.value = event.detail)} /> + {:else if ["string", "longform", "number"].includes(expression.type)} + {:else if expression.type === "options"} diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index 7827850428..3310f89bf7 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -39,7 +39,6 @@ function buildSearchUrl({ if (bookmark) { url += `&bookmark=${bookmark}` } - console.log(url) return checkSlashesInUrl(url) } @@ -142,13 +141,12 @@ class QueryBuilder { if (!value) { return null } - if (isNaN(value.low) || value.low == null || value.low === "") { + if (value.low == null || value.low === "") { return null } - if (isNaN(value.high) || value.high == null || value.high === "") { + if (value.high == null || value.high === "") { return null } - console.log(value) return `${key}:[${value.low} TO ${value.high}]` }) } diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 38dd9f3955..512a4c594d 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -83,20 +83,21 @@ notEmpty: {}, } if (Array.isArray(filter)) { - filter.forEach(expression => { - if (expression.operator.startsWith("range")) { - let range = { - low: Number.MIN_SAFE_INTEGER, - high: Number.MAX_SAFE_INTEGER, + filter.forEach(({ operator, field, type, value }) => { + if (operator.startsWith("range")) { + if (!query.range[field]) { + query.range[field] = { + low: type === "number" ? Number.MIN_SAFE_INTEGER : "0000", + high: type === "number" ? Number.MAX_SAFE_INTEGER : "9999", + } } - if (expression.operator === "rangeLow") { - range.low = expression.value - } else if (expression.operator === "rangeHigh") { - range.high = expression.value + if (operator === "rangeLow") { + query.range[field].low = value + } else if (operator === "rangeHigh") { + query.range[field].high = value } - query.range[expression.field] = range - } else if (query[expression.operator]) { - query[expression.operator][expression.field] = expression.value + } else if (query[operator]) { + query[operator][field] = value } }) } From 889f5f817579360f3fae238634ce534295e878cb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 11 May 2021 19:07:52 +0100 Subject: [PATCH 08/47] Add legacy in-memory filtering and sorting for views and external datasources --- .../EventsEditor/actions/SaveFields.svelte | 2 +- .../FilterEditor/FilterEditor.svelte | 54 +++++++++++++++---- ...lder.svelte => LuceneFilterBuilder.svelte} | 0 .../src/DataProvider.svelte | 10 ++-- 4 files changed, 50 insertions(+), 16 deletions(-) rename packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/{FilterBuilder.svelte => LuceneFilterBuilder.svelte} (100%) diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte index f6bd54ffb6..f4a4bea334 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte @@ -80,7 +80,7 @@ /> {/each}
- 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 d90097939a..1662337409 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte @@ -12,22 +12,35 @@ getDatasourceForProvider, getSchemaForDatasource, } from "builderStore/dataBinding" - import FilterBuilder from "./FilterBuilder.svelte" + import LuceneFilterBuilder from "./LuceneFilterBuilder.svelte" import { currentAsset } from "builderStore" + import SaveFields from "../EventsEditor/actions/SaveFields.svelte" const dispatch = createEventDispatcher() - export let value = {} + export let value = [] export let componentInstance let drawer - let tempValue = Array.isArray(value) ? value : [] + let tempValue = value - $: schemaFields = getSchemaFields(componentInstance) + $: numFilters = Array.isArray(tempValue) + ? tempValue.length + : Object.keys(tempValue || {}).length + $: dataSource = getDatasourceForProvider($currentAsset, componentInstance) + $: schema = getSchemaForDatasource(dataSource)?.schema + $: schemaFields = Object.values(schema || {}) + $: internalTable = dataSource?.type === "table" - const getSchemaFields = component => { - const datasource = getDatasourceForProvider($currentAsset, component) - const { schema } = getSchemaForDatasource(datasource) - return Object.values(schema || {}) + // Reset value if value is wrong type for the datasource. + // Lucene editor needs an array, and simple editor needs an object. + $: { + if (internalTable && !Array.isArray(value)) { + tempValue = [] + dispatch("change", []) + } else if (!internalTable && Array.isArray(value)) { + tempValue = {} + dispatch("change", {}) + } } const saveFilter = async () => { @@ -43,14 +56,35 @@ - {#if !Object.keys(tempValue || {}).length} + {#if !numFilters} Add your first filter column. {:else} Results are filtered to only those which match all of the following constaints. {/if} - + {#if internalTable} + + {:else} +
+ (tempValue = e.detail)} + /> +
+ {/if}
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/LuceneFilterBuilder.svelte similarity index 100% rename from packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterBuilder.svelte rename to packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/LuceneFilterBuilder.svelte diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 512a4c594d..418d80f7a9 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -24,7 +24,7 @@ let pageNumber = 0 $: internalTable = dataSource?.type === "table" - $: query = dataSource?.type === "table" ? buildLuceneQuery(filter) : null + $: query = internalTable ? buildLuceneQuery(filter) : null $: hasNextPage = bookmarks[pageNumber + 1] != null $: hasPrevPage = pageNumber > 0 $: getSchema(dataSource) @@ -34,8 +34,8 @@ if (internalTable) { rows = allRows } else { - rows = sortRows(allRows, sortColumn, sortOrder) - rows = limitRows(rows, limit) + const sortedRows = sortRows(allRows, sortColumn, sortOrder) + rows = limitRows(sortedRows, limit) } } $: actions = [ @@ -213,7 +213,7 @@ } const nextPage = async () => { - if (!hasNextPage) { + if (!hasNextPage || !internalTable) { return } const res = await API.searchTable({ @@ -244,7 +244,7 @@ } const prevPage = async () => { - if (!hasPrevPage) { + if (!hasPrevPage || !internalTable) { return } const res = await API.searchTable({ From c2cc99ef2c3e92e5c2610ad958e1947149b9b69b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 12 May 2021 14:34:25 +0100 Subject: [PATCH 09/47] Fix chart placeholder, fix spectrum colours and begin work on pagination component --- packages/bbui/src/Table/Table.svelte | 8 ++---- .../DataProviderSelect.svelte | 16 +++++++++++- packages/builder/src/global.css | 4 +-- packages/standard-components/manifest.json | 13 ++++++++++ .../standard-components/src/Pagination.svelte | 25 +++++++++++++++++++ .../src/charts/ApexChart.svelte | 12 ++++++--- 6 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 packages/standard-components/src/Pagination.svelte diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 048ded2b5b..56f1ac96a8 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -345,7 +345,7 @@ From e4aaf6979297d619d0327ee52efd9867c342987a Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 12 May 2021 14:44:47 +0100 Subject: [PATCH 10/47] Updating search endpoint to have egress processing. --- packages/server/src/api/controllers/search/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js index 2a309cc7e6..31c81fec9a 100644 --- a/packages/server/src/api/controllers/search/index.js +++ b/packages/server/src/api/controllers/search/index.js @@ -1,4 +1,6 @@ const { QueryBuilder, buildSearchUrl, search } = require("./utils") +const CouchDB = require("../../../db") +const { outputProcessing } = require("../../../utilities/rowProcessor") exports.rowSearch = async ctx => { const appId = ctx.appId @@ -12,6 +14,8 @@ exports.rowSearch = async ctx => { sortOrder, sortType, } = ctx.request.body + const db = new CouchDB(appId) + let url if (query) { url = new QueryBuilder( @@ -32,5 +36,10 @@ exports.rowSearch = async ctx => { bookmark, }) } - ctx.body = await search(url) + const response = await search(url) + const table = await db.get(tableId) + ctx.body = { + rows: await outputProcessing(appId, table, response.rows), + bookmark: response.bookmark, + } } From 34a2382b88f40c059dfe821aad882ec01717b1ef Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 12 May 2021 14:52:43 +0100 Subject: [PATCH 11/47] Fix two-tone spectrum background colours --- packages/bbui/src/Table/Table.svelte | 4 ++-- packages/builder/src/global.css | 6 +----- packages/standard-components/src/forms/Form.svelte | 1 + 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 56f1ac96a8..4278751c5f 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -345,7 +345,7 @@ From 10681d1a44477cb11afd0a84de405615ad8b8852 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:23:25 +0100 Subject: [PATCH 12/47] Add spectrum pagination component to BBUI --- packages/bbui/package.json | 1 + .../bbui/src/Pagination/Pagination.svelte | 54 +++++++++++++++++++ packages/bbui/src/index.js | 1 + packages/bbui/yarn.lock | 5 ++ 4 files changed, 61 insertions(+) create mode 100644 packages/bbui/src/Pagination/Pagination.svelte diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 056f4229cb..c4c7e14b4b 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -56,6 +56,7 @@ "@spectrum-css/link": "^3.1.1", "@spectrum-css/menu": "^3.0.1", "@spectrum-css/modal": "^3.0.1", + "@spectrum-css/pagination": "^3.0.3", "@spectrum-css/picker": "^1.0.1", "@spectrum-css/popover": "^3.0.1", "@spectrum-css/progressbar": "^1.0.2", diff --git a/packages/bbui/src/Pagination/Pagination.svelte b/packages/bbui/src/Pagination/Pagination.svelte new file mode 100644 index 0000000000..cfe989ea14 --- /dev/null +++ b/packages/bbui/src/Pagination/Pagination.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index 9d47958461..8ed1dadf1f 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -51,6 +51,7 @@ export { default as TreeView } from "./TreeView/Tree.svelte" export { default as TreeItem } from "./TreeView/Item.svelte" export { default as Divider } from "./Divider/Divider.svelte" export { default as Search } from "./Form/Search.svelte" +export { default as Pagination } from "./Pagination/Pagination.svelte" // Typography export { default as Body } from "./Typography/Body.svelte" diff --git a/packages/bbui/yarn.lock b/packages/bbui/yarn.lock index e9bad2e162..36e26b0ea6 100644 --- a/packages/bbui/yarn.lock +++ b/packages/bbui/yarn.lock @@ -161,6 +161,11 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/modal/-/modal-3.0.2.tgz#58b6621cab65f90788d310374f40df1f7090473f" integrity sha512-YnIivJhoaao7Otu+HV7sgebPyFbO6sd/oMvTN/Rb2wwgnaMnIIuIRdGandSrcgotN2uNgs+P0knG6mv/xA1/dg== +"@spectrum-css/pagination@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@spectrum-css/pagination/-/pagination-3.0.3.tgz#b204c3ada384c4af751a354bc428346d82eeea65" + integrity sha512-OJ/v9GeNXJOZ9Yr9LDBYPrR2NCiLOWP9wANT/a5sqFuugRnQbn/HYMnRp9TBxwpDY6ihaPo0T/wi7kLiAJFdDw== + "@spectrum-css/picker@^1.0.1": version "1.0.2" resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.2.tgz#b49429ae3c89f9c5f2c0530787ce45392c9612ff" From e57a14a8dd680356148aaa450e04d5be5540ac6f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:23:48 +0100 Subject: [PATCH 13/47] Fix bug preventing progress circle component from animating --- packages/bbui/src/ProgressCircle/ProgressCircle.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte index 711517ec7b..9c8181ec7c 100644 --- a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte +++ b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte @@ -42,7 +42,7 @@
From e09440f077a07858c42c6c2f7e930be7aaca623c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:26:18 +0100 Subject: [PATCH 14/47] Allow multipage searches and implement optional pagination to data providers --- packages/client/src/api/tables.js | 22 +- packages/server/src/api/controllers/row.js | 40 ---- .../src/api/controllers/search/index.js | 47 ++-- .../src/api/controllers/search/utils.js | 206 ++++++++++++------ packages/server/src/api/routes/row.js | 6 - packages/standard-components/manifest.json | 52 +---- .../src/DataProvider.svelte | 91 +++++--- .../standard-components/src/Search.svelte | 195 ----------------- 8 files changed, 241 insertions(+), 418 deletions(-) delete mode 100644 packages/standard-components/src/Search.svelte diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js index 693ce8f013..a75a2d368b 100644 --- a/packages/client/src/api/tables.js +++ b/packages/client/src/api/tables.js @@ -17,24 +17,6 @@ export const fetchTableData = async tableId => { return await enrichRows(rows, tableId) } -/** - * Perform a mango query against an internal table - * @param {String} tableId - id of the table to search - * @param {Object} search - Mango Compliant search object - * @param {Object} pagination - the pagination controls - */ -export const searchTableData = async ({ tableId, search, pagination }) => { - const output = await API.post({ - url: `/api/${tableId}/rows/search`, - body: { - query: search, - pagination, - }, - }) - output.rows = await enrichRows(output.rows, tableId) - return output -} - /** * Searches a table using Lucene. */ @@ -47,6 +29,7 @@ export const searchTable = async ({ sort, sortOrder, sortType, + paginate, }) => { if (!tableId || (!query && !raw)) { return @@ -61,10 +44,11 @@ export const searchTable = async ({ sort, sortOrder, sortType, + paginate, }, }) return { + ...res, rows: await enrichRows(res?.rows, tableId), - bookmark: res.bookmark, } } diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 6d0cc08548..1d73abea47 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -16,7 +16,6 @@ const { const { FieldTypes } = require("../../constants") const { isEqual } = require("lodash") const { cloneDeep } = require("lodash/fp") -const { QueryBuilder, search } = require("./search/utils") const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}` @@ -248,45 +247,6 @@ exports.fetchView = async function (ctx) { } } -exports.search = async function (ctx) { - const appId = ctx.appId - const db = new CouchDB(appId) - const { - query, - pagination: { pageSize = 10, bookmark }, - } = ctx.request.body - const tableId = ctx.params.tableId - - const queryBuilder = new QueryBuilder(appId) - .setLimit(pageSize) - .addTable(tableId) - if (bookmark) { - queryBuilder.setBookmark(bookmark) - } - - let searchString - if (ctx.query && ctx.query.raw && ctx.query.raw !== "") { - searchString = queryBuilder.complete(query["RAW"]) - } else { - // make all strings a starts with operation rather than pure equality - for (const [key, queryVal] of Object.entries(query)) { - if (typeof queryVal === "string") { - queryBuilder.addString(key, queryVal) - } else { - queryBuilder.addEqual(key, queryVal) - } - } - searchString = queryBuilder.complete() - } - - const response = await search(searchString) - const table = await db.get(tableId) - ctx.body = { - rows: await outputProcessing(appId, table, response.rows), - bookmark: response.bookmark, - } -} - exports.fetchTableRows = async function (ctx) { const appId = ctx.appId const db = new CouchDB(appId) diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js index 31c81fec9a..b8411e9d3a 100644 --- a/packages/server/src/api/controllers/search/index.js +++ b/packages/server/src/api/controllers/search/index.js @@ -1,4 +1,4 @@ -const { QueryBuilder, buildSearchUrl, search } = require("./utils") +const { fullSearch, paginatedSearch } = require("./utils") const CouchDB = require("../../../db") const { outputProcessing } = require("../../../utilities/rowProcessor") @@ -8,38 +8,45 @@ exports.rowSearch = async ctx => { const { bookmark, query, - raw, limit, sort, sortOrder, sortType, + paginate, } = ctx.request.body const db = new CouchDB(appId) - let url - if (query) { - url = new QueryBuilder( + let response + const start = Date.now() + if (paginate) { + response = await paginatedSearch( appId, query, - bookmark, - limit, + tableId, sort, sortOrder, - sortType + sortType, + limit, + bookmark ) - .addTable(tableId) - .complete() - } else if (raw) { - url = buildSearchUrl({ + } else { + response = await fullSearch( appId, - query: raw, - bookmark, - }) + query, + tableId, + sort, + sortOrder, + sortType, + limit + ) } - const response = await search(url) - const table = await db.get(tableId) - ctx.body = { - rows: await outputProcessing(appId, table, response.rows), - bookmark: response.bookmark, + const end = Date.now() + console.log("Time: " + (end - start) / 1000 + " ms") + + if (response.rows && response.rows.length) { + const table = await db.get(tableId) + response.rows = await outputProcessing(appId, table, response.rows) } + + ctx.body = response } diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index 3310f89bf7..6f2f29628a 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -3,51 +3,12 @@ const { checkSlashesInUrl } = require("../../../utilities") const env = require("../../../environment") const fetch = require("node-fetch") -/** - * Given a set of inputs this will generate the URL which is to be sent to the search proxy in CouchDB. - * @param {string} appId The ID of the app which we will be searching within. - * @param {string} query The lucene query string which is to be used for searching. - * @param {string|null} bookmark If there were more than the limit specified can send the bookmark that was - * returned with query for next set of search results. - * @param {number} limit The number of entries to return per query. - * @param {string} sort The column to sort by. - * @param {string} sortOrder The order to sort by. "ascending" or "descending". - * @param {string} sortType The type of sort to perform. "string" or "number". - * @param {boolean} excludeDocs By default full rows are returned, if required this can be disabled. - * @return {string} The URL which a GET can be performed on to receive results. - */ -function buildSearchUrl({ - appId, - query, - bookmark, - sort, - sortOrder, - sortType, - excludeDocs, - limit = 50, -}) { - let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search` - url += `/${SearchIndexes.ROWS}?q=${query}` - url += `&limit=${Math.min(limit, 200)}` - if (!excludeDocs) { - url += "&include_docs=true" - } - if (sort) { - const orderChar = sortOrder === "descending" ? "-" : "" - url += `&sort="${orderChar}${sort.replace(/ /, "_")}<${sortType}>"` - } - if (bookmark) { - url += `&bookmark=${bookmark}` - } - return checkSlashesInUrl(url) -} - const luceneEscape = value => { return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") } class QueryBuilder { - constructor(appId, base, bookmark, limit, sort, sortOrder, sortType) { + constructor(appId, base) { this.appId = appId this.query = { string: {}, @@ -59,11 +20,14 @@ class QueryBuilder { notEmpty: {}, ...base, } - this.bookmark = bookmark - this.limit = limit || 50 - this.sort = sort - this.sortOrder = sortOrder || "ascending" - this.sortType = sortType || "string" + this.limit = 50 + this.sortOrder = "ascending" + this.sortType = "string" + } + + setTable(tableId) { + this.query.equal.tableId = tableId + return this } setLimit(limit) { @@ -71,6 +35,21 @@ class QueryBuilder { return this } + setSort(sort) { + this.sort = sort + return this + } + + setSortOrder(sortOrder) { + this.sortOrder = sortOrder + return this + } + + setSortType(sortType) { + this.sortType = sortType + return this + } + setBookmark(bookmark) { this.bookmark = bookmark return this @@ -114,12 +93,7 @@ class QueryBuilder { return this } - addTable(tableId) { - this.query.equal.tableId = tableId - return this - } - - complete(rawQuery = null) { + buildSearchURL(excludeDocs = false) { let output = "*:*" function build(structure, queryFn) { for (let [key, value] of Object.entries(structure)) { @@ -171,22 +145,28 @@ class QueryBuilder { if (this.query.notEmpty) { build(this.query.notEmpty, key => `${key}:["" TO *]`) } - if (rawQuery) { - output = output.length === 0 ? rawQuery : `&${rawQuery}` + + let url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search` + url += `/${SearchIndexes.ROWS}?q=${output}` + url += `&limit=${Math.min(this.limit, 200)}` + if (!excludeDocs) { + url += "&include_docs=true" } - return buildSearchUrl({ - appId: this.appId, - query: output, - bookmark: this.bookmark, - limit: this.limit, - sort: this.sort, - sortOrder: this.sortOrder, - sortType: this.sortType, - }) + if (this.sort) { + const orderChar = this.sortOrder === "descending" ? "-" : "" + url += `&sort="${orderChar}${this.sort.replace(/ /, "_")}<${ + this.sortType + }>"` + } + if (this.bookmark) { + url += `&bookmark=${this.bookmark}` + } + console.log(url) + return checkSlashesInUrl(url) } } -exports.search = async query => { +const runQuery = async query => { const response = await fetch(query, { method: "GET", }) @@ -203,5 +183,101 @@ exports.search = async query => { return output } -exports.QueryBuilder = QueryBuilder -exports.buildSearchUrl = buildSearchUrl +const recursiveSearch = async ( + appId, + query, + tableId, + sort, + sortOrder, + sortType, + limit, + bookmark, + rows +) => { + if (rows.length >= limit) { + return rows + } + const pageSize = rows.length > limit - 200 ? limit - rows.length : 200 + const url = new QueryBuilder(appId, query) + .setTable(tableId) + .setBookmark(bookmark) + .setLimit(pageSize) + .setSort(sort) + .setSortOrder(sortOrder) + .setSortType(sortType) + .buildSearchURL() + const page = await runQuery(url) + if (!page.rows.length) { + return rows + } + if (page.rows.length < 200) { + return [...rows, ...page.rows] + } + return await recursiveSearch( + appId, + query, + tableId, + sort, + sortOrder, + sortType, + limit, + page.bookmark, + [...rows, ...page.rows] + ) +} + +exports.paginatedSearch = async ( + appId, + query, + tableId, + sort, + sortOrder, + sortType, + limit, + bookmark +) => { + if (limit == null || isNaN(limit) || limit < 0) { + limit = 50 + } + const builder = new QueryBuilder(appId, query) + .setTable(tableId) + .setSort(sort) + .setSortOrder(sortOrder) + .setSortType(sortType) + .setBookmark(bookmark) + .setLimit(limit) + const searchUrl = builder.buildSearchURL() + const nextUrl = builder.setLimit(1).buildSearchURL() + const searchResults = await runQuery(searchUrl) + const nextResults = await runQuery(nextUrl) + return { + ...searchResults, + hasNextPage: nextResults.rows && nextResults.rows.length > 0, + } +} + +exports.fullSearch = async ( + appId, + query, + tableId, + sort, + sortOrder, + sortType, + limit +) => { + if (limit == null || isNaN(limit) || limit < 0) { + limit = 1000 + } + const rows = await recursiveSearch( + appId, + query, + tableId, + sort, + sortOrder, + sortType, + Math.min(limit, 1000), + null, + [] + ) + return { rows } +} diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js index be14910f3e..494ea61608 100644 --- a/packages/server/src/api/routes/row.js +++ b/packages/server/src/api/routes/row.js @@ -39,12 +39,6 @@ router usage, rowController.save ) - .post( - "/api/:tableId/rows/search", - paramResource("tableId"), - authorized(PermissionTypes.TABLE, PermissionLevels.READ), - rowController.search - ) .patch( "/api/:tableId/rows/:rowId", paramSubResource("tableId", "rowId"), diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 6a6b00732b..c19deb9d76 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -65,41 +65,6 @@ "type": "schema" } }, - "search": { - "name": "Search", - "description": "A searchable list of items.", - "icon": "Search", - "styleable": true, - "hasChildren": true, - "settings": [ - { - "type": "table", - "label": "Table", - "key": "table" - }, - { - "type": "multifield", - "label": "Columns", - "key": "columns", - "dependsOn": "table" - }, - { - "type": "number", - "label": "Rows/Page", - "defaultValue": 25, - "key": "pageSize" - }, - { - "type": "text", - "label": "Empty Text", - "key": "noRowsMessage", - "defaultValue": "No rows found." - } - ], - "context": { - "type": "schema" - } - }, "stackedlist": { "name": "Stacked List", "icon": "TaskList", @@ -1446,7 +1411,14 @@ { "type": "number", "label": "Limit", - "key": "limit" + "key": "limit", + "defaultValue": 50 + }, + { + "type": "boolean", + "label": "Paginate", + "key": "paginate", + "defaultValue": true } ], "context": { @@ -1464,14 +1436,6 @@ "label": "Schema", "key": "schema" }, - { - "label": "Loading", - "key": "loading" - }, - { - "label": "Loaded", - "key": "loaded" - }, { "label": "Page Number", "key": "pageNumber" diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 418d80f7a9..80ddffedee 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -1,11 +1,13 @@ -
+
- + {#if !loaded && loading} +
+ +
+ {:else} + + {#if paginate} + + {/if} + {/if}
+ + diff --git a/packages/standard-components/src/Search.svelte b/packages/standard-components/src/Search.svelte deleted file mode 100644 index 29ca3b011b..0000000000 --- a/packages/standard-components/src/Search.svelte +++ /dev/null @@ -1,195 +0,0 @@ - - - -
-
- {#if schema} - {#each columns as field} -
- - {#if schema[field].type === "options"} - - {:else if schema[field].type === "datetime"} - - {:else if schema[field].type === "boolean"} - - {:else if schema[field].type === "number"} - - {:else if schema[field].type === "string"} - - {/if} -
- {/each} - {/if} -
- - -
-
- {#if loaded} - {#if rows.length > 0} - {#if $component.children === 0 && $builderStore.inBuilder} -

Add some components to display.

- {:else} - {#each rows as row} - - - - {/each} - {/if} - {:else if noRowsMessage} -

{noRowsMessage}

- {/if} - {/if} - -
-
- - From 4d564365cf1380331efe9149756405d2c3524ae2 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:30:45 +0100 Subject: [PATCH 15/47] Clean up data source context and remove explicit button actions for pagination --- .../EventsEditor/actions/NextPage.svelte | 38 ------------------- .../EventsEditor/actions/PrevPage.svelte | 38 ------------------- .../EventsEditor/actions/index.js | 10 ----- packages/client/src/constants.js | 2 - packages/client/src/utils/buttonActions.js | 33 ++++------------ packages/standard-components/manifest.json | 1 - .../src/DataProvider.svelte | 19 +--------- 7 files changed, 8 insertions(+), 133 deletions(-) delete mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte delete mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/PrevPage.svelte diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte deleted file mode 100644 index a96e1bd3d5..0000000000 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NextPage.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -
- - x._instanceName} - getOptionValue={(x) => x._id} - /> -
- - diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js index 95c92a3f6d..4700ea5c8f 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js @@ -6,8 +6,6 @@ import TriggerAutomation from "./TriggerAutomation.svelte" import ValidateForm from "./ValidateForm.svelte" import LogIn from "./LogIn.svelte" import LogOut from "./LogOut.svelte" -import NextPage from "./NextPage.svelte" -import PrevPage from "./PrevPage.svelte" // defines what actions are available, when adding a new one // the component is the setup panel for the action @@ -47,12 +45,4 @@ export default [ name: "Log Out", component: LogOut, }, - { - name: "Next Page", - component: NextPage, - }, - { - name: "Previous Page", - component: PrevPage, - }, ] diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js index fc69ac212c..3aa302bec9 100644 --- a/packages/client/src/constants.js +++ b/packages/client/src/constants.js @@ -5,6 +5,4 @@ export const TableNames = { export const ActionTypes = { ValidateForm: "ValidateForm", RefreshDatasource: "RefreshDatasource", - NextPage: "NextPage", - PrevPage: "PrevPage", } diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index f78ac1773c..4d2865d586 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -16,27 +16,27 @@ const saveRowHandler = async (action, context) => { } } -const deleteRowHandler = async (action) => { +const deleteRowHandler = async action => { const { tableId, revId, rowId } = action.parameters if (tableId && revId && rowId) { await deleteRow({ tableId, rowId, revId }) } } -const triggerAutomationHandler = async (action) => { +const triggerAutomationHandler = async action => { const { fields } = action.parameters if (fields) { await triggerAutomation(action.parameters.automationId, fields) } } -const navigationHandler = (action) => { +const navigationHandler = action => { if (action.parameters.url) { routeStore.actions.navigate(action.parameters.url) } } -const queryExecutionHandler = async (action) => { +const queryExecutionHandler = async action => { const { datasourceId, queryId, queryParams } = action.parameters await executeQuery({ datasourceId, @@ -68,23 +68,7 @@ const refreshDatasourceHandler = async (action, context) => { ) } -const nextPageHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.NextPage - ) -} - -const prevPageHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.PrevPage - ) -} - -const loginHandler = async (action) => { +const loginHandler = async action => { const { email, password } = action.parameters await authStore.actions.logIn({ email, password }) } @@ -103,8 +87,6 @@ const handlerMap = { ["Refresh Datasource"]: refreshDatasourceHandler, ["Log In"]: loginHandler, ["Log Out"]: logoutHandler, - ["Next Page"]: nextPageHandler, - ["Previous Page"]: prevPageHandler, } /** @@ -114,10 +96,9 @@ const handlerMap = { export const enrichButtonActions = (actions, context) => { // Prevent button actions in the builder preview if (get(builderStore).inBuilder) { - // TODO uncomment - // return () => {} + return () => {} } - const handlers = actions.map((def) => handlerMap[def["##eventHandlerType"]]) + const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) return async () => { for (let i = 0; i < handlers.length; i++) { try { diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index c19deb9d76..dab68f41ce 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1384,7 +1384,6 @@ "icon": "Data", "styleable": false, "hasChildren": true, - "actions": ["NextPage", "PrevPage"], "settings": [ { "type": "dataSource", diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 80ddffedee..64e387462a 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -54,25 +54,8 @@ callback: () => fetchData(dataSource), metadata: { dataSource }, }, - { - type: ActionTypes.NextPage, - callback: () => nextPage(), - }, - { - type: ActionTypes.PrevPage, - callback: () => prevPage(), - }, ] - $: dataContext = { - rows, - schema, - rowsLength: rows.length, - loading, - loaded, - pageNumber: pageNumber + 1, - hasNextPage, - hasPrevPage, - } + $: dataContext = { rows, schema, rowsLength: rows.length } const getSortType = (schema, sortColumn) => { if (!schema || !sortColumn || !schema[sortColumn]) { From 960a2925cab47d6ca8163cbfb5f34412e08db866 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:38:12 +0100 Subject: [PATCH 16/47] Fix bug with determing whether another page exists when performing paginated searches --- packages/server/src/api/controllers/search/utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index 6f2f29628a..b0a6322648 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -244,11 +244,15 @@ exports.paginatedSearch = async ( .setSort(sort) .setSortOrder(sortOrder) .setSortType(sortType) + const searchUrl = builder .setBookmark(bookmark) .setLimit(limit) - const searchUrl = builder.buildSearchURL() - const nextUrl = builder.setLimit(1).buildSearchURL() + .buildSearchURL() const searchResults = await runQuery(searchUrl) + const nextUrl = builder + .setBookmark(searchResults.bookmark) + .setLimit(1) + .buildSearchURL() const nextResults = await runQuery(nextUrl) return { ...searchResults, From 20bacaa50b6c0fdded1205fd4a1f60c59c15f2f4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:38:34 +0100 Subject: [PATCH 17/47] Remove leftover import of search component --- packages/standard-components/src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/standard-components/src/index.js b/packages/standard-components/src/index.js index 2ad685033a..7b4d492fa9 100644 --- a/packages/standard-components/src/index.js +++ b/packages/standard-components/src/index.js @@ -27,7 +27,6 @@ export { default as embed } from "./Embed.svelte" export { default as cardhorizontal } from "./CardHorizontal.svelte" export { default as cardstat } from "./CardStat.svelte" export { default as icon } from "./Icon.svelte" -export { default as search } from "./Search.svelte" export { default as backgroundimage } from "./BackgroundImage.svelte" export * from "./charts" export * from "./forms" From c1b6f26f285bb4dbc29a24569001a581675fc1bc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:38:56 +0100 Subject: [PATCH 18/47] Remove search component from component structure --- .../src/components/design/AppPreview/componentStructure.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index 91f039fe68..9bc6be093e 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -4,7 +4,6 @@ "table", "repeater", "button", - "search", { "name": "Form", "icon": "Form", From 25fdb30e348e04637c357b0882eb75f0464fea6f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 12:39:46 +0100 Subject: [PATCH 19/47] Remove initial work on pagination standard component --- packages/standard-components/manifest.json | 13 ---------- .../standard-components/src/Pagination.svelte | 25 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 packages/standard-components/src/Pagination.svelte diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index dab68f41ce..65aa159e4e 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1521,18 +1521,5 @@ "context": { "type": "schema" } - }, - "pagination": { - "name": "Pagination Control", - "icon": "", - "styleable": true, - "hasChildren": "false", - "settings": [ - { - "type": "dataProvider", - "label": "Provider", - "key": "dataProviderId" - } - ] } } diff --git a/packages/standard-components/src/Pagination.svelte b/packages/standard-components/src/Pagination.svelte deleted file mode 100644 index 12919cba8c..0000000000 --- a/packages/standard-components/src/Pagination.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -{#if hasValidContext} -
- Page {pageNumber} -
-{:else if builderStore.inBuilder} -
Choose a data provider to control with this pagination component.
-{/if} From 8d46cc60fea250cf50c71aab8f59a6badb9fde2d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 13:11:16 +0100 Subject: [PATCH 20/47] Add jsdoc to search utils --- .../src/api/controllers/search/utils.js | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js index b0a6322648..53648eb989 100644 --- a/packages/server/src/api/controllers/search/utils.js +++ b/packages/server/src/api/controllers/search/utils.js @@ -3,10 +3,20 @@ const { checkSlashesInUrl } = require("../../../utilities") const env = require("../../../environment") const fetch = require("node-fetch") +/** + * Escapes any characters in a string which lucene searches require to be + * escaped. + * @param value The value to escape + * @returns {string} + */ const luceneEscape = value => { return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") } +/** + * Class to build lucene query URLs. + * Optionally takes a base lucene query object. + */ class QueryBuilder { constructor(appId, base) { this.appId = appId @@ -105,6 +115,7 @@ class QueryBuilder { } } + // Construct the actual lucene search query string from JSON structure if (this.query.string) { build(this.query.string, (key, value) => { return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null @@ -146,6 +157,7 @@ class QueryBuilder { build(this.query.notEmpty, key => `${key}:["" TO *]`) } + // Build the full search URL let url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search` url += `/${SearchIndexes.ROWS}?q=${output}` url += `&limit=${Math.min(this.limit, 200)}` @@ -161,11 +173,18 @@ class QueryBuilder { if (this.bookmark) { url += `&bookmark=${this.bookmark}` } + console.log(url) + // Fix any double slashes in the URL return checkSlashesInUrl(url) } } +/** + * Executes a lucene search query. + * @param query The query URL + * @returns {Promise<{rows: []}>} + */ const runQuery = async query => { const response = await fetch(query, { method: "GET", @@ -183,6 +202,22 @@ const runQuery = async query => { return output } +/** + * Gets round the fixed limit of 200 results from a query by fetching as many + * pages as required and concatenating the results. This recursively operates + * until enough results have been found. + * @param appId {string} The app ID to search + * @param query {object} The JSON query structure + * @param tableId {string} The table ID to search + * @param sort {string} The sort column + * @param sortOrder {string} The sort order ("ascending" or "descending") + * @param sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * @param limit {number} The number of results to fetch + * @param bookmark {string|null} Current bookmark in the recursive search + * @param rows {array|null} Current results in the recursive search + * @returns {Promise<*[]|*>} + */ const recursiveSearch = async ( appId, query, @@ -191,8 +226,8 @@ const recursiveSearch = async ( sortOrder, sortType, limit, - bookmark, - rows + bookmark = null, + rows = [] ) => { if (rows.length >= limit) { return rows @@ -226,6 +261,21 @@ const recursiveSearch = async ( ) } +/** + * Performs a paginated search. A bookmark will be returned to allow the next + * page to be fetched. There is a max limit off 200 results per page in a + * paginated search. + * @param appId {string} The app ID to search + * @param query {object} The JSON query structure + * @param tableId {string} The table ID to search + * @param sort {string} The sort column + * @param sortOrder {string} The sort order ("ascending" or "descending") + * @param sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * @param limit {number} The desired page size + * @param bookmark {string} The bookmark to resume from + * @returns {Promise<{hasNextPage: boolean, rows: *[]}>} + */ exports.paginatedSearch = async ( appId, query, @@ -239,6 +289,7 @@ exports.paginatedSearch = async ( if (limit == null || isNaN(limit) || limit < 0) { limit = 50 } + limit = Math.min(limit, 200) const builder = new QueryBuilder(appId, query) .setTable(tableId) .setSort(sort) @@ -249,17 +300,36 @@ exports.paginatedSearch = async ( .setLimit(limit) .buildSearchURL() const searchResults = await runQuery(searchUrl) + + // Try fetching 1 row in the next page to see if another page of results + // exists or not const nextUrl = builder .setBookmark(searchResults.bookmark) .setLimit(1) .buildSearchURL() const nextResults = await runQuery(nextUrl) + return { ...searchResults, hasNextPage: nextResults.rows && nextResults.rows.length > 0, } } +/** + * Performs a full search, fetching multiple pages if required to return the + * desired amount of results. There is a limit of 1000 results to avoid + * heavy performance hits, and to avoid client components breaking from + * handling too much data. + * @param appId {string} The app ID to search + * @param query {object} The JSON query structure + * @param tableId {string} The table ID to search + * @param sort {string} The sort column + * @param sortOrder {string} The sort order ("ascending" or "descending") + * @param sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * @param limit {number} The desired number of results + * @returns {Promise<{rows: *}>} + */ exports.fullSearch = async ( appId, query, @@ -272,6 +342,7 @@ exports.fullSearch = async ( if (limit == null || isNaN(limit) || limit < 0) { limit = 1000 } + limit = Math.min(limit, 1000) const rows = await recursiveSearch( appId, query, @@ -279,9 +350,7 @@ exports.fullSearch = async ( sort, sortOrder, sortType, - Math.min(limit, 1000), - null, - [] + limit ) return { rows } } From d82cd0e4afb12bf5ea1be0c8efeb8d82831e5917 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 13:11:45 +0100 Subject: [PATCH 21/47] Wait until table schema loads before searching to avoid initial double search --- .../src/DataProvider.svelte | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 64e387462a..66414ac39b 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -17,6 +17,7 @@ // Loading flag for the initial load let loaded = false + let schemaLoaded = false // Provider state let rows = [] @@ -31,16 +32,23 @@ $: hasPrevPage = pageNumber > 0 $: getSchema(dataSource) $: sortType = getSortType(schema, sortColumn) - $: fetchData( - dataSource, - query, - limit, - sortColumn, - sortOrder, - sortType, - paginate - ) $: { + // 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 + ) + } + } + $: { + // Sort and limit rows in memory when we aren't searching internal tables if (internalTable) { rows = allRows } else { @@ -195,6 +203,7 @@ } }) schema = fixedSchema + schemaLoaded = true } const nextPage = async () => { From 44f0e71aec75df064cf79ecb89c130efdf04f2a2 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 13:12:59 +0100 Subject: [PATCH 22/47] Fix incorrect background colour in standard-components table background --- packages/standard-components/src/table/Table.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/standard-components/src/table/Table.svelte b/packages/standard-components/src/table/Table.svelte index f68835d75b..35839e4722 100644 --- a/packages/standard-components/src/table/Table.svelte +++ b/packages/standard-components/src/table/Table.svelte @@ -94,3 +94,9 @@
+ + From 1a729aca9147f077e1b3c15ad79f03c8fa42f359 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 15:35:54 +0100 Subject: [PATCH 23/47] Remove sorting by auto ID when output processing rows --- .../bbui/src/Pagination/Pagination.svelte | 1 + .../src/api/controllers/search/utils.js | 5 ++--- packages/server/src/utilities/rowProcessor.js | 20 ------------------- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/bbui/src/Pagination/Pagination.svelte b/packages/bbui/src/Pagination/Pagination.svelte index cfe989ea14..e9ee31cc65 100644 --- a/packages/bbui/src/Pagination/Pagination.svelte +++ b/packages/bbui/src/Pagination/Pagination.svelte @@ -47,6 +47,7 @@ From a94682d64588d62e77f68e14d4d1edff2f444f3b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 May 2021 16:33:19 +0100 Subject: [PATCH 28/47] Simplify loading logic to prevent empty state flashing when loading data in data providers --- packages/client/src/api/tables.js | 8 ++++---- packages/standard-components/src/DataProvider.svelte | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js index a75a2d368b..59381e35bf 100644 --- a/packages/client/src/api/tables.js +++ b/packages/client/src/api/tables.js @@ -23,7 +23,6 @@ export const fetchTableData = async tableId => { export const searchTable = async ({ tableId, query, - raw, bookmark, limit, sort, @@ -31,14 +30,15 @@ export const searchTable = async ({ sortType, paginate, }) => { - if (!tableId || (!query && !raw)) { - return + if (!tableId || !query) { + return { + rows: [], + } } const res = await API.post({ url: `/api/search/${tableId}/rows`, body: { query, - raw, bookmark, limit, sort, diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 66414ac39b..83f30fab86 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -114,7 +114,6 @@ sortType, paginate ) => { - console.log("FETCH") loading = true if (dataSource?.type === "table") { const res = await API.searchTable({ @@ -248,7 +247,7 @@
- {#if !loaded && loading} + {#if !loaded}
From c9273ca9c2f398b365a7e9fb3a30a2143b11fd4f Mon Sep 17 00:00:00 2001 From: sovlookup Date: Fri, 14 May 2021 15:27:28 +0800 Subject: [PATCH 29/47] zh_translation --- i18n/README.zh.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 i18n/README.zh.md diff --git a/i18n/README.zh.md b/i18n/README.zh.md new file mode 100644 index 0000000000..4b9be29ebb --- /dev/null +++ b/i18n/README.zh.md @@ -0,0 +1,193 @@ +

+ + Budibase + +

+

+ Budibase +

+

+ 在您自己的设施上快速构建应用 +

+

+ Budibase是一个开放源代码的低代码平台,可帮助开发人员和IT专业人员在几分钟内在自己的设施上构建,自动化和交付自定义业务应用。 +

+ + +

+ 🤖 🎨 🚀 +

+ + +

+ +

+ +

+ + GitHub all releases + + + GitHub release (latest by date) + + + Discord + + + Follow @budibase + + Code of conduct + + + +

+

+ 注册 + · + 文档 + · + 有新需求? + · + 报告bug + · + Support: 讨论 + & + Discord +

+ + + +## ✨ 特征 +当其他平台选择封闭源代码路线时,我们决定采用开放源代码。当其他平台选择云构建器时,我们决定由本地构建器提供更好的开发人员体验。我们喜欢在Budibase上做不同的事情。 + +- **构建和发布真实的软件。**与其他平台不同,使用Budibase可以构建和交付单页应用程序。Budibase应用程序具有增强的性能,并且可以进行响应式设计,从而为您的用户提供出色的体验。 +- **开源且可扩展。**Budibase是开源的。该构建器已获得AGPL v3许可,服务器是GPL v3,客户端是MPL。这会让您充满信心,Budibase将永远存在。您还可以针对Budibase进行编码或对其进行分叉,并根据需要进行更改,以提供开发人员友好的体验。 +- **加载数据或从头开始。**Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[请求新的数据源](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **使用强大的预制组件设计和构建应用程序。**Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[要求新的组件](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **自动化流程,与其他工具集成,并连接到Webhooks。**通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[请求新的集成](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **云托管和自我托管。**用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们的目标是在将来进行更改。对于大量使用,我们建议用户进行自我托管。 + +

+ Budibase design ui +

+ + +## ⌛ 状态 +- [x] Alpha:我们正在将Budibase演示给用户并收到反馈 +- [x] 内部测试:我们正在与一组封闭的客户一起测试Budibase +- [x] 公开测试:任何人都可以[注册并使用Budibase](https://portal.budi.live/signup) +- [ ] 正式发布 + +关注此存储库的“release”以获取主要更新的通知,并给我们一个star~。 + +

+ +

+ +### Star时序图 + +[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) + +如果您在两个版本之间遇到问题,请使用[此处](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting)的指南清理您的环境。 + + +## 🏁 Budibase入门 + +Budibase构建器在Mac,PC和Linux上的Electron中运行。请按照以下步骤开始: + +- [ ] [注册Budibase](https://portal.budi.live/signup) +- [ ] 创建用户名和密码 +- [ ] 复制您的API密钥 +- [ ] 下载Budibase +- [ ] 打开Budibase并输入您的API密钥 + +如果您需要其他帮助,请[参阅以下指导教程](https://docs.budibase.com/tutorial/tutorial-signing-up)。 + + +## 🤖 自托管 + +Budibase希望确保任何人都可以使用我们开发的工具,并且我们知道很多人需要能够在自己的系统上托管他们制作的应用程序-这就是为什么我们决定尝试使自托管变得如此简单的原因可能的! + +当前,您可以使用Docker或Digital Ocean托管您的应用程序。可以在[此处](https://docs.budibase.com/self-hosting/introduction-to-self-hosting)找到有关自我托管的文档。 + +[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04) + + +## 🎓 学习Budibase + +Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase.com/)。 + +您还可以按照有关[如何使用Budibase构建CRM](https://docs.budibase.com/tutorial/tutorial-introduction)的快速教程进行[操作](https://docs.budibase.com/tutorial/tutorial-introduction)https://docs.budibase.com/tutorial/tutorial-introduction) + + +## 路线图 +查看我们的公共路线图。如果您想讨论路线图上的某些项目,请与Discord或通过Github进行讨论 + + +## ❗ 行为守则 + +Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。请阅读。 + +## 🙌 为Budibase贡献 + +从打开错误报告到创建拉取请求:每一个贡献都将受到赞赏和欢迎。如果您打算实施一项新功能或更改API,请先创建一个问题。这样我们可以确保您的工作没有白费。 + +### 不知道从哪里开始? + +[首次发行项目](https://github.com/Budibase/budibase/projects/22)是[一个](https://github.com/Budibase/budibase/projects/22)开始做出贡献的好地方。 + +### 存储库的组织方式 + +Budibase是由lerna管理的monorepo。Lerna管理budibase软件包的构建和发布。在较高的层次上,这里是构成Budibase的软件包。 + +- [包/构建器](https://github.com/Budibase/budibase/tree/HEAD/packages/builder)-包含budibase构建器客户端苗条应用程序的代码。 +- [包/客户端](https://github.com/Budibase/budibase/tree/HEAD/packages/client)-在浏览器中运行的模块,负责读取JSON定义并从中创建生动的Web应用程序。 +- [包/服务器](https://github.com/Budibase/budibase/tree/HEAD/packages/server)-budibase服务器。该Koa应用程序负责为构建器和budibase应用程序提供JS服务,并提供与数据库和文件系统交互的API。 + +有关更多信息,请参见[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) + +## 📝 开源协议 + +Budibase是开源的。该构建器的许可证为[AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html),服务器的许可证为[GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html),客户端的许可证为[MPL](https://directory.fsf.org/wiki/License:MPL-2.0)。 + +## 💬 保持联系 + +如果您有任何疑问或想与其他Budibase用户交谈,请跳至[Github讨论](https://github.com/Budibase/budibase/discussions)或加入我们的Discord服务器: + +[Discord 聊天室](https://discord.gg/rCYayfe) + +![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield) + + +## 贡献者 ✨ + +感谢这些出色的人 ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + +

Martin McKeaveney

💻 📖 ⚠️ 🚇

Michael Drury

📖 💻 ⚠️ 🚇

Andrew Kingston

📖 💻 ⚠️ 🎨

Michael Shanks

📖 💻 ⚠️

Kevin Åberg Kultalahti

📖 💻 ⚠️

Joe

📖 💻 🖋 🎨

Conor_Mack

💻 ⚠️

pngwn

💻 ⚠️

HugoLd

💻

victoriasloan

💻

yashank09

💻

SOVLOOKUP

💻
+ + + + + + +该项目遵循[所有参与者的](https://github.com/all-contributors/all-contributors)规范。欢迎任何形式的捐助! \ No newline at end of file From cba5c8899cc7f83a7934a50f62afb2b2b02d9393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:36:43 +0800 Subject: [PATCH 30/47] Update README.zh.md --- i18n/README.zh.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 4b9be29ebb..67e2a9d1f0 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -50,7 +50,7 @@ · 报告bug · - Support: 讨论 + 支持: 讨论区 & Discord @@ -190,4 +190,4 @@ Budibase是开源的。该构建器的许可证为[AGPL v3](https://www.gnu.org/ -该项目遵循[所有参与者的](https://github.com/all-contributors/all-contributors)规范。欢迎任何形式的捐助! \ No newline at end of file +该项目遵循[所有参与者的](https://github.com/all-contributors/all-contributors)规范。欢迎任何形式的捐助! From cd874cbcb3fe99682be88bb6673f9dc0a43853c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:42:39 +0800 Subject: [PATCH 31/47] Update README.zh.md --- i18n/README.zh.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 67e2a9d1f0..268a97592d 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -46,9 +46,9 @@ · 文档 · - 有新需求? + 提出需求 · - 报告bug + 报告Bug · 支持: 讨论区 & @@ -58,14 +58,14 @@ ## ✨ 特征 -当其他平台选择封闭源代码路线时,我们决定采用开放源代码。当其他平台选择云构建器时,我们决定由本地构建器提供更好的开发人员体验。我们喜欢在Budibase上做不同的事情。 +当其他平台选择闭源路线时,我们决定采用开放源代码。当其他平台选择云服务时,我们决定提供本地构建器提供更好的开发体验。我们喜欢在Budibase上做不同的事情。 -- **构建和发布真实的软件。**与其他平台不同,使用Budibase可以构建和交付单页应用程序。Budibase应用程序具有增强的性能,并且可以进行响应式设计,从而为您的用户提供出色的体验。 -- **开源且可扩展。**Budibase是开源的。该构建器已获得AGPL v3许可,服务器是GPL v3,客户端是MPL。这会让您充满信心,Budibase将永远存在。您还可以针对Budibase进行编码或对其进行分叉,并根据需要进行更改,以提供开发人员友好的体验。 -- **加载数据或从头开始。**Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[请求新的数据源](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 -- **使用强大的预制组件设计和构建应用程序。**Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[要求新的组件](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 -- **自动化流程,与其他工具集成,并连接到Webhooks。**通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[请求新的集成](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 -- **云托管和自我托管。**用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们的目标是在将来进行更改。对于大量使用,我们建议用户进行自我托管。 +- **构建和发布真实的软件** 与其他平台不同,使用Budibase可以构建和交付单页应用程序。Budibase应用程序具有增强的性能,并且可以进行响应式设计,从而为您的用户提供出色的体验。 +- **开源且可扩展** Budibase是开源的。Builder使用AGPLv3许可,Server使用GPLv3,Client使用MPL。这会让您充满信心,Budibase将永远存在。您还可以针对Budibase进行修改或对其进行分叉,并根据需要进行更改,以提供开发人员友好的体验。 +- **加载数据或从头开始** Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[新的数据源需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **使用强大的预制组件设计和构建应用程序** Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[新的组件需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **自动化流程,与其他工具集成,并连接到Webhooks** 通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[新的集成需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **云托管和自我托管** 用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们的目标是在将来进行更改。对于大量使用,我们建议用户进行自我托管。

Budibase design ui From 5933069304c2c0b16f39039f5e5dc6b27a5293b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:44:17 +0800 Subject: [PATCH 32/47] Update README.zh.md --- i18n/README.zh.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 268a97592d..bfe2099b9b 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -57,12 +57,12 @@ -## ✨ 特征 +## ✨ 功能 当其他平台选择闭源路线时,我们决定采用开放源代码。当其他平台选择云服务时,我们决定提供本地构建器提供更好的开发体验。我们喜欢在Budibase上做不同的事情。 - **构建和发布真实的软件** 与其他平台不同,使用Budibase可以构建和交付单页应用程序。Budibase应用程序具有增强的性能,并且可以进行响应式设计,从而为您的用户提供出色的体验。 - **开源且可扩展** Budibase是开源的。Builder使用AGPLv3许可,Server使用GPLv3,Client使用MPL。这会让您充满信心,Budibase将永远存在。您还可以针对Budibase进行修改或对其进行分叉,并根据需要进行更改,以提供开发人员友好的体验。 -- **加载数据或从头开始** Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[新的数据源需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 +- **连接数据库或直接开始** Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[新的数据源需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 - **使用强大的预制组件设计和构建应用程序** Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[新的组件需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 - **自动化流程,与其他工具集成,并连接到Webhooks** 通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[新的集成需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 - **云托管和自我托管** 用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们的目标是在将来进行更改。对于大量使用,我们建议用户进行自我托管。 @@ -78,7 +78,7 @@ - [x] 公开测试:任何人都可以[注册并使用Budibase](https://portal.budi.live/signup) - [ ] 正式发布 -关注此存储库的“release”以获取主要更新的通知,并给我们一个star~。 +关注此存储库的“release”以获取主要更新的通知,并给我们一个star~。

From ac9ded70ff5f5a9b6c74589e2a6150300ebb7081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:45:57 +0800 Subject: [PATCH 33/47] Update README.zh.md --- i18n/README.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index bfe2099b9b..64a2d61ed9 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -106,7 +106,7 @@ Budibase构建器在Mac,PC和Linux上的Electron中运行。请按照以下步 ## 🤖 自托管 -Budibase希望确保任何人都可以使用我们开发的工具,并且我们知道很多人需要能够在自己的系统上托管他们制作的应用程序-这就是为什么我们决定尝试使自托管变得如此简单的原因可能的! +Budibase希望确保任何人都可以使用我们开发的工具,并且我们知道很多人需要能够在自己的系统上托管他们制作的应用程序——这就是为什么我们决定尝试使自托管服务变得如此简单的原因! 当前,您可以使用Docker或Digital Ocean托管您的应用程序。可以在[此处](https://docs.budibase.com/self-hosting/introduction-to-self-hosting)找到有关自我托管的文档。 From 79cd10181d6c8c98e59e19bdcf3e9487ba4741d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:53:34 +0800 Subject: [PATCH 34/47] Update README.zh.md --- i18n/README.zh.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 64a2d61ed9..286452df02 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -121,16 +121,16 @@ Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase ## 路线图 -查看我们的公共路线图。如果您想讨论路线图上的某些项目,请与Discord或通过Github进行讨论 +查看我们的[公共路线图](https://github.com/Budibase/budibase/projects/10)。如果您想讨论路线图上的某些项目,请与[Discord](https://discord.gg/rCYayfe)或通过Github进行[讨论](https://github.com/Budibase/budibase/discussions) ## ❗ 行为守则 -Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。请阅读。 +Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**]请阅读(https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 ## 🙌 为Budibase贡献 -从打开错误报告到创建拉取请求:每一个贡献都将受到赞赏和欢迎。如果您打算实施一项新功能或更改API,请先创建一个问题。这样我们可以确保您的工作没有白费。 +从错误报告到PR请求:每一个贡献都将受到赞赏和欢迎。如果您打算实施一项新功能或更改API,请先创建一个Issue。这样我们可以确保您的工作没有白费。 ### 不知道从哪里开始? @@ -138,11 +138,11 @@ Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们 ### 存储库的组织方式 -Budibase是由lerna管理的monorepo。Lerna管理budibase软件包的构建和发布。在较高的层次上,这里是构成Budibase的软件包。 +Budibase是由lerna管理的项目。Lerna管理budibase软件包的构建和发布。在较高的层次上,这里是构成Budibase的软件包。 -- [包/构建器](https://github.com/Budibase/budibase/tree/HEAD/packages/builder)-包含budibase构建器客户端苗条应用程序的代码。 -- [包/客户端](https://github.com/Budibase/budibase/tree/HEAD/packages/client)-在浏览器中运行的模块,负责读取JSON定义并从中创建生动的Web应用程序。 -- [包/服务器](https://github.com/Budibase/budibase/tree/HEAD/packages/server)-budibase服务器。该Koa应用程序负责为构建器和budibase应用程序提供JS服务,并提供与数据库和文件系统交互的API。 +- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder)-包含budibase构建器客户端苗条应用程序的代码。 +- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client)-在浏览器中运行的模块,负责读取JSON定义并从中创建生动的Web应用程序。 +- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server)-budibase服务器。该Koa应用程序负责为构建器和budibase应用程序提供JS服务,并提供与数据库和文件系统交互的API。 有关更多信息,请参见[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) From cb41b9a00e39129e76cb4e5d7a6358189eefcfbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:56:50 +0800 Subject: [PATCH 35/47] Update README.zh.md --- i18n/README.zh.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 286452df02..7238ea847d 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -117,7 +117,7 @@ Budibase希望确保任何人都可以使用我们开发的工具,并且我们 Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase.com/)。 -您还可以按照有关[如何使用Budibase构建CRM](https://docs.budibase.com/tutorial/tutorial-introduction)的快速教程进行[操作](https://docs.budibase.com/tutorial/tutorial-introduction)https://docs.budibase.com/tutorial/tutorial-introduction) +您还可以按照有关[如何使用Budibase构建CRM](https://docs.budibase.com/tutorial/tutorial-introduction)的快速教程进行[操作](https://docs.budibase.com/tutorial/tutorial-introduction) ## 路线图 @@ -126,7 +126,7 @@ Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase ## ❗ 行为守则 -Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**]请阅读(https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 +Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 ## 🙌 为Budibase贡献 From 907cbac74a6f1a900cc51c44c2c2e2faa4dc2b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 15:57:52 +0800 Subject: [PATCH 36/47] Update README.zh.md --- i18n/README.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 7238ea847d..635a928bea 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -126,7 +126,7 @@ Budibase[文档](https://docs.budibase.com/)位于[此处](https://docs.budibase ## ❗ 行为守则 -Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们[**的行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 +Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们希望Budibase社区中的每个人都遵守我们的[**行为准则**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)。 ## 🙌 为Budibase贡献 From c2816577c93038991d8b88dff9b27862ae14bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 16:02:50 +0800 Subject: [PATCH 37/47] Update README.zh.md --- i18n/README.zh.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 635a928bea..2d0634a1a5 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -10,7 +10,7 @@ 在您自己的设施上快速构建应用

- Budibase是一个开放源代码的低代码平台,可帮助开发人员和IT专业人员在几分钟内在自己的设施上构建,自动化和交付自定义业务应用。 + Budibase是一个开放源代码的低代码平台,可帮助开发人员和IT专业人员在几分钟内在自己的设施上构建、自动化和交付部署定制业务应用。

@@ -134,7 +134,7 @@ Budibase致力于为每个人提供热情,多样且无烦恼的体验。我们 ### 不知道从哪里开始? -[首次发行项目](https://github.com/Budibase/budibase/projects/22)是[一个](https://github.com/Budibase/budibase/projects/22)开始做出贡献的好地方。 +[第一次提出Issue](https://github.com/Budibase/budibase/projects/22)是一个开始做出贡献的好地方。 ### 存储库的组织方式 @@ -144,7 +144,7 @@ Budibase是由lerna管理的项目。Lerna管理budibase软件包的构建和发 - [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client)-在浏览器中运行的模块,负责读取JSON定义并从中创建生动的Web应用程序。 - [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server)-budibase服务器。该Koa应用程序负责为构建器和budibase应用程序提供JS服务,并提供与数据库和文件系统交互的API。 -有关更多信息,请参见[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) +有关更多信息,请参见[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) ## 📝 开源协议 From 682a3ad99d4347e32544fb37b6e8181fb0102ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=8C=97?= <805408477@qq.com> Date: Fri, 14 May 2021 16:03:50 +0800 Subject: [PATCH 38/47] Update README.zh.md --- i18n/README.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/README.zh.md b/i18n/README.zh.md index 2d0634a1a5..7e4dffd387 100644 --- a/i18n/README.zh.md +++ b/i18n/README.zh.md @@ -65,7 +65,7 @@ - **连接数据库或直接开始** Budibase从多个来源(包括MongoDB,CouchDB,PostgreSQL,mySQL,Airtable,Google Sheets,S3,DyanmoDB或REST API)提取数据。与其他平台不同,使用Budibase可以从头开始,创建没有数据源的业务应用程序。[新的数据源需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 - **使用强大的预制组件设计和构建应用程序** Budibase开箱即用,具有精美设计,功能强大的组件,您可以使用它们像构建基块来构建UI。我们还提供了许多您最喜欢的CSS样式选项,因此您可以发挥更多的创意。[新的组件需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 - **自动化流程,与其他工具集成,并连接到Webhooks** 通过自动化手动流程和工作流来节省时间。从连接到Webhook,到自动发送电子邮件,只需告诉Budibase要做些什么,然后让它为您工作。您可以[在此处](https://github.com/Budibase/automations)轻松地[为Budibase创建新的自动化,](https://github.com/Budibase/automations)或[在此处](https://github.com/Budibase/automations)[新的集成需求](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 -- **云托管和自我托管** 用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们的目标是在将来进行更改。对于大量使用,我们建议用户进行自我托管。 +- **云托管和自我托管** 用户可以自行托管(请参见下文),或使用Budibase托管其应用。目前,我们的云托管产品仅限于免费套餐,但我们会在将来进行更改。对于大量使用,我们建议用户进行自我托管。

Budibase design ui From f82bd4465b0a4fc9679fe3321b8ce859f199bb8b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 May 2021 13:11:23 +0100 Subject: [PATCH 39/47] Add brief docs to Table component explaining the schema prop --- packages/bbui/src/Table/Table.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 4278751c5f..07ba7fa6e9 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -4,6 +4,16 @@ import CellRenderer from "./CellRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte" + /** + * The expected schema is our normal couch schemas for our tables. + * Each field schema can be enriched with a few extra properties to customise + * the behaviour. + * All of these are optional and do not need to be added. + * displayName: Overrides the field name displayed as the column title + * sortable: Set to false to disable sorting data by a certain column + * editable: Set to false to disable editing a certain column if the + * allowEditColumns prop is true + */ export let data = [] export let schema = {} export let showAutoColumns = false From e78d686292e23f0394ae0b0266c93eb983815841 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 May 2021 13:29:14 +0100 Subject: [PATCH 40/47] Add optional info text to components --- .../design/PropertiesPanel/SettingsView.svelte | 9 ++++++--- packages/standard-components/manifest.json | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte index 9a81a0dba3..eeb4fe1bb3 100644 --- a/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/SettingsView.svelte @@ -156,8 +156,11 @@ {/if} {/each} {:else} -

- This component doesn't have any additional settings. +
This component doesn't have any additional settings.
+ {/if} + {#if componentDefinition?.info} +
+ {@html componentDefinition?.info}
{/if} @@ -185,7 +188,7 @@ height: 100%; gap: var(--spacing-s); } - .empty { + .text { font-size: var(--spectrum-global-dimension-font-size-75); margin-top: var(--spacing-m); color: var(--grey-6); diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 65aa159e4e..1c8502c0f5 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1381,6 +1381,7 @@ }, "dataprovider": { "name": "Data Provider", + "info": "Pagination can only be used with data sources that are tables.", "icon": "Data", "styleable": false, "hasChildren": true, From 4675272f4ba93b6637fe8b80234b63e4fc5511e3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 May 2021 13:29:37 +0100 Subject: [PATCH 41/47] Don't show pagination control if the data source isn't an internal table --- packages/standard-components/src/DataProvider.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index 83f30fab86..e0b2ad859a 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -253,7 +253,7 @@
{:else} - {#if paginate} + {#if paginate && internalTable}