Merge pull request #2790 from Budibase/data-ui-pagination
Data UI pagination and searching
This commit is contained in:
commit
38b32d78a8
|
@ -27,6 +27,7 @@
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let editColumnTitle = "Edit"
|
export let editColumnTitle = "Edit"
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
|
export let disableSorting = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset state when data changes
|
// Reset state when data changes
|
||||||
$: data.length, reset()
|
$: rows.length, reset()
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
nextScrollTop = 0
|
nextScrollTop = 0
|
||||||
scrollTop = 0
|
scrollTop = 0
|
||||||
|
@ -107,7 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||||
if (!sortColumn || !sortOrder) {
|
if (!sortColumn || !sortOrder || disableSorting) {
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
return rows.slice().sort((a, b) => {
|
return rows.slice().sort((a, b) => {
|
||||||
|
@ -131,6 +132,7 @@
|
||||||
sortColumn = fieldSchema.name
|
sortColumn = fieldSchema.name
|
||||||
sortOrder = "Descending"
|
sortOrder = "Descending"
|
||||||
}
|
}
|
||||||
|
dispatch("sort", { column: sortColumn, order: sortOrder })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDisplayName = schema => {
|
const getDisplayName = schema => {
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
// need the client lucene builder to convert to the structure API expects
|
// need the client lucene builder to convert to the structure API expects
|
||||||
import { buildLuceneQuery } from "../../../../../client/src/utils/lucene"
|
import { buildLuceneQuery } from "helpers/lucene"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables, views } from "stores/backend"
|
import { fade } from "svelte/transition"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
|
@ -8,72 +8,118 @@
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
import * as api from "./api"
|
import TableFilterButton from "./buttons/TableFilterButton.svelte"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
|
import { fetchTableData } from "helpers/fetchTableData"
|
||||||
|
import { Pagination } from "@budibase/bbui"
|
||||||
|
|
||||||
let hideAutocolumns = true
|
let hideAutocolumns = true
|
||||||
let data = []
|
|
||||||
let loading = false
|
|
||||||
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||||
$: title = $tables.selected?.name
|
|
||||||
$: schema = $tables.selected?.schema
|
$: schema = $tables.selected?.schema
|
||||||
$: tableView = {
|
|
||||||
schema,
|
|
||||||
name: $views.selected?.name,
|
|
||||||
}
|
|
||||||
$: type = $tables.selected?.type
|
$: type = $tables.selected?.type
|
||||||
$: isInternal = type !== "external"
|
$: isInternal = type !== "external"
|
||||||
|
$: id = $tables.selected?._id
|
||||||
|
$: search = searchTable(id)
|
||||||
|
$: columnOptions = Object.keys($search.schema || {})
|
||||||
|
|
||||||
// Fetch rows for specified table
|
// Fetches new data whenever the table changes
|
||||||
$: {
|
const searchTable = tableId => {
|
||||||
loading = true
|
return fetchTableData({
|
||||||
const loadingTableId = $tables.selected?._id
|
tableId,
|
||||||
api.fetchDataForTable($tables.selected?._id).then(rows => {
|
schema,
|
||||||
loading = false
|
limit: 10,
|
||||||
|
paginate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// If we started a slow request then quickly change table, sometimes
|
// Fetch data whenever sorting option changes
|
||||||
// the old data overwrites the new data.
|
const onSort = e => {
|
||||||
// This check ensures that we don't do that.
|
search.update({
|
||||||
if (loadingTableId !== $tables.selected?._id) {
|
sortColumn: e.detail.column,
|
||||||
return
|
sortOrder: e.detail.order,
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
data = rows || []
|
// Fetch data whenever filters change
|
||||||
|
const onFilter = e => {
|
||||||
|
search.update({
|
||||||
|
filters: e.detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever schema changes
|
||||||
|
const onUpdateColumns = () => {
|
||||||
|
search.update({
|
||||||
|
schema,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table
|
<div>
|
||||||
{title}
|
<Table
|
||||||
{schema}
|
title={$tables.selected?.name}
|
||||||
tableId={$tables.selected?._id}
|
{schema}
|
||||||
{data}
|
{type}
|
||||||
{type}
|
tableId={id}
|
||||||
allowEditing={true}
|
data={$search.rows}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
{loading}
|
loading={$search.loading}
|
||||||
>
|
on:sort={onSort}
|
||||||
{#if isInternal}
|
allowEditing
|
||||||
<CreateColumnButton />
|
disableSorting
|
||||||
{/if}
|
on:updatecolumns={onUpdateColumns}
|
||||||
{#if schema && Object.keys(schema).length > 0}
|
on:updaterows={search.refresh}
|
||||||
{#if !isUsersTable}
|
>
|
||||||
<CreateRowButton
|
|
||||||
title={"Create row"}
|
|
||||||
modalContentComponent={CreateEditRow}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
<CreateViewButton />
|
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
||||||
{/if}
|
{/if}
|
||||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
{#if schema && Object.keys(schema).length > 0}
|
||||||
{#if isUsersTable}
|
{#if !isUsersTable}
|
||||||
<EditRolesButton />
|
<CreateRowButton
|
||||||
|
on:updaterows={search.refresh}
|
||||||
|
title={"Create row"}
|
||||||
|
modalContentComponent={CreateEditRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if isInternal}
|
||||||
|
<CreateViewButton />
|
||||||
|
{/if}
|
||||||
|
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||||
|
{#if isUsersTable}
|
||||||
|
<EditRolesButton />
|
||||||
|
{/if}
|
||||||
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
|
<!-- always have the export last -->
|
||||||
|
<ExportButton view={$tables.selected?._id} />
|
||||||
|
{#key id}
|
||||||
|
<TableFilterButton {schema} on:change={onFilter} />
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
<HideAutocolumnButton bind:hideAutocolumns />
|
</Table>
|
||||||
<!-- always have the export last -->
|
{#key id}
|
||||||
<ExportButton view={$tables.selected?._id} />
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
{/if}
|
<div class="pagination">
|
||||||
</Table>
|
<Pagination
|
||||||
|
page={$search.pageNumber + 1}
|
||||||
|
hasPrevPage={$search.hasPrevPage}
|
||||||
|
hasNextPage={$search.hasNextPage}
|
||||||
|
goToPrevPage={$search.loading ? null : search.prevPage}
|
||||||
|
goToNextPage={$search.loading ? null : search.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Table, Modal, Heading, notifications } from "@budibase/bbui"
|
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||||
|
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||||
|
@ -21,6 +20,7 @@
|
||||||
export let hideAutocolumns
|
export let hideAutocolumns
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let type
|
export let type
|
||||||
|
export let disableSorting = false
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let editableColumn
|
let editableColumn
|
||||||
|
@ -98,47 +98,57 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<Layout noPadding gap="S">
|
||||||
<div class="table-title">
|
<div>
|
||||||
{#if title}
|
<div class="table-title">
|
||||||
<Heading size="S">{title}</Heading>
|
{#if title}
|
||||||
{/if}
|
<Heading size="S">{title}</Heading>
|
||||||
{#if loading}
|
{/if}
|
||||||
<div transition:fade>
|
{#if loading}
|
||||||
<Spinner size="10" />
|
<div transition:fade|local>
|
||||||
</div>
|
<Spinner size="10" />
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="popovers">
|
||||||
|
<slot />
|
||||||
|
{#if !isUsersTable && selectedRows.length > 0}
|
||||||
|
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="popovers">
|
{#key tableId}
|
||||||
<slot />
|
<div class="table-wrapper" in:fade={{ delay: 200, duration: 100 }}>
|
||||||
{#if !isUsersTable && selectedRows.length > 0}
|
<Table
|
||||||
<DeleteRowsButton {selectedRows} {deleteRows} />
|
{data}
|
||||||
{/if}
|
{schema}
|
||||||
</div>
|
{loading}
|
||||||
</div>
|
{customRenderers}
|
||||||
{#key tableId}
|
{rowCount}
|
||||||
<Table
|
{disableSorting}
|
||||||
{data}
|
bind:selectedRows
|
||||||
{schema}
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
{loading}
|
allowEditRows={allowEditing}
|
||||||
{customRenderers}
|
allowEditColumns={allowEditing && isInternal}
|
||||||
{rowCount}
|
showAutoColumns={!hideAutocolumns}
|
||||||
bind:selectedRows
|
on:editcolumn={e => editColumn(e.detail)}
|
||||||
allowSelectRows={allowEditing && !isUsersTable}
|
on:editrow={e => editRow(e.detail)}
|
||||||
allowEditRows={allowEditing}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
allowEditColumns={allowEditing && isInternal}
|
on:sort
|
||||||
showAutoColumns={!hideAutocolumns}
|
/>
|
||||||
on:editcolumn={e => editColumn(e.detail)}
|
</div>
|
||||||
on:editrow={e => editRow(e.detail)}
|
{/key}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
</Layout>
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<Modal bind:this={editRowModal}>
|
<Modal bind:this={editRowModal}>
|
||||||
<svelte:component this={editRowComponent} row={editableRow} />
|
<svelte:component this={editRowComponent} on:updaterows row={editableRow} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={editColumnModal}>
|
<Modal bind:this={editColumnModal}>
|
||||||
<CreateEditColumn field={editableColumn} onClosed={editColumnModal.hide} />
|
<CreateEditColumn
|
||||||
|
field={editableColumn}
|
||||||
|
on:updatecolumns
|
||||||
|
onClosed={editColumnModal.hide}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -152,6 +162,9 @@
|
||||||
.table-title > div {
|
.table-title > div {
|
||||||
margin-left: var(--spacing-xs);
|
margin-left: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
.table-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.popovers {
|
.popovers {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import CalculateButton from "./buttons/CalculateButton.svelte"
|
import CalculateButton from "./buttons/CalculateButton.svelte"
|
||||||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||||
import FilterButton from "./buttons/FilterButton.svelte"
|
import ViewFilterButton from "./buttons/ViewFilterButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
allowEditing={!view?.calculation}
|
allowEditing={!view?.calculation}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
<FilterButton {view} />
|
<ViewFilterButton {view} />
|
||||||
<CalculateButton {view} />
|
<CalculateButton {view} />
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
<GroupByButton {view} />
|
<GroupByButton {view} />
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
Create column
|
Create column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateEditColumn />
|
<CreateEditColumn on:updatecolumns />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -12,5 +12,5 @@
|
||||||
{title}
|
{title}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<svelte:component this={modalContentComponent} />
|
<svelte:component this={modalContentComponent} on:updaterows />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
export let selectedRows
|
export let selectedRows
|
||||||
export let deleteRows
|
export let deleteRows
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
async function confirmDeletion() {
|
async function confirmDeletion() {
|
||||||
await deleteRows()
|
await deleteRows()
|
||||||
modal?.hide()
|
modal?.hide()
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
export let filters
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let modal
|
||||||
|
let tempValue = filters || []
|
||||||
|
|
||||||
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon="Filter"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
active={tempValue?.length > 0}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</ActionButton>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent
|
||||||
|
title="Filter"
|
||||||
|
confirmText="Save"
|
||||||
|
size="XL"
|
||||||
|
onConfirm={() => dispatch("change", tempValue)}
|
||||||
|
>
|
||||||
|
<div class="wrapper">
|
||||||
|
<FilterDrawer
|
||||||
|
allowBindings={false}
|
||||||
|
bind:filters={tempValue}
|
||||||
|
{schemaFields}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper :global(.main) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -10,6 +10,7 @@
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Context,
|
Context,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
|
@ -30,8 +31,9 @@
|
||||||
const AUTO_TYPE = "auto"
|
const AUTO_TYPE = "auto"
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
const dispatch = createEventDispatcher()
|
||||||
const { hide } = getContext(Context.Modal)
|
const { hide } = getContext(Context.Modal)
|
||||||
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
|
|
||||||
export let field = {
|
export let field = {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -81,12 +83,13 @@
|
||||||
if (field.type === AUTO_TYPE) {
|
if (field.type === AUTO_TYPE) {
|
||||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||||
}
|
}
|
||||||
tables.saveField({
|
await tables.saveField({
|
||||||
originalName,
|
originalName,
|
||||||
field,
|
field,
|
||||||
primaryDisplay,
|
primaryDisplay,
|
||||||
indexes,
|
indexes,
|
||||||
})
|
})
|
||||||
|
dispatch("updatecolumns")
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteColumn() {
|
function deleteColumn() {
|
||||||
|
@ -99,6 +102,7 @@
|
||||||
hide()
|
hide()
|
||||||
deletion = false
|
deletion = false
|
||||||
}
|
}
|
||||||
|
dispatch("updatecolumns")
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeChange(event) {
|
function handleTypeChange(event) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables, rows } from "stores/backend"
|
import { tables, rows } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
let errors = []
|
let errors = []
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: creating = row?._id == null
|
$: creating = row?._id == null
|
||||||
$: table = row.tableId
|
$: table = row.tableId
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
|
|
||||||
notifications.success("Row saved successfully.")
|
notifications.success("Row saved successfully.")
|
||||||
rows.save(rowResponse)
|
rows.save(rowResponse)
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables, rows } from "stores/backend"
|
import { tables, rows } from "stores/backend"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
|
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let errors = []
|
let errors = []
|
||||||
|
|
||||||
$: creating = row?._id == null
|
$: creating = row?._id == null
|
||||||
|
@ -71,6 +73,7 @@
|
||||||
|
|
||||||
notifications.success("User saved successfully")
|
notifications.success("User saved successfully")
|
||||||
rows.save(rowResponse)
|
rows.save(rowResponse)
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { BUDIBASE_INTERNAL_DB } from "constants"
|
import { BUDIBASE_INTERNAL_DB } from "constants"
|
||||||
import { database, datasources, queries, tables } from "stores/backend"
|
import { database, datasources, queries, tables, views } from "stores/backend"
|
||||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
@ -11,16 +11,27 @@
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
$: enrichedDataSources = $datasources.list.map(datasource => ({
|
$: enrichedDataSources = $datasources.list.map(datasource => {
|
||||||
...datasource,
|
const selected = $datasources.selected === datasource._id
|
||||||
open:
|
const open = openDataSources.includes(datasource._id)
|
||||||
openDataSources.includes(datasource._id) ||
|
const containsSelected = containsActiveEntity(datasource)
|
||||||
containsActiveTable(datasource),
|
return {
|
||||||
selected: $datasources.selected === datasource._id,
|
...datasource,
|
||||||
}))
|
selected,
|
||||||
|
open: selected || open || containsSelected,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||||
|
$: {
|
||||||
|
// Ensure the open data source is always included in the list of open
|
||||||
|
// data sources
|
||||||
|
if (openDataSource) {
|
||||||
|
openNode(openDataSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectDatasource(datasource) {
|
function selectDatasource(datasource) {
|
||||||
toggleNode(datasource)
|
openNode(datasource)
|
||||||
datasources.select(datasource._id)
|
datasources.select(datasource._id)
|
||||||
$goto(`./datasource/${datasource._id}`)
|
$goto(`./datasource/${datasource._id}`)
|
||||||
}
|
}
|
||||||
|
@ -30,12 +41,22 @@
|
||||||
$goto(`./datasource/${query.datasourceId}/${query._id}`)
|
$goto(`./datasource/${query.datasourceId}/${query._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeNode(datasource) {
|
||||||
|
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNode(datasource) {
|
||||||
|
if (!openDataSources.includes(datasource._id)) {
|
||||||
|
openDataSources = [...openDataSources, datasource._id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleNode(datasource) {
|
function toggleNode(datasource) {
|
||||||
const isOpen = openDataSources.includes(datasource._id)
|
const isOpen = openDataSources.includes(datasource._id)
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
closeNode(datasource)
|
||||||
} else {
|
} else {
|
||||||
openDataSources = [...openDataSources, datasource._id]
|
openNode(datasource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,16 +65,35 @@
|
||||||
queries.fetch()
|
queries.fetch()
|
||||||
})
|
})
|
||||||
|
|
||||||
const containsActiveTable = datasource => {
|
const containsActiveEntity = datasource => {
|
||||||
const activeTableId = get(tables).selected?._id
|
// If we're view a query then the data source ID is in the URL
|
||||||
|
if ($params.selectedDatasource === datasource._id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no entities it can't contain anything
|
||||||
if (!datasource.entities) {
|
if (!datasource.entities) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let tableOptions = datasource.entities
|
|
||||||
if (!Array.isArray(tableOptions)) {
|
// Get a list of table options
|
||||||
tableOptions = Object.values(tableOptions)
|
let options = datasource.entities
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
options = Object.values(options)
|
||||||
}
|
}
|
||||||
return tableOptions.find(x => x._id === activeTableId) != null
|
|
||||||
|
// Check for a matching table
|
||||||
|
if ($params.selectedTable) {
|
||||||
|
const selectedTable = get(tables).selected?._id
|
||||||
|
return options.find(x => x._id === selectedTable) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching view
|
||||||
|
const selectedView = get(views).selected?.name
|
||||||
|
const table = options.find(table => {
|
||||||
|
return table.views?.[selectedView] != null
|
||||||
|
})
|
||||||
|
return table != null
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
import { dndzone } from "svelte-dnd-action"
|
import { dndzone } from "svelte-dnd-action"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
|
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
|
||||||
import { selectedComponent, store } from "builderStore"
|
import { selectedComponent, store } from "builderStore"
|
||||||
import { getComponentForSettingType } from "./componentSettings"
|
import { getComponentForSettingType } from "./componentSettings"
|
||||||
import PropertyControl from "./PropertyControl.svelte"
|
import PropertyControl from "./PropertyControl.svelte"
|
||||||
|
|
|
@ -13,18 +13,20 @@
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { getValidOperatorsForType, OperatorOptions } from "helpers/lucene"
|
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let panel = BindingPanel
|
export let panel = BindingPanel
|
||||||
|
export let allowBindings = true
|
||||||
|
|
||||||
const BannedTypes = ["link", "attachment", "formula"]
|
const BannedTypes = ["link", "attachment", "formula"]
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
.filter(field => !BannedTypes.includes(field.type))
|
.filter(field => !BannedTypes.includes(field.type))
|
||||||
.map(field => field.name)
|
.map(field => field.name)
|
||||||
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
filters = [
|
filters = [
|
||||||
|
@ -93,7 +95,7 @@
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{#if !filters?.length}
|
{#if !filters?.length}
|
||||||
Add your first filter column.
|
Add your first filter expression.
|
||||||
{:else}
|
{:else}
|
||||||
Results are filtered to only those which match all of the following
|
Results are filtered to only those which match all of the following
|
||||||
constraints.
|
constraints.
|
||||||
|
@ -117,7 +119,7 @@
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
disabled={filter.noValue || !filter.field}
|
disabled={filter.noValue || !filter.field}
|
||||||
options={["Value", "Binding"]}
|
options={valueTypeOptions}
|
||||||
bind:value={filter.valueType}
|
bind:value={filter.valueType}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
$: internalTable = dataSource?.type === "table"
|
|
||||||
|
|
||||||
const saveFilter = async () => {
|
const saveFilter = async () => {
|
||||||
dispatch("change", tempValue)
|
dispatch("change", tempValue)
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Operator options for lucene queries
|
||||||
|
*/
|
||||||
|
export const OperatorOptions = {
|
||||||
|
Equals: {
|
||||||
|
value: "equal",
|
||||||
|
label: "Equals",
|
||||||
|
},
|
||||||
|
NotEquals: {
|
||||||
|
value: "notEqual",
|
||||||
|
label: "Not equals",
|
||||||
|
},
|
||||||
|
Empty: {
|
||||||
|
value: "empty",
|
||||||
|
label: "Is empty",
|
||||||
|
},
|
||||||
|
NotEmpty: {
|
||||||
|
value: "notEmpty",
|
||||||
|
label: "Is not empty",
|
||||||
|
},
|
||||||
|
StartsWith: {
|
||||||
|
value: "string",
|
||||||
|
label: "Starts with",
|
||||||
|
},
|
||||||
|
Like: {
|
||||||
|
value: "fuzzy",
|
||||||
|
label: "Like",
|
||||||
|
},
|
||||||
|
MoreThan: {
|
||||||
|
value: "rangeLow",
|
||||||
|
label: "More than",
|
||||||
|
},
|
||||||
|
LessThan: {
|
||||||
|
value: "rangeHigh",
|
||||||
|
label: "Less than",
|
||||||
|
},
|
||||||
|
Contains: {
|
||||||
|
value: "equal",
|
||||||
|
label: "Contains",
|
||||||
|
},
|
||||||
|
NotContains: {
|
||||||
|
value: "notEqual",
|
||||||
|
label: "Does Not Contain",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the valid operator options for a certain data type
|
||||||
|
* @param type the data type
|
||||||
|
*/
|
||||||
|
export const getValidOperatorsForType = type => {
|
||||||
|
const Op = OperatorOptions
|
||||||
|
if (type === "string") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.StartsWith,
|
||||||
|
Op.Like,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "number") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.MoreThan,
|
||||||
|
Op.LessThan,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "options") {
|
||||||
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "array") {
|
||||||
|
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "boolean") {
|
||||||
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "longform") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.StartsWith,
|
||||||
|
Op.Like,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "datetime") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.MoreThan,
|
||||||
|
Op.LessThan,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
// Do not use any aliased imports in common files, as these will be bundled
|
||||||
|
// by multiple bundlers which may not be able to resolve them
|
||||||
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
import * as API from "../builderStore/api"
|
||||||
|
import { buildLuceneQuery } from "./lucene"
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
tableId: null,
|
||||||
|
filters: null,
|
||||||
|
limit: 10,
|
||||||
|
sortColumn: null,
|
||||||
|
sortOrder: "ascending",
|
||||||
|
paginate: true,
|
||||||
|
schema: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTableData = opts => {
|
||||||
|
// Save option set so we can override it later rather than relying on params
|
||||||
|
let options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...opts,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local non-observable state
|
||||||
|
let query
|
||||||
|
let sortType
|
||||||
|
let lastBookmark
|
||||||
|
|
||||||
|
// Local observable state
|
||||||
|
const store = writable({
|
||||||
|
rows: [],
|
||||||
|
schema: null,
|
||||||
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
|
bookmarks: [],
|
||||||
|
pageNumber: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Derive certain properties to return
|
||||||
|
const derivedStore = derived(store, $store => {
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null,
|
||||||
|
hasPrevPage: $store.pageNumber > 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchPage = async bookmark => {
|
||||||
|
lastBookmark = bookmark
|
||||||
|
const { tableId, limit, sortColumn, sortOrder, paginate } = options
|
||||||
|
store.update($store => ({ ...$store, loading: true }))
|
||||||
|
const res = await API.post(`/api/${options.tableId}/search`, {
|
||||||
|
tableId,
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
sort: sortColumn,
|
||||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
|
sortType,
|
||||||
|
paginate,
|
||||||
|
bookmark,
|
||||||
|
})
|
||||||
|
store.update($store => ({ ...$store, loading: false, loaded: true }))
|
||||||
|
return await res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches a fresh set of results from the server
|
||||||
|
const fetchData = async () => {
|
||||||
|
const { tableId, schema, sortColumn, filters } = options
|
||||||
|
|
||||||
|
// Ensure table ID exists
|
||||||
|
if (!tableId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and enrich schema.
|
||||||
|
// Ensure there are "name" properties for all fields and that field schema
|
||||||
|
// are objects
|
||||||
|
let enrichedSchema = schema
|
||||||
|
if (!enrichedSchema) {
|
||||||
|
const definition = await API.get(`/api/tables/${tableId}`)
|
||||||
|
enrichedSchema = definition?.schema ?? null
|
||||||
|
}
|
||||||
|
if (enrichedSchema) {
|
||||||
|
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
|
||||||
|
if (typeof fieldSchema === "string") {
|
||||||
|
enrichedSchema[fieldName] = {
|
||||||
|
type: fieldSchema,
|
||||||
|
name: fieldName,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enrichedSchema[fieldName] = {
|
||||||
|
...fieldSchema,
|
||||||
|
name: fieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save fixed schema so we can provide it later
|
||||||
|
options.schema = enrichedSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure schema exists
|
||||||
|
if (!schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.update($store => ({ ...$store, schema }))
|
||||||
|
|
||||||
|
// Work out what sort type to use
|
||||||
|
if (!sortColumn || !schema[sortColumn]) {
|
||||||
|
sortType = "string"
|
||||||
|
}
|
||||||
|
const type = schema?.[sortColumn]?.type
|
||||||
|
sortType = type === "number" ? "number" : "string"
|
||||||
|
|
||||||
|
// Build the lucene query
|
||||||
|
query = buildLuceneQuery(filters)
|
||||||
|
|
||||||
|
// Actually fetch data
|
||||||
|
const page = await fetchPage()
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
loading: false,
|
||||||
|
loaded: true,
|
||||||
|
pageNumber: 0,
|
||||||
|
rows: page.rows,
|
||||||
|
bookmarks: page.hasNextPage ? [null, page.bookmark] : [null],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the next page of data
|
||||||
|
const nextPage = async () => {
|
||||||
|
const state = get(derivedStore)
|
||||||
|
if (state.loading || !options.paginate || !state.hasNextPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch next page
|
||||||
|
const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
store.update($store => {
|
||||||
|
let { bookmarks, pageNumber } = $store
|
||||||
|
if (page.hasNextPage) {
|
||||||
|
bookmarks[pageNumber + 2] = page.bookmark
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
pageNumber: pageNumber + 1,
|
||||||
|
rows: page.rows,
|
||||||
|
bookmarks,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the previous page of data
|
||||||
|
const prevPage = async () => {
|
||||||
|
const state = get(derivedStore)
|
||||||
|
if (state.loading || !options.paginate || !state.hasPrevPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch previous page
|
||||||
|
const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
store.update($store => {
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
pageNumber: $store.pageNumber - 1,
|
||||||
|
rows: page.rows,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resets the data set and updates options
|
||||||
|
const update = async newOptions => {
|
||||||
|
if (newOptions) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
...newOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the same page again
|
||||||
|
const refresh = async () => {
|
||||||
|
if (get(store).loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const page = await fetchPage(lastBookmark)
|
||||||
|
store.update($store => ({ ...$store, rows: page.rows }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initially fetch data but don't bother waiting for the result
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
// Return our derived store which will be updated over time
|
||||||
|
return {
|
||||||
|
subscribe: derivedStore.subscribe,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
update,
|
||||||
|
refresh,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,90 +1,179 @@
|
||||||
export const OperatorOptions = {
|
/**
|
||||||
Equals: {
|
* Builds a lucene JSON query from the filter structure generated in the builder
|
||||||
value: "equal",
|
* @param filter the builder filter structure
|
||||||
label: "Equals",
|
*/
|
||||||
},
|
export const buildLuceneQuery = filter => {
|
||||||
NotEquals: {
|
let query = {
|
||||||
value: "notEqual",
|
string: {},
|
||||||
label: "Not equals",
|
fuzzy: {},
|
||||||
},
|
range: {},
|
||||||
Empty: {
|
equal: {},
|
||||||
value: "empty",
|
notEqual: {},
|
||||||
label: "Is empty",
|
empty: {},
|
||||||
},
|
notEmpty: {},
|
||||||
NotEmpty: {
|
contains: {},
|
||||||
value: "notEmpty",
|
notContains: {},
|
||||||
label: "Is not empty",
|
}
|
||||||
},
|
if (Array.isArray(filter)) {
|
||||||
StartsWith: {
|
filter.forEach(expression => {
|
||||||
value: "string",
|
let { operator, field, type, value } = expression
|
||||||
label: "Starts with",
|
// Parse all values into correct types
|
||||||
},
|
if (type === "datetime" && value) {
|
||||||
Like: {
|
value = new Date(value).toISOString()
|
||||||
value: "fuzzy",
|
}
|
||||||
label: "Like",
|
if (type === "number") {
|
||||||
},
|
value = parseFloat(value)
|
||||||
MoreThan: {
|
}
|
||||||
value: "rangeLow",
|
if (type === "boolean") {
|
||||||
label: "More than",
|
value = `${value}`?.toLowerCase() === "true"
|
||||||
},
|
}
|
||||||
LessThan: {
|
if (operator.startsWith("range")) {
|
||||||
value: "rangeHigh",
|
if (!query.range[field]) {
|
||||||
label: "Less than",
|
query.range[field] = {
|
||||||
},
|
low:
|
||||||
Contains: {
|
type === "number"
|
||||||
value: "equal",
|
? Number.MIN_SAFE_INTEGER
|
||||||
label: "Contains",
|
: "0000-00-00T00:00:00.000Z",
|
||||||
},
|
high:
|
||||||
NotContains: {
|
type === "number"
|
||||||
value: "notEqual",
|
? Number.MAX_SAFE_INTEGER
|
||||||
label: "Does Not Contain",
|
: "9999-00-00T00:00:00.000Z",
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
if (operator === "rangeLow" && value != null && value !== "") {
|
||||||
|
query.range[field].low = value
|
||||||
|
} else if (operator === "rangeHigh" && value != null && value !== "") {
|
||||||
|
query.range[field].high = value
|
||||||
|
}
|
||||||
|
} else if (query[operator]) {
|
||||||
|
if (type === "boolean") {
|
||||||
|
// Transform boolean filters to cope with null.
|
||||||
|
// "equals false" needs to be "not equals true"
|
||||||
|
// "not equals false" needs to be "equals true"
|
||||||
|
if (operator === "equal" && value === false) {
|
||||||
|
query.notEqual[field] = true
|
||||||
|
} else if (operator === "notEqual" && value === false) {
|
||||||
|
query.equal[field] = true
|
||||||
|
} else {
|
||||||
|
query[operator][field] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query[operator][field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getValidOperatorsForType = type => {
|
/**
|
||||||
const Op = OperatorOptions
|
* Performs a client-side lucene search on an array of data
|
||||||
if (type === "string") {
|
* @param docs the data
|
||||||
return [
|
* @param query the JSON lucene query
|
||||||
Op.Equals,
|
*/
|
||||||
Op.NotEquals,
|
export const luceneQuery = (docs, query) => {
|
||||||
Op.StartsWith,
|
if (!query) {
|
||||||
Op.Like,
|
return docs
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "number") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.MoreThan,
|
|
||||||
Op.LessThan,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "options") {
|
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "array") {
|
|
||||||
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "boolean") {
|
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "longform") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.StartsWith,
|
|
||||||
Op.Like,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "datetime") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.MoreThan,
|
|
||||||
Op.LessThan,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
return []
|
|
||||||
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||||
|
const match = (type, failFn) => doc => {
|
||||||
|
const filters = Object.entries(query[type] || {})
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
if (failFn(filters[i][0], filters[i][1], doc)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a string match (fails if the value does not start with the string)
|
||||||
|
const stringMatch = match("string", (key, value, doc) => {
|
||||||
|
return !doc[key] || !doc[key].startsWith(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
|
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
||||||
|
return !doc[key] || !doc[key].startsWith(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a range match
|
||||||
|
const rangeMatch = match("range", (key, value, doc) => {
|
||||||
|
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process an equal match (fails if the value is different)
|
||||||
|
const equalMatch = match("equal", (key, value, doc) => {
|
||||||
|
return value != null && value !== "" && doc[key] !== value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a not-equal match (fails if the value is the same)
|
||||||
|
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
||||||
|
return value != null && value !== "" && doc[key] === value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process an empty match (fails if the value is not empty)
|
||||||
|
const emptyMatch = match("empty", (key, value, doc) => {
|
||||||
|
return doc[key] != null && doc[key] !== ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a not-empty match (fails is the value is empty)
|
||||||
|
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
||||||
|
return doc[key] == null || doc[key] === ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// Match a document against all criteria
|
||||||
|
const docMatch = doc => {
|
||||||
|
return (
|
||||||
|
stringMatch(doc) &&
|
||||||
|
fuzzyMatch(doc) &&
|
||||||
|
rangeMatch(doc) &&
|
||||||
|
equalMatch(doc) &&
|
||||||
|
notEqualMatch(doc) &&
|
||||||
|
emptyMatch(doc) &&
|
||||||
|
notEmptyMatch(doc)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all docs
|
||||||
|
return docs.filter(docMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a client-side sort from the equivalent server-side lucene sort
|
||||||
|
* parameters.
|
||||||
|
* @param docs the data
|
||||||
|
* @param sort the sort column
|
||||||
|
* @param sortOrder the sort order ("ascending" or "descending")
|
||||||
|
* @param sortType the type of sort ("string" or "number")
|
||||||
|
*/
|
||||||
|
export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
|
||||||
|
if (!sort || !sortOrder || !sortType) {
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
|
||||||
|
return docs.slice().sort((a, b) => {
|
||||||
|
const colA = parse(a[sort])
|
||||||
|
const colB = parse(b[sort])
|
||||||
|
if (sortOrder === "Descending") {
|
||||||
|
return colA > colB ? -1 : 1
|
||||||
|
} else {
|
||||||
|
return colA > colB ? 1 : -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limits the specified docs to the specified number of rows from the equivalent
|
||||||
|
* server-side lucene limit parameters.
|
||||||
|
* @param docs the data
|
||||||
|
* @param limit the number of docs to limit to
|
||||||
|
*/
|
||||||
|
export const luceneLimit = (docs, limit) => {
|
||||||
|
const numLimit = parseFloat(limit)
|
||||||
|
if (isNaN(numLimit)) {
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
return docs.slice(0, numLimit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const suppressWarnings = warnings => {
|
||||||
|
if (!warnings?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const regex = new RegExp(warnings.map(x => `(${x})`).join("|"), "gi")
|
||||||
|
const warn = console.warn
|
||||||
|
console.warn = (...params) => {
|
||||||
|
const msg = params[0]
|
||||||
|
if (msg && typeof msg === "string") {
|
||||||
|
if (msg.match(regex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warn(...params)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,19 @@ import "@spectrum-css/vars/dist/spectrum-light.css"
|
||||||
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
||||||
import "@spectrum-css/page/dist/index-vars.css"
|
import "@spectrum-css/page/dist/index-vars.css"
|
||||||
import "./global.css"
|
import "./global.css"
|
||||||
|
import { suppressWarnings } from "./helpers/warnings"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||||
|
import App from "./App.svelte"
|
||||||
|
|
||||||
|
// Init spectrum icons
|
||||||
loadSpectrumIcons()
|
loadSpectrumIcons()
|
||||||
|
|
||||||
import App from "./App.svelte"
|
// Suppress svelte runtime warnings
|
||||||
|
suppressWarnings([
|
||||||
|
"was created with unknown prop",
|
||||||
|
"was created without expected prop",
|
||||||
|
"received an unexpected slot",
|
||||||
|
])
|
||||||
|
|
||||||
export default new App({
|
export default new App({
|
||||||
target: document.getElementById("app"),
|
target: document.getElementById("app"),
|
||||||
|
|
|
@ -1 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
import { queries } from "stores/backend"
|
||||||
|
|
||||||
|
if ($params.query) {
|
||||||
|
const query = $queries.list.find(q => q._id === $params.query)
|
||||||
|
if (query) {
|
||||||
|
queries.select(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
if ($params.selectedDatasource) {
|
if ($params.selectedDatasource && !$params.query) {
|
||||||
const datasource = $datasources.list.find(
|
const datasource = $datasources.list.find(
|
||||||
m => m._id === $params.selectedDatasource
|
m => m._id === $params.selectedDatasource
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1 @@
|
||||||
<script>
|
|
||||||
import { datasources } from "stores/backend"
|
|
||||||
import { goto, leftover } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// navigate to first datasource in list, if not already selected
|
|
||||||
if (!$leftover && $datasources.list.length > 0 && !$datasources.selected) {
|
|
||||||
$goto(`./${$datasources.list[0]._id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import { params } from "@roxi/routify"
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
|
|
||||||
if ($params.selectedTable) {
|
|
||||||
const table = $tables.list.find(m => m._id === $params.selectedTable)
|
|
||||||
if (table) {
|
|
||||||
tables.select(table)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
|
|
||||||
import { tables, database } from "stores/backend"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $database?._id && $tables?.selected?.name}
|
|
||||||
<TableDataTable />
|
|
||||||
{:else}<i>Create your first table to start building</i>{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
i {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: var(--grey-5);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script>
|
|
||||||
import { params } from "@roxi/routify"
|
|
||||||
import RelationshipDataTable from "components/backend/DataTable/RelationshipDataTable.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<RelationshipDataTable
|
|
||||||
tableId={$params.selectedTable}
|
|
||||||
rowId={$params.selectedRow}
|
|
||||||
fieldName={decodeURI($params.selectedField)}
|
|
||||||
/>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
$goto("../../")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- routify:options index=false -->
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
$goto("../")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- routify:options index=false -->
|
|
|
@ -1,19 +0,0 @@
|
||||||
<script>
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
import { goto, leftover } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// navigate to first table in list, if not already selected
|
|
||||||
// and this is the final url (i.e. no selectedTable)
|
|
||||||
if (
|
|
||||||
!$leftover &&
|
|
||||||
$tables.list.length > 0
|
|
||||||
// (!$tables.selected || !$tables.selected._id)
|
|
||||||
) {
|
|
||||||
$goto(`./${$tables.list[0]._id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
$tables.list.length > 0 && $goto(`./${$tables.list[0]._id}`)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $tables.list.length === 0}
|
|
||||||
<i>Create your first table to start building</i>
|
|
||||||
{:else}<i>Select a table to edit</i>{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
i {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: var(--grey-5);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { datasources, integrations, tables } from "./"
|
import { datasources, integrations, tables, views } from "./"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
export function createQueriesStore() {
|
export function createQueriesStore() {
|
||||||
|
@ -55,10 +55,9 @@ export function createQueriesStore() {
|
||||||
},
|
},
|
||||||
select: query => {
|
select: query => {
|
||||||
update(state => ({ ...state, selected: query._id }))
|
update(state => ({ ...state, selected: query._id }))
|
||||||
tables.update(state => ({
|
views.unselect()
|
||||||
...state,
|
tables.unselect()
|
||||||
selected: null,
|
datasources.unselect()
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
unselect: () => {
|
unselect: () => {
|
||||||
update(state => ({ ...state, selected: null }))
|
update(state => ({ ...state, selected: null }))
|
||||||
|
|
|
@ -95,7 +95,13 @@ export function createTablesStore() {
|
||||||
selected: {},
|
selected: {},
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
|
saveField: async ({
|
||||||
|
originalName,
|
||||||
|
field,
|
||||||
|
primaryDisplay = false,
|
||||||
|
indexes,
|
||||||
|
}) => {
|
||||||
|
let promise
|
||||||
update(state => {
|
update(state => {
|
||||||
// delete the original if renaming
|
// delete the original if renaming
|
||||||
// need to handle if the column had no name, empty string
|
// need to handle if the column had no name, empty string
|
||||||
|
@ -126,9 +132,12 @@ export function createTablesStore() {
|
||||||
...state.draft.schema,
|
...state.draft.schema,
|
||||||
[field.name]: cloneDeep(field),
|
[field.name]: cloneDeep(field),
|
||||||
}
|
}
|
||||||
save(state.draft)
|
promise = save(state.draft)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteField: field => {
|
deleteField: field => {
|
||||||
update(state => {
|
update(state => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function createViewsStore() {
|
||||||
...state,
|
...state,
|
||||||
selected: view,
|
selected: view,
|
||||||
}))
|
}))
|
||||||
|
tables.unselect()
|
||||||
queries.unselect()
|
queries.unselect()
|
||||||
datasources.unselect()
|
datasources.unselect()
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,6 +58,10 @@ export default {
|
||||||
find: "sdk",
|
find: "sdk",
|
||||||
replacement: path.resolve("./src/sdk"),
|
replacement: path.resolve("./src/sdk"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
find: "builder",
|
||||||
|
replacement: path.resolve("../builder"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
svelte({
|
svelte({
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import ErrorSVG from "../../../builder/assets/error.svg"
|
import ErrorSVG from "builder/assets/error.svg"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
luceneQuery,
|
luceneQuery,
|
||||||
luceneSort,
|
luceneSort,
|
||||||
luceneLimit,
|
luceneLimit,
|
||||||
} from "utils/lucene"
|
} from "builder/src/helpers/lucene"
|
||||||
import Placeholder from "./Placeholder.svelte"
|
import Placeholder from "./Placeholder.svelte"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
|
import { localStorageStore } from "builder/src/builderStore/store/localStorage"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
|
||||||
const createStateStore = () => {
|
const createStateStore = () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { buildLuceneQuery, luceneQuery } from "./lucene"
|
import { buildLuceneQuery, luceneQuery } from "builder/src/helpers/lucene"
|
||||||
|
|
||||||
export const getActiveConditions = conditions => {
|
export const getActiveConditions = conditions => {
|
||||||
if (!conditions?.length) {
|
if (!conditions?.length) {
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
/**
|
|
||||||
* Builds a lucene JSON query from the filter structure generated in the builder
|
|
||||||
* @param filter the builder filter structure
|
|
||||||
*/
|
|
||||||
export const buildLuceneQuery = filter => {
|
|
||||||
let query = {
|
|
||||||
string: {},
|
|
||||||
fuzzy: {},
|
|
||||||
range: {},
|
|
||||||
equal: {},
|
|
||||||
notEqual: {},
|
|
||||||
empty: {},
|
|
||||||
notEmpty: {},
|
|
||||||
contains: {},
|
|
||||||
notContains: {},
|
|
||||||
}
|
|
||||||
if (Array.isArray(filter)) {
|
|
||||||
filter.forEach(expression => {
|
|
||||||
let { operator, field, type, value } = expression
|
|
||||||
// Parse all values into correct types
|
|
||||||
if (type === "datetime" && value) {
|
|
||||||
value = new Date(value).toISOString()
|
|
||||||
}
|
|
||||||
if (type === "number") {
|
|
||||||
value = parseFloat(value)
|
|
||||||
}
|
|
||||||
if (type === "boolean") {
|
|
||||||
value = `${value}`?.toLowerCase() === "true"
|
|
||||||
}
|
|
||||||
if (operator.startsWith("range")) {
|
|
||||||
if (!query.range[field]) {
|
|
||||||
query.range[field] = {
|
|
||||||
low:
|
|
||||||
type === "number"
|
|
||||||
? Number.MIN_SAFE_INTEGER
|
|
||||||
: "0000-00-00T00:00:00.000Z",
|
|
||||||
high:
|
|
||||||
type === "number"
|
|
||||||
? Number.MAX_SAFE_INTEGER
|
|
||||||
: "9999-00-00T00:00:00.000Z",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (operator === "rangeLow" && value != null && value !== "") {
|
|
||||||
query.range[field].low = value
|
|
||||||
} else if (operator === "rangeHigh" && value != null && value !== "") {
|
|
||||||
query.range[field].high = value
|
|
||||||
}
|
|
||||||
} else if (query[operator]) {
|
|
||||||
if (type === "boolean") {
|
|
||||||
// Transform boolean filters to cope with null.
|
|
||||||
// "equals false" needs to be "not equals true"
|
|
||||||
// "not equals false" needs to be "equals true"
|
|
||||||
if (operator === "equal" && value === false) {
|
|
||||||
query.notEqual[field] = true
|
|
||||||
} else if (operator === "notEqual" && value === false) {
|
|
||||||
query.equal[field] = true
|
|
||||||
} else {
|
|
||||||
query[operator][field] = value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query[operator][field] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a client-side lucene search on an array of data
|
|
||||||
* @param docs the data
|
|
||||||
* @param query the JSON lucene query
|
|
||||||
*/
|
|
||||||
export const luceneQuery = (docs, query) => {
|
|
||||||
if (!query) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterates over a set of filters and evaluates a fail function against a doc
|
|
||||||
const match = (type, failFn) => doc => {
|
|
||||||
const filters = Object.entries(query[type] || {})
|
|
||||||
for (let i = 0; i < filters.length; i++) {
|
|
||||||
if (failFn(filters[i][0], filters[i][1], doc)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a string match (fails if the value does not start with the string)
|
|
||||||
const stringMatch = match("string", (key, value, doc) => {
|
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
|
||||||
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a range match
|
|
||||||
const rangeMatch = match("range", (key, value, doc) => {
|
|
||||||
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process an equal match (fails if the value is different)
|
|
||||||
const equalMatch = match("equal", (key, value, doc) => {
|
|
||||||
return value != null && value !== "" && doc[key] !== value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a not-equal match (fails if the value is the same)
|
|
||||||
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
|
||||||
return value != null && value !== "" && doc[key] === value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
|
||||||
const emptyMatch = match("empty", (key, value, doc) => {
|
|
||||||
return doc[key] != null && doc[key] !== ""
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
|
||||||
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
|
||||||
return doc[key] == null || doc[key] === ""
|
|
||||||
})
|
|
||||||
|
|
||||||
// Match a document against all criteria
|
|
||||||
const docMatch = doc => {
|
|
||||||
return (
|
|
||||||
stringMatch(doc) &&
|
|
||||||
fuzzyMatch(doc) &&
|
|
||||||
rangeMatch(doc) &&
|
|
||||||
equalMatch(doc) &&
|
|
||||||
notEqualMatch(doc) &&
|
|
||||||
emptyMatch(doc) &&
|
|
||||||
notEmptyMatch(doc)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all docs
|
|
||||||
return docs.filter(docMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a client-side sort from the equivalent server-side lucene sort
|
|
||||||
* parameters.
|
|
||||||
* @param docs the data
|
|
||||||
* @param sort the sort column
|
|
||||||
* @param sortOrder the sort order ("ascending" or "descending")
|
|
||||||
* @param sortType the type of sort ("string" or "number")
|
|
||||||
*/
|
|
||||||
export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
|
|
||||||
if (!sort || !sortOrder || !sortType) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
|
|
||||||
return docs.slice().sort((a, b) => {
|
|
||||||
const colA = parse(a[sort])
|
|
||||||
const colB = parse(b[sort])
|
|
||||||
if (sortOrder === "Descending") {
|
|
||||||
return colA > colB ? -1 : 1
|
|
||||||
} else {
|
|
||||||
return colA > colB ? 1 : -1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limits the specified docs to the specified number of rows from the equivalent
|
|
||||||
* server-side lucene limit parameters.
|
|
||||||
* @param docs the data
|
|
||||||
* @param limit the number of docs to limit to
|
|
||||||
*/
|
|
||||||
export const luceneLimit = (docs, limit) => {
|
|
||||||
const numLimit = parseFloat(limit)
|
|
||||||
if (isNaN(numLimit)) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
return docs.slice(0, numLimit)
|
|
||||||
}
|
|
|
@ -351,6 +351,11 @@ exports.bulkDestroy = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.search = async ctx => {
|
exports.search = async ctx => {
|
||||||
|
// Fetch the whole table when running in cypress, as search doesn't work
|
||||||
|
if (env.isCypress()) {
|
||||||
|
return { rows: await exports.fetch(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const { tableId } = ctx.params
|
const { tableId } = ctx.params
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
|
|
@ -13,6 +13,10 @@ function isDev() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCypress() {
|
||||||
|
return process.env.NODE_ENV === "cypress"
|
||||||
|
}
|
||||||
|
|
||||||
let LOADED = false
|
let LOADED = false
|
||||||
if (!LOADED && isDev() && !isTest()) {
|
if (!LOADED && isDev() && !isTest()) {
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
|
@ -61,6 +65,7 @@ module.exports = {
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
},
|
},
|
||||||
isTest,
|
isTest,
|
||||||
|
isCypress,
|
||||||
isDev,
|
isDev,
|
||||||
isProd: () => {
|
isProd: () => {
|
||||||
return !isDev()
|
return !isDev()
|
||||||
|
|
Loading…
Reference in New Issue