Add support for numerical sorting

This commit is contained in:
Andrew Kingston 2021-05-11 11:24:16 +01:00
parent 3eabaea42f
commit 78ae68981e
8 changed files with 101 additions and 43 deletions

View File

@ -40,13 +40,11 @@
if (wasSelectedTable._id === table._id) { if (wasSelectedTable._id === table._id) {
$goto("./table") $goto("./table")
} }
editorModal.hide()
} }
async function save() { async function save() {
await tables.save(table) await tables.save(table)
notifications.success("Table renamed successfully") notifications.success("Table renamed successfully")
editorModal.hide()
} }
function checkValid(evt) { function checkValid(evt) {

View File

@ -9,7 +9,7 @@
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid" import { generate } from "shortid"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -159,35 +159,44 @@
bind:value={expression.field} bind:value={expression.field}
options={fieldOptions} options={fieldOptions}
on:change={e => onFieldChange(expression, e.detail)} on:change={e => onFieldChange(expression, e.detail)}
placeholder="Column" /> placeholder="Column"
/>
<Select <Select
disabled={!expression.field} disabled={!expression.field}
options={getValidOperatorsForType(expression.type)} options={getValidOperatorsForType(expression.type)}
bind:value={expression.operator} bind:value={expression.operator}
on:change={e => onOperatorChange(expression, e.detail)} on:change={e => onOperatorChange(expression, e.detail)}
placeholder={null} /> placeholder={null}
{#if ['string', 'longform', 'number'].includes(expression.type)} />
{#if ["string", "longform", "number"].includes(expression.type)}
<DrawerBindableInput <DrawerBindableInput
disabled={expression.noValue} disabled={expression.noValue}
title={`Value for "${expression.field}"`} title={`Value for "${expression.field}"`}
value={expression.value} value={expression.value}
placeholder="Value" placeholder="Value"
bindings={bindableProperties} bindings={bindableProperties}
on:change={event => (expression.value = event.detail)} /> on:change={event => (expression.value = event.detail)}
{:else if expression.type === 'options'} />
{:else if expression.type === "options"}
<Combobox <Combobox
disabled={expression.noValue} disabled={expression.noValue}
options={getFieldOptions(expression.field)} options={getFieldOptions(expression.field)}
bind:value={expression.value} /> bind:value={expression.value}
{:else if expression.type === 'boolean'} />
{:else if expression.type === "boolean"}
<Combobox <Combobox
disabled disabled
options={[{ label: 'True', value: true }, { label: 'False', value: false }]} options={[
bind:value={expression.value} /> { label: "True", value: true },
{:else if expression.type === 'datetime'} { label: "False", value: false },
]}
bind:value={expression.value}
/>
{:else if expression.type === "datetime"}
<DatePicker <DatePicker
disabled={expression.noValue} disabled={expression.noValue}
bind:value={expression.value} /> bind:value={expression.value}
/>
{:else} {:else}
<DrawerBindableInput disabled /> <DrawerBindableInput disabled />
{/if} {/if}
@ -196,7 +205,8 @@
size="S" size="S"
quiet quiet
icon="Close" icon="Close"
on:click={() => removeField(expression.id)} /> on:click={() => removeField(expression.id)}
/>
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -8,7 +8,6 @@
Layout, Layout,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import {} from "@budibase/bbui"
import { import {
getDatasourceForProvider, getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,

View File

@ -5,14 +5,14 @@ import { enrichRows } from "./rows"
* Fetches a table definition. * Fetches a table definition.
* Since definitions cannot change at runtime, the result is cached. * 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 }) return await API.get({ url: `/api/tables/${tableId}`, cache: true })
} }
/** /**
* Fetches all rows from a table. * 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` }) const rows = await API.get({ url: `/api/${tableId}/rows` })
return await enrichRows(rows, tableId) return await enrichRows(rows, tableId)
} }
@ -46,6 +46,7 @@ export const searchTable = async ({
limit, limit,
sort, sort,
sortOrder, sortOrder,
sortType,
}) => { }) => {
if (!tableId || (!query && !raw)) { if (!tableId || (!query && !raw)) {
return return
@ -59,6 +60,7 @@ export const searchTable = async ({
limit, limit,
sort, sort,
sortOrder, sortOrder,
sortType,
}, },
}) })
return { return {

View File

@ -1,12 +1,28 @@
const { QueryBuilder, buildSearchUrl, search } = require("./utils") const { QueryBuilder, buildSearchUrl, search } = require("./utils")
exports.rowSearch = async (ctx) => { exports.rowSearch = async ctx => {
const appId = ctx.appId const appId = ctx.appId
const { tableId } = ctx.params const { tableId } = ctx.params
const { bookmark, query, raw, limit, sort, sortOrder } = ctx.request.body const {
bookmark,
query,
raw,
limit,
sort,
sortOrder,
sortType,
} = ctx.request.body
let url let url
if (query) { if (query) {
url = new QueryBuilder(appId, query, bookmark, limit, sort, sortOrder) url = new QueryBuilder(
appId,
query,
bookmark,
limit,
sort,
sortOrder,
sortType
)
.addTable(tableId) .addTable(tableId)
.complete() .complete()
} else if (raw) { } else if (raw) {

View File

@ -12,6 +12,7 @@ const fetch = require("node-fetch")
* @param {number} limit The number of entries to return per query. * @param {number} limit The number of entries to return per query.
* @param {string} sort The column to sort by. * @param {string} sort The column to sort by.
* @param {string} sortOrder The order to sort by. "ascending" or "descending". * @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. * @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. * @return {string} The URL which a GET can be performed on to receive results.
*/ */
@ -21,18 +22,19 @@ function buildSearchUrl({
bookmark, bookmark,
sort, sort,
sortOrder, sortOrder,
sortType,
excludeDocs, excludeDocs,
limit = 50, limit = 50,
}) { }) {
let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search` let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search`
url += `/${SearchIndexes.ROWS}?q=${query}` url += `/${SearchIndexes.ROWS}?q=${query}`
url += `&limit=${limit}` url += `&limit=${Math.min(limit, 200)}`
if (!excludeDocs) { if (!excludeDocs) {
url += "&include_docs=true" url += "&include_docs=true"
} }
if (sort) { if (sort) {
const orderChar = sortOrder === "descending" ? "-" : "" const orderChar = sortOrder === "descending" ? "-" : ""
url += `&sort="${orderChar}${sort.replace(/ /, "_")}<string>"` url += `&sort="${orderChar}${sort.replace(/ /, "_")}<${sortType}>"`
} }
if (bookmark) { if (bookmark) {
url += `&bookmark=${bookmark}` url += `&bookmark=${bookmark}`
@ -41,12 +43,12 @@ function buildSearchUrl({
return checkSlashesInUrl(url) return checkSlashesInUrl(url)
} }
const luceneEscape = (value) => { const luceneEscape = value => {
return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&")
} }
class QueryBuilder { class QueryBuilder {
constructor(appId, base, bookmark, limit, sort, sortOrder) { constructor(appId, base, bookmark, limit, sort, sortOrder, sortType) {
this.appId = appId this.appId = appId
this.query = { this.query = {
string: {}, string: {},
@ -62,6 +64,7 @@ class QueryBuilder {
this.limit = limit || 50 this.limit = limit || 50
this.sort = sort this.sort = sort
this.sortOrder = sortOrder || "ascending" this.sortOrder = sortOrder || "ascending"
this.sortType = sortType || "string"
} }
setLimit(limit) { setLimit(limit) {
@ -165,10 +168,10 @@ class QueryBuilder {
}) })
} }
if (this.query.empty) { if (this.query.empty) {
build(this.query.empty, (key) => `!${key}:["" TO *]`) build(this.query.empty, key => `!${key}:["" TO *]`)
} }
if (this.query.notEmpty) { if (this.query.notEmpty) {
build(this.query.notEmpty, (key) => `${key}:["" TO *]`) build(this.query.notEmpty, key => `${key}:["" TO *]`)
} }
if (rawQuery) { if (rawQuery) {
output = output.length === 0 ? rawQuery : `&${rawQuery}` output = output.length === 0 ? rawQuery : `&${rawQuery}`
@ -180,11 +183,12 @@ class QueryBuilder {
limit: this.limit, limit: this.limit,
sort: this.sort, sort: this.sort,
sortOrder: this.sortOrder, sortOrder: this.sortOrder,
sortType: this.sortType,
}) })
} }
} }
exports.search = async (query) => { exports.search = async query => {
const response = await fetch(query, { const response = await fetch(query, {
method: "GET", method: "GET",
}) })
@ -193,7 +197,7 @@ exports.search = async (query) => {
rows: [], rows: [],
} }
if (json.rows != null && json.rows.length > 0) { 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) { if (json.bookmark) {
output.bookmark = json.bookmark output.bookmark = json.bookmark

View File

@ -18,17 +18,26 @@
// Provider state // Provider state
let rows = [] let rows = []
let allRows = []
let schema = {} let schema = {}
let bookmarks = [null] let bookmarks = [null]
let pageNumber = 0 let pageNumber = 0
$: internalTable = dataSource?.type === "table"
$: query = dataSource?.type === "table" ? buildLuceneQuery(filter) : null $: query = dataSource?.type === "table" ? buildLuceneQuery(filter) : null
$: hasNextPage = bookmarks[pageNumber + 1] != null $: hasNextPage = bookmarks[pageNumber + 1] != null
$: hasPrevPage = pageNumber > 0 $: hasPrevPage = pageNumber > 0
$: fetchData(dataSource, query, limit, sortColumn, sortOrder)
// $: sortedRows = sortRows(filteredRows, sortColumn, sortOrder)
// $: rows = limitRows(sortedRows, limit)
$: getSchema(dataSource) $: getSchema(dataSource)
$: sortType = getSortType(schema, sortColumn)
$: fetchData(dataSource, query, limit, sortColumn, sortOrder, sortType)
$: {
if (internalTable) {
rows = allRows
} else {
rows = sortRows(allRows, sortColumn, sortOrder)
rows = limitRows(rows, limit)
}
}
$: actions = [ $: actions = [
{ {
type: ActionTypes.RefreshDatasource, type: ActionTypes.RefreshDatasource,
@ -55,7 +64,15 @@
hasPrevPage, hasPrevPage,
} }
const buildLuceneQuery = (filter) => { const getSortType = (schema, sortColumn) => {
if (!schema || !sortColumn || !schema[sortColumn]) {
return "string"
}
const type = schema?.[sortColumn]?.type
return type === "number" ? "number" : "string"
}
const buildLuceneQuery = filter => {
let query = { let query = {
string: {}, string: {},
fuzzy: {}, fuzzy: {},
@ -66,7 +83,7 @@
notEmpty: {}, notEmpty: {},
} }
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
filter.forEach((expression) => { filter.forEach(expression => {
if (expression.operator.startsWith("range")) { if (expression.operator.startsWith("range")) {
let range = { let range = {
low: Number.MIN_SAFE_INTEGER, low: Number.MIN_SAFE_INTEGER,
@ -86,7 +103,14 @@
return query return query
} }
const fetchData = async (dataSource, query, limit, sortColumn, sortOrder) => { const fetchData = async (
dataSource,
query,
limit,
sortColumn,
sortOrder,
sortType
) => {
loading = true loading = true
if (dataSource?.type === "table") { if (dataSource?.type === "table") {
const res = await API.searchTable({ const res = await API.searchTable({
@ -95,9 +119,10 @@
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
}) })
pageNumber = 0 pageNumber = 0
rows = res.rows allRows = res.rows
// Check we have next data // Check we have next data
const next = await API.searchTable({ const next = await API.searchTable({
@ -107,6 +132,7 @@
bookmark: res.bookmark, bookmark: res.bookmark,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
}) })
if (next.rows?.length) { if (next.rows?.length) {
bookmarks = [null, res.bookmark] bookmarks = [null, res.bookmark]
@ -115,7 +141,7 @@
} }
} else { } else {
const rows = await API.fetchDatasource(dataSource) const rows = await API.fetchDatasource(dataSource)
rows = inMemoryFilterRows(rows, filter) allRows = inMemoryFilterRows(rows, filter)
} }
loading = false loading = false
loaded = true loaded = true
@ -123,9 +149,9 @@
const inMemoryFilterRows = (rows, filter) => { const inMemoryFilterRows = (rows, filter) => {
let filteredData = [...rows] let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => { Object.entries(filter || {}).forEach(([field, value]) => {
if (value != null && value !== "") { if (value != null && value !== "") {
filteredData = filteredData.filter((row) => { filteredData = filteredData.filter(row => {
return row[field] === value return row[field] === value
}) })
} }
@ -156,7 +182,7 @@
return rows.slice(0, numLimit) return rows.slice(0, numLimit)
} }
const getSchema = async (dataSource) => { const getSchema = async dataSource => {
if (dataSource?.schema) { if (dataSource?.schema) {
schema = dataSource.schema schema = dataSource.schema
} else if (dataSource?.tableId) { } else if (dataSource?.tableId) {
@ -196,9 +222,10 @@
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
}) })
pageNumber++ pageNumber++
rows = res.rows allRows = res.rows
// Check we have next data // Check we have next data
const next = await API.searchTable({ const next = await API.searchTable({
@ -208,6 +235,7 @@
bookmark: res.bookmark, bookmark: res.bookmark,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
}) })
if (next.rows?.length) { if (next.rows?.length) {
bookmarks[pageNumber + 1] = res.bookmark bookmarks[pageNumber + 1] = res.bookmark
@ -225,9 +253,10 @@
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
}) })
pageNumber-- pageNumber--
rows = res.rows allRows = res.rows
} }
</script> </script>

View File

@ -4559,7 +4559,7 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svelte@^3.37.0: svelte@^3.38.2:
version "3.38.2" version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg== integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==