forked from tsai/budibase
committed by
GitHub
53 changed files with 1037 additions and 527 deletions
@ -0,0 +1,57 @@ |
|||
<script> |
|||
import "@spectrum-css/pagination/dist/index-vars.css" |
|||
import "@spectrum-css/actionbutton/dist/index-vars.css" |
|||
import "@spectrum-css/typography/dist/index-vars.css" |
|||
|
|||
export let page |
|||
export let goToPrevPage |
|||
export let goToNextPage |
|||
export let hasPrevPage = true |
|||
export let hasNextPage = true |
|||
</script> |
|||
|
|||
<nav class="spectrum-Pagination spectrum-Pagination--explicit"> |
|||
<div |
|||
href="#" |
|||
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-ActionButton--quiet spectrum-Pagination-prevButton" |
|||
on:click={hasPrevPage ? goToPrevPage : null} |
|||
class:is-disabled={!hasPrevPage} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronLeft100" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="ChevronLeft" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</div> |
|||
<span class="spectrum-Body--secondary spectrum-Pagination-counter"> |
|||
Page {page} |
|||
</span> |
|||
<div |
|||
href="#" |
|||
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-ActionButton--quiet spectrum-Pagination-nextButton" |
|||
on:click={hasNextPage ? goToNextPage : null} |
|||
class:is-disabled={!hasNextPage} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronRight100" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
aria-label="ChevronLeft" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</div> |
|||
</nav> |
|||
|
|||
<style> |
|||
.spectrum-Pagination-counter { |
|||
margin-left: 0; |
|||
user-select: none; |
|||
} |
|||
.is-disabled:hover { |
|||
cursor: initial; |
|||
} |
|||
</style> |
|||
@ -1,71 +0,0 @@ |
|||
<script> |
|||
import { Button, Drawer, Body, DrawerContent, Layout } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { notifications } from "@budibase/bbui" |
|||
import { |
|||
getDatasourceForProvider, |
|||
getSchemaForDatasource, |
|||
} from "builderStore/dataBinding" |
|||
import SaveFields from "./EventsEditor/actions/SaveFields.svelte" |
|||
import { currentAsset } from "builderStore" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = {} |
|||
export let componentInstance |
|||
let drawer |
|||
let tempValue = value |
|||
|
|||
$: schemaFields = getSchemaFields(componentInstance) |
|||
|
|||
const getSchemaFields = component => { |
|||
const datasource = getDatasourceForProvider($currentAsset, component) |
|||
const { schema } = getSchemaForDatasource(datasource) |
|||
return Object.values(schema || {}) |
|||
} |
|||
|
|||
const saveFilter = async () => { |
|||
dispatch("change", tempValue) |
|||
notifications.success("Filters saved.") |
|||
drawer.hide() |
|||
} |
|||
|
|||
const onFieldsChanged = event => { |
|||
tempValue = event.detail |
|||
} |
|||
</script> |
|||
|
|||
<Button secondary on:click={drawer.show}>Define Filters</Button> |
|||
<Drawer bind:this={drawer} title="Filtering"> |
|||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<Layout> |
|||
<Body size="S"> |
|||
{#if !Object.keys(tempValue || {}).length} |
|||
Add your first filter column. |
|||
{:else} |
|||
Results are filtered to only those which match all of the following |
|||
constaints. |
|||
{/if} |
|||
</Body> |
|||
<div class="fields"> |
|||
<SaveFields |
|||
parameterFields={value} |
|||
{schemaFields} |
|||
valueLabel="Equals" |
|||
on:change={onFieldsChanged} |
|||
/> |
|||
</div> |
|||
</Layout> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: auto 1fr auto 1fr auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,90 @@ |
|||
<script> |
|||
import { |
|||
notifications, |
|||
Button, |
|||
Drawer, |
|||
Body, |
|||
DrawerContent, |
|||
Layout, |
|||
} from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { |
|||
getDatasourceForProvider, |
|||
getSchemaForDatasource, |
|||
} from "builderStore/dataBinding" |
|||
import LuceneFilterBuilder from "./LuceneFilterBuilder.svelte" |
|||
import { currentAsset } from "builderStore" |
|||
import SaveFields from "../EventsEditor/actions/SaveFields.svelte" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = [] |
|||
export let componentInstance |
|||
let drawer |
|||
let tempValue = value |
|||
|
|||
$: 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" |
|||
|
|||
// 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 () => { |
|||
dispatch("change", tempValue) |
|||
notifications.success("Filters saved.") |
|||
drawer.hide() |
|||
} |
|||
</script> |
|||
|
|||
<Button secondary on:click={drawer.show}>Define Filters</Button> |
|||
<Drawer bind:this={drawer} title="Filtering"> |
|||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button> |
|||
<DrawerContent slot="body"> |
|||
<Layout> |
|||
<Body size="S"> |
|||
{#if !numFilters} |
|||
Add your first filter column. |
|||
{:else} |
|||
Results are filtered to only those which match all of the following |
|||
constaints. |
|||
{/if} |
|||
</Body> |
|||
{#if internalTable} |
|||
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} /> |
|||
{:else} |
|||
<div class="fields"> |
|||
<SaveFields |
|||
parameterFields={Array.isArray(value) ? {} : value} |
|||
{schemaFields} |
|||
valueLabel="Equals" |
|||
on:change={e => (tempValue = e.detail)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</Layout> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: auto 1fr auto 1fr auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,237 @@ |
|||
<script> |
|||
import { |
|||
DatePicker, |
|||
ActionButton, |
|||
Button, |
|||
Select, |
|||
Combobox, |
|||
Input, |
|||
} from "@budibase/bbui" |
|||
import { store, currentAsset } from "builderStore" |
|||
import { getBindableProperties } from "builderStore/dataBinding" |
|||
import { createEventDispatcher } from "svelte" |
|||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" |
|||
import { generate } from "shortid" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let schemaFields |
|||
export let value |
|||
|
|||
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", |
|||
}, |
|||
} |
|||
const BannedTypes = ["link", "attachment"] |
|||
$: bindableProperties = getBindableProperties( |
|||
$currentAsset, |
|||
$store.selectedComponentId |
|||
) |
|||
$: fieldOptions = (schemaFields ?? []) |
|||
.filter(field => !BannedTypes.includes(field.type)) |
|||
.map(field => field.name) |
|||
|
|||
const addField = () => { |
|||
value = [ |
|||
...value, |
|||
{ |
|||
id: generate(), |
|||
field: null, |
|||
operator: OperatorOptions.Equals.value, |
|||
value: null, |
|||
valueType: "Value", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
const removeField = id => { |
|||
value = value.filter(field => field.id !== id) |
|||
} |
|||
|
|||
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 === "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 [] |
|||
} |
|||
|
|||
const onFieldChange = (expression, field) => { |
|||
// Update the field type |
|||
expression.type = schemaFields.find(x => x.name === field)?.type |
|||
|
|||
// Ensure a valid operator is set |
|||
const validOperators = getValidOperatorsForType(expression.type) |
|||
if (!validOperators.includes(expression.operator)) { |
|||
expression.operator = |
|||
validOperators[0]?.value ?? OperatorOptions.Equals.value |
|||
onOperatorChange(expression, expression.operator) |
|||
} |
|||
} |
|||
|
|||
const onOperatorChange = (expression, operator) => { |
|||
const noValueOptions = [ |
|||
OperatorOptions.Empty.value, |
|||
OperatorOptions.NotEmpty.value, |
|||
] |
|||
expression.noValue = noValueOptions.includes(operator) |
|||
if (expression.noValue) { |
|||
expression.value = null |
|||
} |
|||
} |
|||
|
|||
const getFieldOptions = field => { |
|||
const schema = schemaFields.find(x => x.name === field) |
|||
return schema?.constraints?.inclusion || [] |
|||
} |
|||
</script> |
|||
|
|||
{#if value?.length} |
|||
<div class="fields"> |
|||
{#each value as expression, idx} |
|||
<Select |
|||
bind:value={expression.field} |
|||
options={fieldOptions} |
|||
on:change={e => onFieldChange(expression, e.detail)} |
|||
placeholder="Column" |
|||
/> |
|||
<Select |
|||
disabled={!expression.field} |
|||
options={getValidOperatorsForType(expression.type)} |
|||
bind:value={expression.operator} |
|||
on:change={e => onOperatorChange(expression, e.detail)} |
|||
placeholder={null} |
|||
/> |
|||
<Select |
|||
disabled={expression.noValue || !expression.field} |
|||
options={["Value", "Binding"]} |
|||
bind:value={expression.valueType} |
|||
placeholder={null} |
|||
/> |
|||
{#if expression.valueType === "Binding"} |
|||
<DrawerBindableInput |
|||
disabled={expression.noValue} |
|||
title={`Value for "${expression.field}"`} |
|||
value={expression.value} |
|||
placeholder="Value" |
|||
bindings={bindableProperties} |
|||
on:change={event => (expression.value = event.detail)} |
|||
/> |
|||
{:else if ["string", "longform", "number"].includes(expression.type)} |
|||
<Input disabled={expression.noValue} bind:value={expression.value} /> |
|||
{:else if expression.type === "options"} |
|||
<Combobox |
|||
disabled={expression.noValue} |
|||
options={getFieldOptions(expression.field)} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else if expression.type === "boolean"} |
|||
<Combobox |
|||
disabled |
|||
options={[ |
|||
{ label: "True", value: true }, |
|||
{ label: "False", value: false }, |
|||
]} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else if expression.type === "datetime"} |
|||
<DatePicker |
|||
disabled={expression.noValue} |
|||
bind:value={expression.value} |
|||
/> |
|||
{:else} |
|||
<DrawerBindableInput disabled /> |
|||
{/if} |
|||
|
|||
<ActionButton |
|||
size="S" |
|||
quiet |
|||
icon="Close" |
|||
on:click={() => removeField(expression.id)} |
|||
/> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
<div> |
|||
<Button icon="AddCircle" size="M" secondary on:click={addField}> |
|||
Add expression |
|||
</Button> |
|||
</div> |
|||
|
|||
<style> |
|||
.fields { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
align-items: center; |
|||
grid-template-columns: 1fr 120px 120px 1fr auto; |
|||
} |
|||
</style> |
|||
@ -1,18 +1,26 @@ |
|||
const { QueryBuilder, buildSearchUrl, search } = require("./utils") |
|||
const { fullSearch, paginatedSearch } = require("./utils") |
|||
const CouchDB = require("../../../db") |
|||
const { outputProcessing } = require("../../../utilities/rowProcessor") |
|||
|
|||
exports.rowSearch = async ctx => { |
|||
const appId = ctx.appId |
|||
const { tableId } = ctx.params |
|||
const { bookmark, query, raw } = ctx.request.body |
|||
let url |
|||
if (query) { |
|||
url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete() |
|||
} else if (raw) { |
|||
url = buildSearchUrl({ |
|||
appId, |
|||
query: raw, |
|||
bookmark, |
|||
}) |
|||
const db = new CouchDB(appId) |
|||
const { paginate, query, ...params } = ctx.request.body |
|||
params.tableId = tableId |
|||
|
|||
let response |
|||
if (paginate) { |
|||
response = await paginatedSearch(appId, query, params) |
|||
} else { |
|||
response = await fullSearch(appId, query, params) |
|||
} |
|||
|
|||
// Enrich search results with relationships
|
|||
if (response.rows && response.rows.length) { |
|||
const table = await db.get(tableId) |
|||
response.rows = await outputProcessing(appId, table, response.rows) |
|||
} |
|||
ctx.body = await search(url) |
|||
|
|||
ctx.body = response |
|||
} |
|||
|
|||
@ -1,195 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { |
|||
Button, |
|||
DatePicker, |
|||
Label, |
|||
Select, |
|||
Toggle, |
|||
Input, |
|||
} from "@budibase/bbui" |
|||
|
|||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( |
|||
"sdk" |
|||
) |
|||
const component = getContext("component") |
|||
|
|||
export let table |
|||
export let columns = [] |
|||
export let pageSize |
|||
export let noRowsMessage |
|||
|
|||
let rows = [] |
|||
let loaded = false |
|||
let search = {} |
|||
let tableDefinition |
|||
let schema |
|||
|
|||
let nextBookmark = null |
|||
let bookmark = null |
|||
let lastBookmark = null |
|||
|
|||
$: fetchData(table, bookmark) |
|||
// omit empty strings |
|||
$: parsedSearch = Object.keys(search).reduce( |
|||
(acc, next) => |
|||
search[next] === "" ? acc : { ...acc, [next]: search[next] }, |
|||
{} |
|||
) |
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData(table, bookmark), |
|||
metadata: { datasource: { type: "table", tableId: table } }, |
|||
}, |
|||
] |
|||
|
|||
async function fetchData(table, mark) { |
|||
if (table) { |
|||
const tableDef = await API.fetchTableDefinition(table) |
|||
schema = tableDef.schema |
|||
const output = await API.searchTableData({ |
|||
tableId: table, |
|||
search: parsedSearch, |
|||
pagination: { |
|||
pageSize, |
|||
bookmark: mark, |
|||
}, |
|||
}) |
|||
rows = output.rows |
|||
nextBookmark = output.bookmark |
|||
} |
|||
loaded = true |
|||
} |
|||
|
|||
function nextPage() { |
|||
lastBookmark = bookmark |
|||
bookmark = nextBookmark |
|||
} |
|||
|
|||
function previousPage() { |
|||
nextBookmark = bookmark |
|||
if (lastBookmark !== bookmark) { |
|||
bookmark = lastBookmark |
|||
} else { |
|||
// special case for going back to beginning |
|||
bookmark = null |
|||
lastBookmark = null |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Provider {actions}> |
|||
<div use:styleable={$component.styles}> |
|||
<div class="query-builder"> |
|||
{#if schema} |
|||
{#each columns as field} |
|||
<div class="form-field"> |
|||
<Label extraSmall grey>{schema[field].name}</Label> |
|||
{#if schema[field].type === "options"} |
|||
<Select secondary bind:value={search[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema[field].type === "datetime"} |
|||
<DatePicker bind:value={search[field]} /> |
|||
{:else if schema[field].type === "boolean"} |
|||
<Toggle text={schema[field].name} bind:checked={search[field]} /> |
|||
{:else if schema[field].type === "number"} |
|||
<Input type="number" bind:value={search[field]} /> |
|||
{:else if schema[field].type === "string"} |
|||
<Input bind:value={search[field]} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
{/if} |
|||
<div class="actions"> |
|||
<Button |
|||
secondary |
|||
on:click={() => { |
|||
search = {} |
|||
bookmark = null |
|||
}} |
|||
> |
|||
Reset |
|||
</Button> |
|||
<Button |
|||
primary |
|||
on:click={() => { |
|||
bookmark = null |
|||
fetchData(table, bookmark) |
|||
}} |
|||
> |
|||
Search |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
{#if loaded} |
|||
{#if rows.length > 0} |
|||
{#if $component.children === 0 && $builderStore.inBuilder} |
|||
<p><i class="ri-image-line" />Add some components to display.</p> |
|||
{:else} |
|||
{#each rows as row} |
|||
<Provider data={row}> |
|||
<slot /> |
|||
</Provider> |
|||
{/each} |
|||
{/if} |
|||
{:else if noRowsMessage} |
|||
<p><i class="ri-search-2-line" />{noRowsMessage}</p> |
|||
{/if} |
|||
{/if} |
|||
<div class="pagination"> |
|||
{#if lastBookmark != null || bookmark != null} |
|||
<Button primary on:click={previousPage}>Back</Button> |
|||
{/if} |
|||
{#if nextBookmark != null && rows.length !== 0} |
|||
<Button primary on:click={nextPage}>Next</Button> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</Provider> |
|||
|
|||
<style> |
|||
p { |
|||
margin: 0 var(--spacing-m); |
|||
background-color: var(--grey-2); |
|||
color: var(--grey-6); |
|||
font-size: var(--font-size-s); |
|||
padding: var(--spacing-l); |
|||
border-radius: var(--border-radius-s); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
p i { |
|||
margin-bottom: var(--spacing-m); |
|||
font-size: 1.5rem; |
|||
color: var(--grey-5); |
|||
} |
|||
|
|||
.query-builder { |
|||
padding: var(--spacing-m); |
|||
border-radius: var(--border-radius-s); |
|||
} |
|||
|
|||
.actions { |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
justify-content: flex-end; |
|||
grid-auto-flow: column; |
|||
} |
|||
|
|||
.form-field { |
|||
margin-bottom: var(--spacing-m); |
|||
} |
|||
|
|||
.pagination { |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
justify-content: flex-end; |
|||
margin-top: var(--spacing-m); |
|||
grid-auto-flow: column; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue