Merge pull request #1490 from Budibase/lab-day-search
Searching and pagination for internal tables
This commit is contained in:
commit
982336af82
53 changed files with 1037 additions and 527 deletions
|
@ -56,6 +56,7 @@
|
||||||
"@spectrum-css/link": "^3.1.1",
|
"@spectrum-css/link": "^3.1.1",
|
||||||
"@spectrum-css/menu": "^3.0.1",
|
"@spectrum-css/menu": "^3.0.1",
|
||||||
"@spectrum-css/modal": "^3.0.1",
|
"@spectrum-css/modal": "^3.0.1",
|
||||||
|
"@spectrum-css/pagination": "^3.0.3",
|
||||||
"@spectrum-css/picker": "^1.0.1",
|
"@spectrum-css/picker": "^1.0.1",
|
||||||
"@spectrum-css/popover": "^3.0.1",
|
"@spectrum-css/popover": "^3.0.1",
|
||||||
"@spectrum-css/progressbar": "^1.0.2",
|
"@spectrum-css/progressbar": "^1.0.2",
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<Checkbox {error} {disabled} {text} {value} on:change={onChange} />
|
<Checkbox {error} {disabled} {text} {value} on:change={onChange} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let error = null
|
export let error = null
|
||||||
export let placeholder = "Choose an option"
|
export let placeholder = "Choose an option or type"
|
||||||
export let options = []
|
export let options = []
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<Combobox
|
<Combobox
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let id = null
|
export let id = null
|
||||||
export let placeholder = "Choose an option"
|
export let placeholder = "Choose an option or type"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let options = []
|
export let options = []
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
const getFieldText = (value, options, placeholder) => {
|
const getFieldText = (value, options, placeholder) => {
|
||||||
// Always use placeholder if no value
|
// Always use placeholder if no value
|
||||||
if (value == null || value === "") {
|
if (value == null || value === "") {
|
||||||
return placeholder || "Choose an option"
|
return placeholder || "Choose an option or type"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for options to load if there is a value but no options
|
// Wait for options to load if there is a value but no options
|
||||||
|
@ -45,10 +45,16 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="spectrum-InputGroup" class:is-focused={open || focus}>
|
<div
|
||||||
|
class="spectrum-InputGroup"
|
||||||
|
class:is-focused={open || focus}
|
||||||
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
class:is-disabled={!!error}
|
class:is-invalid={!!error}
|
||||||
|
class:is-disabled={disabled}
|
||||||
class:is-focused={open || focus}
|
class:is-focused={open || focus}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -57,6 +63,7 @@
|
||||||
on:blur={() => (focus = false)}
|
on:blur={() => (focus = false)}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
{value}
|
{value}
|
||||||
|
{disabled}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
/>
|
/>
|
||||||
|
@ -65,7 +72,7 @@
|
||||||
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
disabled={!!error}
|
{disabled}
|
||||||
on:click={() => (open = true)}
|
on:click={() => (open = true)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -116,6 +123,9 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.spectrum-Textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.spectrum-Textfield-input {
|
.spectrum-Textfield-input {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<CoreDropzone
|
<CoreDropzone
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
export let id = null
|
export let id = null
|
||||||
export let label = null
|
export let label = null
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let disabled = false
|
|
||||||
export let error = null
|
export let error = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<TextField
|
<TextField
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<Multiselect
|
<Multiselect
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled}>
|
<Field {label} {labelPosition}>
|
||||||
<Search
|
<Search
|
||||||
{disabled}
|
{disabled}
|
||||||
{value}
|
{value}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<Select
|
<Select
|
||||||
{quiet}
|
{quiet}
|
||||||
{error}
|
{error}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<TextArea
|
<TextArea
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
{error}
|
{error}
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {disabled} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
57
packages/bbui/src/Pagination/Pagination.svelte
Normal file
57
packages/bbui/src/Pagination/Pagination.svelte
Normal file
|
@ -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>
|
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
on:click
|
on:click
|
||||||
class:spectrum-ProgressBar--indeterminate={!value}
|
class:spectrum-ProgressCircle--indeterminate={!value}
|
||||||
class:spectrum-ProgressCircle--overBackground={overBackground}
|
class:spectrum-ProgressCircle--overBackground={overBackground}
|
||||||
class="spectrum-ProgressCircle spectrum-ProgressCircle--{convertSize(size)}"
|
class="spectrum-ProgressCircle spectrum-ProgressCircle--{convertSize(size)}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,16 @@
|
||||||
import CellRenderer from "./CellRenderer.svelte"
|
import CellRenderer from "./CellRenderer.svelte"
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
* Each field schema can be enriched with a few extra properties to customise
|
||||||
|
* the behaviour.
|
||||||
|
* All of these are optional and do not need to be added.
|
||||||
|
* displayName: Overrides the field name displayed as the column title
|
||||||
|
* sortable: Set to false to disable sorting data by a certain column
|
||||||
|
* editable: Set to false to disable editing a certain column if the
|
||||||
|
* allowEditColumns prop is true
|
||||||
|
*/
|
||||||
export let data = []
|
export let data = []
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let showAutoColumns = false
|
export let showAutoColumns = false
|
||||||
|
@ -462,10 +472,6 @@
|
||||||
tbody tr.hidden {
|
tbody tr.hidden {
|
||||||
height: calc(var(--row-height) + 1px);
|
height: calc(var(--row-height) + 1px);
|
||||||
}
|
}
|
||||||
tbody tr.offset {
|
|
||||||
background-color: red;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
td {
|
td {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
|
@ -51,6 +51,7 @@ export { default as TreeView } from "./TreeView/Tree.svelte"
|
||||||
export { default as TreeItem } from "./TreeView/Item.svelte"
|
export { default as TreeItem } from "./TreeView/Item.svelte"
|
||||||
export { default as Divider } from "./Divider/Divider.svelte"
|
export { default as Divider } from "./Divider/Divider.svelte"
|
||||||
export { default as Search } from "./Form/Search.svelte"
|
export { default as Search } from "./Form/Search.svelte"
|
||||||
|
export { default as Pagination } from "./Pagination/Pagination.svelte"
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
export { default as Body } from "./Typography/Body.svelte"
|
export { default as Body } from "./Typography/Body.svelte"
|
||||||
|
|
|
@ -161,6 +161,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/modal/-/modal-3.0.2.tgz#58b6621cab65f90788d310374f40df1f7090473f"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/modal/-/modal-3.0.2.tgz#58b6621cab65f90788d310374f40df1f7090473f"
|
||||||
integrity sha512-YnIivJhoaao7Otu+HV7sgebPyFbO6sd/oMvTN/Rb2wwgnaMnIIuIRdGandSrcgotN2uNgs+P0knG6mv/xA1/dg==
|
integrity sha512-YnIivJhoaao7Otu+HV7sgebPyFbO6sd/oMvTN/Rb2wwgnaMnIIuIRdGandSrcgotN2uNgs+P0knG6mv/xA1/dg==
|
||||||
|
|
||||||
|
"@spectrum-css/pagination@^3.0.3":
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@spectrum-css/pagination/-/pagination-3.0.3.tgz#b204c3ada384c4af751a354bc428346d82eeea65"
|
||||||
|
integrity sha512-OJ/v9GeNXJOZ9Yr9LDBYPrR2NCiLOWP9wANT/a5sqFuugRnQbn/HYMnRp9TBxwpDY6ihaPo0T/wi7kLiAJFdDw==
|
||||||
|
|
||||||
"@spectrum-css/picker@^1.0.1":
|
"@spectrum-css/picker@^1.0.1":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.2.tgz#b49429ae3c89f9c5f2c0530787ce45392c9612ff"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.2.tgz#b49429ae3c89f9c5f2c0530787ce45392c9612ff"
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const gradient = (node, config = {}) => {
|
||||||
lightness: 0.7,
|
lightness: 0.7,
|
||||||
softness: 0.9,
|
softness: 0.9,
|
||||||
seed: null,
|
seed: null,
|
||||||
|
version: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies a gradient background
|
// Applies a gradient background
|
||||||
|
@ -15,6 +16,7 @@ export const gradient = (node, config = {}) => {
|
||||||
}
|
}
|
||||||
const { saturation, lightness, softness, points } = config
|
const { saturation, lightness, softness, points } = config
|
||||||
const seed = config.seed || Math.random().toString(32).substring(2)
|
const seed = config.seed || Math.random().toString(32).substring(2)
|
||||||
|
const version = config.version ?? 0
|
||||||
|
|
||||||
// Hash function which returns a fixed hash between specified limits
|
// Hash function which returns a fixed hash between specified limits
|
||||||
// for a given seed and a given version
|
// for a given seed and a given version
|
||||||
|
@ -69,10 +71,10 @@ export const gradient = (node, config = {}) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let css = `opacity:0.9;background:${randomHSL(seed, 0, 0.7)};`
|
let css = `opacity:0.9;background:${randomHSL(seed, version, 0.7)};`
|
||||||
css += "background-image:"
|
css += "background-image:"
|
||||||
for (let i = 0; i < points - 1; i++) {
|
for (let i = 0; i < points - 1; i++) {
|
||||||
css += `${randomGradientPoint(seed, i)},`
|
css += `${randomGradientPoint(seed, version + i)},`
|
||||||
}
|
}
|
||||||
css += `${randomGradientPoint(seed, points)};`
|
css += `${randomGradientPoint(seed, points)};`
|
||||||
node.style = css
|
node.style = css
|
||||||
|
|
|
@ -90,10 +90,17 @@ const createScreen = table => {
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
type: "table",
|
type: "table",
|
||||||
},
|
},
|
||||||
filter: {
|
filter: [
|
||||||
_id: `{{ ${makePropSafe("url")}.${makePropSafe("id")} }}`,
|
{
|
||||||
|
field: "_id",
|
||||||
|
operator: "equal",
|
||||||
|
type: "string",
|
||||||
|
value: `{{ ${makePropSafe("url")}.${makePropSafe("id")} }}`,
|
||||||
|
valueType: "Binding",
|
||||||
},
|
},
|
||||||
|
],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
paginate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const repeater = new Component("@budibase/standard-components/repeater")
|
const repeater = new Component("@budibase/standard-components/repeater")
|
||||||
|
|
|
@ -80,6 +80,7 @@ const createScreen = table => {
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
type: "table",
|
type: "table",
|
||||||
},
|
},
|
||||||
|
paginate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const spectrumTable = new Component("@budibase/standard-components/table")
|
const spectrumTable = new Component("@budibase/standard-components/table")
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
const inputChanged = ev => {
|
const inputChanged = ev => {
|
||||||
try {
|
try {
|
||||||
values = ev.target.value.split("\n")
|
values = ev.detail.split("\n")
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
values = []
|
values = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,11 @@
|
||||||
export let title = "Bindings"
|
export let title = "Bindings"
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let label
|
export let label
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
$: tempValue = value
|
$: tempValue = Array.isArray(value) ? value : []
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
@ -32,13 +33,16 @@
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Input
|
<Input
|
||||||
{label}
|
{label}
|
||||||
|
{disabled}
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
on:change={event => onChange(event.detail)}
|
on:change={event => onChange(event.detail)}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
/>
|
/>
|
||||||
|
{#if !disabled}
|
||||||
<div class="icon" on:click={bindingDrawer.show}>
|
<div class="icon" on:click={bindingDrawer.show}>
|
||||||
<Icon size="S" name="FlashOn" />
|
<Icon size="S" name="FlashOn" />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={bindingDrawer} {title}>
|
<Drawer bind:this={bindingDrawer} {title}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"table",
|
"table",
|
||||||
"repeater",
|
"repeater",
|
||||||
"button",
|
"button",
|
||||||
"search",
|
|
||||||
{
|
{
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
|
|
|
@ -3,20 +3,34 @@
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { findComponentPath } from "builderStore/storeUtils"
|
import { findComponentPath } from "builderStore/storeUtils"
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
export let onChange
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
||||||
$: providers = path.filter(
|
$: providers = path.filter(
|
||||||
component =>
|
component =>
|
||||||
component._component === "@budibase/standard-components/dataprovider"
|
component._component === "@budibase/standard-components/dataprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set initial value to closest data provider
|
||||||
|
onMount(() => {
|
||||||
|
const valid = value && providers.find(x => getValue(x) === value) != null
|
||||||
|
if (!valid && providers.length) {
|
||||||
|
dispatch("change", getValue(providers[providers.length - 1]))
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
{value}
|
{value}
|
||||||
|
placeholder={null}
|
||||||
on:change
|
on:change
|
||||||
options={providers}
|
options={providers}
|
||||||
getOptionLabel={component => component._instanceName}
|
getOptionLabel={component => component._instanceName}
|
||||||
getOptionValue={component => `{{ literal ${makePropSafe(component._id)} }}`}
|
getOptionValue={getValue}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<div>
|
<div>
|
||||||
<Button icon="AddCircle" size="S" cta on:click={addField}>
|
<Button icon="AddCircle" secondary on:click={addField}>
|
||||||
Add
|
Add
|
||||||
{fieldLabel}
|
{fieldLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -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>
|
|
@ -16,7 +16,7 @@
|
||||||
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||||
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
|
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
|
||||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||||
|
@ -156,8 +156,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty">
|
<div class="text">This component doesn't have any additional settings.</div>
|
||||||
This component doesn't have any additional settings.
|
{/if}
|
||||||
|
{#if componentDefinition?.info}
|
||||||
|
<div class="text">
|
||||||
|
{@html componentDefinition?.info}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -185,7 +188,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
.empty {
|
.text {
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
color: var(--grey-6);
|
color: var(--grey-6);
|
||||||
|
|
|
@ -6,10 +6,6 @@ html, body {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum--light {
|
|
||||||
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-75);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--background: var(--spectrum-alias-background-color-primary);
|
--background: var(--spectrum-alias-background-color-primary);
|
||||||
--background-alt: var(--spectrum-alias-background-color-secondary);
|
--background-alt: var(--spectrum-alias-background-color-secondary);
|
||||||
|
|
|
@ -18,19 +18,37 @@ export const fetchTableData = async tableId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a mango query against an internal table
|
* Searches a table using Lucene.
|
||||||
* @param {String} tableId - id of the table to search
|
|
||||||
* @param {Object} search - Mango Compliant search object
|
|
||||||
* @param {Object} pagination - the pagination controls
|
|
||||||
*/
|
*/
|
||||||
export const searchTableData = async ({ tableId, search, pagination }) => {
|
export const searchTable = async ({
|
||||||
const output = await API.post({
|
tableId,
|
||||||
url: `/api/${tableId}/rows/search`,
|
query,
|
||||||
|
bookmark,
|
||||||
|
limit,
|
||||||
|
sort,
|
||||||
|
sortOrder,
|
||||||
|
sortType,
|
||||||
|
paginate,
|
||||||
|
}) => {
|
||||||
|
if (!tableId || !query) {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await API.post({
|
||||||
|
url: `/api/search/${tableId}/rows`,
|
||||||
body: {
|
body: {
|
||||||
query: search,
|
query,
|
||||||
pagination,
|
bookmark,
|
||||||
|
limit,
|
||||||
|
sort,
|
||||||
|
sortOrder,
|
||||||
|
sortType,
|
||||||
|
paginate,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
output.rows = await enrichRows(output.rows, tableId)
|
return {
|
||||||
return output
|
...res,
|
||||||
|
rows: await enrichRows(res?.rows, tableId),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded && $screenStore.activeLayout}
|
{#if loaded && $screenStore.activeLayout}
|
||||||
|
<div lang="en" dir="ltr" class="spectrum spectrum--medium spectrum--light">
|
||||||
<Provider key="user" data={$authStore} {actions}>
|
<Provider key="user" data={$authStore} {actions}>
|
||||||
<Component definition={$screenStore.activeLayout.props} />
|
<Component definition={$screenStore.activeLayout.props} />
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background: transparent;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -16,7 +16,6 @@ const {
|
||||||
const { FieldTypes } = require("../../constants")
|
const { FieldTypes } = require("../../constants")
|
||||||
const { isEqual } = require("lodash")
|
const { isEqual } = require("lodash")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { QueryBuilder, search } = require("./search/utils")
|
|
||||||
|
|
||||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||||
|
|
||||||
|
@ -248,45 +247,6 @@ exports.fetchView = async function (ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.search = async function (ctx) {
|
|
||||||
const appId = ctx.appId
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const {
|
|
||||||
query,
|
|
||||||
pagination: { pageSize = 10, bookmark },
|
|
||||||
} = ctx.request.body
|
|
||||||
const tableId = ctx.params.tableId
|
|
||||||
|
|
||||||
const queryBuilder = new QueryBuilder(appId)
|
|
||||||
.setLimit(pageSize)
|
|
||||||
.addTable(tableId)
|
|
||||||
if (bookmark) {
|
|
||||||
queryBuilder.setBookmark(bookmark)
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchString
|
|
||||||
if (ctx.query && ctx.query.raw && ctx.query.raw !== "") {
|
|
||||||
searchString = queryBuilder.complete(query["RAW"])
|
|
||||||
} else {
|
|
||||||
// make all strings a starts with operation rather than pure equality
|
|
||||||
for (const [key, queryVal] of Object.entries(query)) {
|
|
||||||
if (typeof queryVal === "string") {
|
|
||||||
queryBuilder.addString(key, queryVal)
|
|
||||||
} else {
|
|
||||||
queryBuilder.addEqual(key, queryVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searchString = queryBuilder.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await search(searchString)
|
|
||||||
const table = await db.get(tableId)
|
|
||||||
ctx.body = {
|
|
||||||
rows: await outputProcessing(appId, table, response.rows),
|
|
||||||
bookmark: response.bookmark,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.fetchTableRows = async function (ctx) {
|
exports.fetchTableRows = async function (ctx) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
|
|
@ -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 => {
|
exports.rowSearch = async ctx => {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const { tableId } = ctx.params
|
const { tableId } = ctx.params
|
||||||
const { bookmark, query, raw } = ctx.request.body
|
const db = new CouchDB(appId)
|
||||||
let url
|
const { paginate, query, ...params } = ctx.request.body
|
||||||
if (query) {
|
params.tableId = tableId
|
||||||
url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete()
|
|
||||||
} else if (raw) {
|
let response
|
||||||
url = buildSearchUrl({
|
if (paginate) {
|
||||||
appId,
|
response = await paginatedSearch(appId, query, params)
|
||||||
query: raw,
|
} else {
|
||||||
bookmark,
|
response = await fullSearch(appId, query, params)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
ctx.body = await search(url)
|
|
||||||
|
// 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 = response
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,19 @@ const env = require("../../../environment")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a set of inputs this will generate the URL which is to be sent to the search proxy in CouchDB.
|
* Escapes any characters in a string which lucene searches require to be
|
||||||
* @param {string} appId The ID of the app which we will be searching within.
|
* escaped.
|
||||||
* @param {string} query The lucene query string which is to be used for searching.
|
* @param value The value to escape
|
||||||
* @param {string|null} bookmark If there were more than the limit specified can send the bookmark that was
|
* @returns {string}
|
||||||
* returned with query for next set of search results.
|
|
||||||
* @param {number} limit The number of entries to return per query.
|
|
||||||
* @param {boolean} excludeDocs By default full rows are returned, if required this can be disabled.
|
|
||||||
* @return {string} The URL which a GET can be performed on to receive results.
|
|
||||||
*/
|
*/
|
||||||
function buildSearchUrl({ appId, query, bookmark, excludeDocs, limit = 50 }) {
|
const luceneEscape = value => {
|
||||||
let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search`
|
return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&")
|
||||||
url += `/${SearchIndexes.ROWS}?q=${query}`
|
|
||||||
url += `&limit=${limit}`
|
|
||||||
if (!excludeDocs) {
|
|
||||||
url += "&include_docs=true"
|
|
||||||
}
|
|
||||||
if (bookmark) {
|
|
||||||
url += `&bookmark=${bookmark}`
|
|
||||||
}
|
|
||||||
return checkSlashesInUrl(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to build lucene query URLs.
|
||||||
|
* Optionally takes a base lucene query object.
|
||||||
|
*/
|
||||||
class QueryBuilder {
|
class QueryBuilder {
|
||||||
constructor(appId, base) {
|
constructor(appId, base) {
|
||||||
this.appId = appId
|
this.appId = appId
|
||||||
|
@ -34,10 +25,20 @@ class QueryBuilder {
|
||||||
fuzzy: {},
|
fuzzy: {},
|
||||||
range: {},
|
range: {},
|
||||||
equal: {},
|
equal: {},
|
||||||
|
notEqual: {},
|
||||||
|
empty: {},
|
||||||
|
notEmpty: {},
|
||||||
...base,
|
...base,
|
||||||
}
|
}
|
||||||
this.limit = 50
|
this.limit = 50
|
||||||
this.bookmark = null
|
this.sortOrder = "ascending"
|
||||||
|
this.sortType = "string"
|
||||||
|
this.includeDocs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setTable(tableId) {
|
||||||
|
this.query.equal.tableId = tableId
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setLimit(limit) {
|
setLimit(limit) {
|
||||||
|
@ -45,11 +46,31 @@ class QueryBuilder {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSort(sort) {
|
||||||
|
this.sort = sort
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortOrder(sortOrder) {
|
||||||
|
this.sortOrder = sortOrder
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortType(sortType) {
|
||||||
|
this.sortType = sortType
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
setBookmark(bookmark) {
|
setBookmark(bookmark) {
|
||||||
this.bookmark = bookmark
|
this.bookmark = bookmark
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
excludeDocs() {
|
||||||
|
this.includeDocs = false
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
addString(key, partial) {
|
addString(key, partial) {
|
||||||
this.query.string[key] = partial
|
this.query.string[key] = partial
|
||||||
return this
|
return this
|
||||||
|
@ -73,52 +94,113 @@ class QueryBuilder {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
addTable(tableId) {
|
addNotEqual(key, value) {
|
||||||
this.query.equal.tableId = tableId
|
this.query.notEqual[key] = value
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(rawQuery = null) {
|
addEmpty(key, value) {
|
||||||
let output = ""
|
this.query.empty[key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotEmpty(key, value) {
|
||||||
|
this.query.notEmpty[key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchQuery() {
|
||||||
|
let query = "*:*"
|
||||||
|
|
||||||
function build(structure, queryFn) {
|
function build(structure, queryFn) {
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
if (output.length !== 0) {
|
const expression = queryFn(luceneEscape(key.replace(/ /, "_")), value)
|
||||||
output += " AND "
|
if (expression == null) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
output += queryFn(key, value)
|
query += ` AND ${expression}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct the actual lucene search query string from JSON structure
|
||||||
if (this.query.string) {
|
if (this.query.string) {
|
||||||
build(this.query.string, (key, value) => `${key}:${value}*`)
|
build(this.query.string, (key, value) => {
|
||||||
|
return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (this.query.range) {
|
if (this.query.range) {
|
||||||
build(
|
build(this.query.range, (key, value) => {
|
||||||
this.query.range,
|
if (!value) {
|
||||||
(key, value) => `${key}:[${value.low} TO ${value.high}]`
|
return null
|
||||||
)
|
}
|
||||||
|
if (value.low == null || value.low === "") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (value.high == null || value.high === "") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `${key}:[${value.low} TO ${value.high}]`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (this.query.fuzzy) {
|
if (this.query.fuzzy) {
|
||||||
build(this.query.fuzzy, (key, value) => `${key}:${value}~`)
|
build(this.query.fuzzy, (key, value) => {
|
||||||
|
return value ? `${key}:${luceneEscape(value.toLowerCase())}~` : null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (this.query.equal) {
|
if (this.query.equal) {
|
||||||
build(this.query.equal, (key, value) => `${key}:${value}`)
|
build(this.query.equal, (key, value) => {
|
||||||
}
|
return value ? `${key}:${luceneEscape(value.toLowerCase())}` : null
|
||||||
if (rawQuery) {
|
|
||||||
output = output.length === 0 ? rawQuery : `&${rawQuery}`
|
|
||||||
}
|
|
||||||
return buildSearchUrl({
|
|
||||||
appId: this.appId,
|
|
||||||
query: output,
|
|
||||||
bookmark: this.bookmark,
|
|
||||||
limit: this.limit,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.query.notEqual) {
|
||||||
|
build(this.query.notEqual, (key, value) => {
|
||||||
|
return value ? `!${key}:${luceneEscape(value.toLowerCase())}` : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.query.empty) {
|
||||||
|
build(this.query.empty, key => `!${key}:["" TO *]`)
|
||||||
|
}
|
||||||
|
if (this.query.notEmpty) {
|
||||||
|
build(this.query.notEmpty, key => `${key}:["" TO *]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchBody() {
|
||||||
|
let body = {
|
||||||
|
q: this.buildSearchQuery(),
|
||||||
|
limit: Math.min(this.limit, 200),
|
||||||
|
include_docs: this.includeDocs,
|
||||||
|
}
|
||||||
|
if (this.bookmark) {
|
||||||
|
body.bookmark = this.bookmark
|
||||||
|
}
|
||||||
|
if (this.sort) {
|
||||||
|
const order = this.sortOrder === "descending" ? "-" : ""
|
||||||
|
const type = `<${this.sortType}>`
|
||||||
|
body.sort = `${order}${this.sort.replace(/ /, "_")}${type}`
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search/${SearchIndexes.ROWS}`
|
||||||
|
const body = this.buildSearchBody()
|
||||||
|
return await runQuery(url, body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.search = async query => {
|
/**
|
||||||
const response = await fetch(query, {
|
* Executes a lucene search query.
|
||||||
method: "GET",
|
* @param url The query URL
|
||||||
|
* @param body The request body defining search criteria
|
||||||
|
* @returns {Promise<{rows: []}>}
|
||||||
|
*/
|
||||||
|
const runQuery = async (url, body) => {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
method: "POST",
|
||||||
})
|
})
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
let output = {
|
let output = {
|
||||||
|
@ -133,5 +215,122 @@ exports.search = async query => {
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.QueryBuilder = QueryBuilder
|
/**
|
||||||
exports.buildSearchUrl = buildSearchUrl
|
* Gets round the fixed limit of 200 results from a query by fetching as many
|
||||||
|
* pages as required and concatenating the results. This recursively operates
|
||||||
|
* until enough results have been found.
|
||||||
|
* @param appId {string} The app ID to search
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The number of results to fetch
|
||||||
|
* bookmark {string|null} Current bookmark in the recursive search
|
||||||
|
* rows {array|null} Current results in the recursive search
|
||||||
|
* @returns {Promise<*[]|*>}
|
||||||
|
*/
|
||||||
|
const recursiveSearch = async (appId, query, params) => {
|
||||||
|
const bookmark = params.bookmark
|
||||||
|
const rows = params.rows || []
|
||||||
|
if (rows.length >= params.limit) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
let pageSize = 200
|
||||||
|
if (rows.length > params.limit - 200) {
|
||||||
|
pageSize = params.limit - rows.length
|
||||||
|
}
|
||||||
|
const page = await new QueryBuilder(appId, query)
|
||||||
|
.setTable(params.tableId)
|
||||||
|
.setBookmark(bookmark)
|
||||||
|
.setLimit(pageSize)
|
||||||
|
.setSort(params.sort)
|
||||||
|
.setSortOrder(params.sortOrder)
|
||||||
|
.setSortType(params.sortType)
|
||||||
|
.run()
|
||||||
|
if (!page.rows.length) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
if (page.rows.length < 200) {
|
||||||
|
return [...rows, ...page.rows]
|
||||||
|
}
|
||||||
|
const newParams = {
|
||||||
|
...params,
|
||||||
|
bookmark: page.bookmark,
|
||||||
|
rows: [...rows, ...page.rows],
|
||||||
|
}
|
||||||
|
return await recursiveSearch(appId, query, newParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a paginated search. A bookmark will be returned to allow the next
|
||||||
|
* page to be fetched. There is a max limit off 200 results per page in a
|
||||||
|
* paginated search.
|
||||||
|
* @param appId {string} The app ID to search
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The desired page size
|
||||||
|
* bookmark {string} The bookmark to resume from
|
||||||
|
* @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
|
||||||
|
*/
|
||||||
|
exports.paginatedSearch = async (appId, query, params) => {
|
||||||
|
let limit = params.limit
|
||||||
|
if (limit == null || isNaN(limit) || limit < 0) {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
limit = Math.min(limit, 200)
|
||||||
|
const search = new QueryBuilder(appId, query)
|
||||||
|
.setTable(params.tableId)
|
||||||
|
.setSort(params.sort)
|
||||||
|
.setSortOrder(params.sortOrder)
|
||||||
|
.setSortType(params.sortType)
|
||||||
|
const searchResults = await search
|
||||||
|
.setBookmark(params.bookmark)
|
||||||
|
.setLimit(limit)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
// Try fetching 1 row in the next page to see if another page of results
|
||||||
|
// exists or not
|
||||||
|
const nextResults = await search
|
||||||
|
.setBookmark(searchResults.bookmark)
|
||||||
|
.setLimit(1)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...searchResults,
|
||||||
|
hasNextPage: nextResults.rows && nextResults.rows.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a full search, fetching multiple pages if required to return the
|
||||||
|
* desired amount of results. There is a limit of 1000 results to avoid
|
||||||
|
* heavy performance hits, and to avoid client components breaking from
|
||||||
|
* handling too much data.
|
||||||
|
* @param appId {string} The app ID to search
|
||||||
|
* @param query {object} The JSON query structure
|
||||||
|
* @param params {object} The search params including:
|
||||||
|
* tableId {string} The table ID to search
|
||||||
|
* sort {string} The sort column
|
||||||
|
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||||
|
* sortType {string} Whether to treat sortable values as strings or
|
||||||
|
* numbers. ("string" or "number")
|
||||||
|
* limit {number} The desired number of results
|
||||||
|
* @returns {Promise<{rows: *}>}
|
||||||
|
*/
|
||||||
|
exports.fullSearch = async (appId, query, params) => {
|
||||||
|
let limit = params.limit
|
||||||
|
if (limit == null || isNaN(limit) || limit < 0) {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
params.limit = Math.min(limit, 1000)
|
||||||
|
const rows = await recursiveSearch(appId, query, params)
|
||||||
|
return { rows }
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ const queryRoutes = require("./query")
|
||||||
const hostingRoutes = require("./hosting")
|
const hostingRoutes = require("./hosting")
|
||||||
const backupRoutes = require("./backup")
|
const backupRoutes = require("./backup")
|
||||||
const devRoutes = require("./dev")
|
const devRoutes = require("./dev")
|
||||||
|
const searchRoutes = require("./search")
|
||||||
|
|
||||||
exports.mainRoutes = [
|
exports.mainRoutes = [
|
||||||
authRoutes,
|
authRoutes,
|
||||||
|
@ -51,6 +52,7 @@ exports.mainRoutes = [
|
||||||
// this could be breaking as koa may recognise other routes as this
|
// this could be breaking as koa may recognise other routes as this
|
||||||
tableRoutes,
|
tableRoutes,
|
||||||
rowRoutes,
|
rowRoutes,
|
||||||
|
searchRoutes,
|
||||||
]
|
]
|
||||||
|
|
||||||
exports.staticRoutes = staticRoutes
|
exports.staticRoutes = staticRoutes
|
||||||
|
|
|
@ -39,12 +39,6 @@ router
|
||||||
usage,
|
usage,
|
||||||
rowController.save
|
rowController.save
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/api/:tableId/rows/search",
|
|
||||||
paramResource("tableId"),
|
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
|
||||||
rowController.search
|
|
||||||
)
|
|
||||||
.patch(
|
.patch(
|
||||||
"/api/:tableId/rows/:rowId",
|
"/api/:tableId/rows/:rowId",
|
||||||
paramSubResource("tableId", "rowId"),
|
paramSubResource("tableId", "rowId"),
|
||||||
|
|
|
@ -84,6 +84,7 @@ async function searchIndex(appId, indexName, fnString) {
|
||||||
designDoc.indexes = {
|
designDoc.indexes = {
|
||||||
[indexName]: {
|
[indexName]: {
|
||||||
index: fnString,
|
index: fnString,
|
||||||
|
analyzer: "keyword",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
|
@ -96,11 +97,15 @@ exports.createAllSearchIndex = async appId => {
|
||||||
function (doc) {
|
function (doc) {
|
||||||
function idx(input, prev) {
|
function idx(input, prev) {
|
||||||
for (let key of Object.keys(input)) {
|
for (let key of Object.keys(input)) {
|
||||||
const idxKey = prev != null ? `${prev}.${key}` : key
|
let idxKey = prev != null ? `${prev}.${key}` : key
|
||||||
if (key === "_id" || key === "_rev") {
|
idxKey = idxKey.replace(/ /, "_")
|
||||||
|
if (key === "_id" || key === "_rev" || input[key] == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (typeof input[key] !== "object") {
|
if (typeof input[key] === "string") {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
index(idxKey, input[key].toLowerCase(), { store: true })
|
||||||
|
} else if (typeof input[key] !== "object") {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
index(idxKey, input[key], { store: true })
|
index(idxKey, input[key], { store: true })
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -123,24 +123,6 @@ function processAutoColumn(user, table, row) {
|
||||||
return { table, row }
|
return { table, row }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a set of rows and the table they came from this function will sort by auto ID or a custom
|
|
||||||
* method if provided (not implemented yet).
|
|
||||||
*/
|
|
||||||
function sortRows(table, rows) {
|
|
||||||
// sort based on auto ID (if found)
|
|
||||||
let autoIDColumn = Object.entries(table.schema).find(
|
|
||||||
schema => schema[1].subtype === AutoFieldSubTypes.AUTO_ID
|
|
||||||
)
|
|
||||||
// get the column name, this is the first element in the array (Object.entries)
|
|
||||||
autoIDColumn = autoIDColumn && autoIDColumn.length ? autoIDColumn[0] : null
|
|
||||||
if (autoIDColumn) {
|
|
||||||
// sort in ascending order
|
|
||||||
rows.sort((a, b) => a[autoIDColumn] - b[autoIDColumn])
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks through the rows provided and finds formulas - which it then processes.
|
* Looks through the rows provided and finds formulas - which it then processes.
|
||||||
*/
|
*/
|
||||||
|
@ -213,8 +195,6 @@ exports.outputProcessing = async (appId, table, rows) => {
|
||||||
rows = [rows]
|
rows = [rows]
|
||||||
wasArray = false
|
wasArray = false
|
||||||
}
|
}
|
||||||
// sort by auto ID
|
|
||||||
rows = sortRows(table, rows)
|
|
||||||
// attach any linked row information
|
// attach any linked row information
|
||||||
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
|
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
|
||||||
|
|
||||||
|
|
|
@ -65,41 +65,6 @@
|
||||||
"type": "schema"
|
"type": "schema"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
|
||||||
"name": "Search",
|
|
||||||
"description": "A searchable list of items.",
|
|
||||||
"icon": "Search",
|
|
||||||
"styleable": true,
|
|
||||||
"hasChildren": true,
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "table",
|
|
||||||
"label": "Table",
|
|
||||||
"key": "table"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "multifield",
|
|
||||||
"label": "Columns",
|
|
||||||
"key": "columns",
|
|
||||||
"dependsOn": "table"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"label": "Rows/Page",
|
|
||||||
"defaultValue": 25,
|
|
||||||
"key": "pageSize"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Empty Text",
|
|
||||||
"key": "noRowsMessage",
|
|
||||||
"defaultValue": "No rows found."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"context": {
|
|
||||||
"type": "schema"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stackedlist": {
|
"stackedlist": {
|
||||||
"name": "Stacked List",
|
"name": "Stacked List",
|
||||||
"icon": "TaskList",
|
"icon": "TaskList",
|
||||||
|
@ -1416,6 +1381,7 @@
|
||||||
},
|
},
|
||||||
"dataprovider": {
|
"dataprovider": {
|
||||||
"name": "Data Provider",
|
"name": "Data Provider",
|
||||||
|
"info": "Pagination is only available for data stored in internal tables.",
|
||||||
"icon": "Data",
|
"icon": "Data",
|
||||||
"styleable": false,
|
"styleable": false,
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
|
@ -1445,7 +1411,14 @@
|
||||||
{
|
{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"label": "Limit",
|
"label": "Limit",
|
||||||
"key": "limit"
|
"key": "limit",
|
||||||
|
"defaultValue": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Paginate",
|
||||||
|
"key": "paginate",
|
||||||
|
"defaultValue": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
|
@ -1464,12 +1437,8 @@
|
||||||
"key": "schema"
|
"key": "schema"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Loading",
|
"label": "Page Number",
|
||||||
"key": "loading"
|
"key": "pageNumber"
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Loaded",
|
|
||||||
"key": "loaded"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { ProgressCircle, Pagination } from "@budibase/bbui"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
export let filter
|
export let filter
|
||||||
export let sortColumn
|
export let sortColumn
|
||||||
export let sortOrder
|
export let sortOrder
|
||||||
export let limit
|
export let limit
|
||||||
|
export let paginate
|
||||||
|
|
||||||
const { API, styleable, Provider, ActionTypes } = getContext("sdk")
|
const { API, styleable, Provider, ActionTypes } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -15,15 +17,45 @@
|
||||||
|
|
||||||
// Loading flag for the initial load
|
// Loading flag for the initial load
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let schemaLoaded = false
|
||||||
|
|
||||||
|
// Provider state
|
||||||
|
let rows = []
|
||||||
let allRows = []
|
let allRows = []
|
||||||
let schema = {}
|
let schema = {}
|
||||||
|
let bookmarks = [null]
|
||||||
|
let pageNumber = 0
|
||||||
|
|
||||||
$: fetchData(dataSource)
|
$: internalTable = dataSource?.type === "table"
|
||||||
$: filteredRows = filterRows(allRows, filter)
|
$: query = internalTable ? buildLuceneQuery(filter) : null
|
||||||
$: sortedRows = sortRows(filteredRows, sortColumn, sortOrder)
|
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
||||||
$: rows = limitRows(sortedRows, limit)
|
$: hasPrevPage = pageNumber > 0
|
||||||
$: getSchema(dataSource)
|
$: getSchema(dataSource)
|
||||||
|
$: sortType = getSortType(schema, sortColumn)
|
||||||
|
$: {
|
||||||
|
// Wait until schema loads before loading data, so that we can determine
|
||||||
|
// the correct sort type first time
|
||||||
|
if (schemaLoaded) {
|
||||||
|
fetchData(
|
||||||
|
dataSource,
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
sortColumn,
|
||||||
|
sortOrder,
|
||||||
|
sortType,
|
||||||
|
paginate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
// Sort and limit rows in memory when we aren't searching internal tables
|
||||||
|
if (internalTable) {
|
||||||
|
rows = allRows
|
||||||
|
} else {
|
||||||
|
const sortedRows = sortRows(allRows, sortColumn, sortOrder)
|
||||||
|
rows = limitRows(sortedRows, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
type: ActionTypes.RefreshDatasource,
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
@ -31,27 +63,86 @@
|
||||||
metadata: { dataSource },
|
metadata: { dataSource },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
$: dataContext = {
|
$: dataContext = { rows, schema, rowsLength: rows.length }
|
||||||
rows,
|
|
||||||
schema,
|
const getSortType = (schema, sortColumn) => {
|
||||||
rowsLength: rows.length,
|
if (!schema || !sortColumn || !schema[sortColumn]) {
|
||||||
loading,
|
return "string"
|
||||||
loaded,
|
}
|
||||||
|
const type = schema?.[sortColumn]?.type
|
||||||
|
return type === "number" ? "number" : "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchData = async dataSource => {
|
const buildLuceneQuery = filter => {
|
||||||
|
let query = {
|
||||||
|
string: {},
|
||||||
|
fuzzy: {},
|
||||||
|
range: {},
|
||||||
|
equal: {},
|
||||||
|
notEqual: {},
|
||||||
|
empty: {},
|
||||||
|
notEmpty: {},
|
||||||
|
}
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
filter.forEach(({ operator, field, type, value }) => {
|
||||||
|
if (operator.startsWith("range")) {
|
||||||
|
if (!query.range[field]) {
|
||||||
|
query.range[field] = {
|
||||||
|
low: type === "number" ? Number.MIN_SAFE_INTEGER : "0000",
|
||||||
|
high: type === "number" ? Number.MAX_SAFE_INTEGER : "9999",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (operator === "rangeLow") {
|
||||||
|
query.range[field].low = value
|
||||||
|
} else if (operator === "rangeHigh") {
|
||||||
|
query.range[field].high = value
|
||||||
|
}
|
||||||
|
} else if (query[operator]) {
|
||||||
|
query[operator][field] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async (
|
||||||
|
dataSource,
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
sortColumn,
|
||||||
|
sortOrder,
|
||||||
|
sortType,
|
||||||
|
paginate
|
||||||
|
) => {
|
||||||
loading = true
|
loading = true
|
||||||
allRows = await API.fetchDatasource(dataSource)
|
if (dataSource?.type === "table") {
|
||||||
|
const res = await API.searchTable({
|
||||||
|
tableId: dataSource.tableId,
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
sort: sortColumn,
|
||||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
|
sortType,
|
||||||
|
paginate,
|
||||||
|
})
|
||||||
|
pageNumber = 0
|
||||||
|
allRows = res.rows
|
||||||
|
if (res.hasNextPage) {
|
||||||
|
bookmarks = [null, res.bookmark]
|
||||||
|
} else {
|
||||||
|
bookmarks = [null]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const rows = await API.fetchDatasource(dataSource)
|
||||||
|
allRows = inMemoryFilterRows(rows, filter)
|
||||||
|
}
|
||||||
loading = false
|
loading = false
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterRows = (rows, filter) => {
|
const inMemoryFilterRows = (rows, filter) => {
|
||||||
if (!Object.keys(filter || {}).length) {
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
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
|
||||||
|
@ -111,11 +202,91 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
schema = fixedSchema
|
schema = fixedSchema
|
||||||
|
schemaLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPage = async () => {
|
||||||
|
if (!hasNextPage || !internalTable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await API.searchTable({
|
||||||
|
tableId: dataSource?.tableId,
|
||||||
|
query,
|
||||||
|
bookmark: bookmarks[pageNumber + 1],
|
||||||
|
limit,
|
||||||
|
sort: sortColumn,
|
||||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
|
sortType,
|
||||||
|
paginate: true,
|
||||||
|
})
|
||||||
|
pageNumber++
|
||||||
|
allRows = res.rows
|
||||||
|
if (res.hasNextPage) {
|
||||||
|
bookmarks[pageNumber + 1] = res.bookmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevPage = async () => {
|
||||||
|
if (!hasPrevPage || !internalTable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await API.searchTable({
|
||||||
|
tableId: dataSource?.tableId,
|
||||||
|
query,
|
||||||
|
bookmark: bookmarks[pageNumber - 1],
|
||||||
|
limit,
|
||||||
|
sort: sortColumn,
|
||||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
|
sortType,
|
||||||
|
paginate: true,
|
||||||
|
})
|
||||||
|
pageNumber--
|
||||||
|
allRows = res.rows
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles} class="container">
|
||||||
<Provider {actions} data={dataContext}>
|
<Provider {actions} data={dataContext}>
|
||||||
|
{#if !loaded}
|
||||||
|
<div class="loading">
|
||||||
|
<ProgressCircle />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if paginate && internalTable}
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={pageNumber + 1}
|
||||||
|
{hasPrevPage}
|
||||||
|
{hasNextPage}
|
||||||
|
goToPrevPage={prevPage}
|
||||||
|
goToNextPage={nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -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>
|
|
|
@ -10,9 +10,9 @@
|
||||||
|
|
||||||
{#if options}
|
{#if options}
|
||||||
<div use:chart={options} use:styleable={$component.styles} />
|
<div use:chart={options} use:styleable={$component.styles} />
|
||||||
{:else if builderStore.inBuilder}
|
{:else if $builderStore.inBuilder}
|
||||||
<div use:styleable={$component.styles}>
|
<div class="placeholder" use:styleable={$component.styles}>
|
||||||
Use the settings panel to build your chart -->
|
Use the settings panel to build your chart.
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -21,4 +21,10 @@
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
div :global(.apexcharts-yaxis-label, .apexcharts-xaxis-label) {
|
||||||
|
fill: #aaa;
|
||||||
|
}
|
||||||
|
div.placeholder {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -187,5 +187,6 @@
|
||||||
div {
|
div {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,7 +27,6 @@ export { default as embed } from "./Embed.svelte"
|
||||||
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
||||||
export { default as cardstat } from "./CardStat.svelte"
|
export { default as cardstat } from "./CardStat.svelte"
|
||||||
export { default as icon } from "./Icon.svelte"
|
export { default as icon } from "./Icon.svelte"
|
||||||
export { default as search } from "./Search.svelte"
|
|
||||||
export { default as backgroundimage } from "./BackgroundImage.svelte"
|
export { default as backgroundimage } from "./BackgroundImage.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
|
|
|
@ -94,3 +94,9 @@
|
||||||
<slot />
|
<slot />
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@ const processors = require("./processors")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const {
|
const {
|
||||||
removeNull,
|
removeNull,
|
||||||
addConstants,
|
updateContext,
|
||||||
removeHandlebarsStatements,
|
removeHandlebarsStatements,
|
||||||
} = require("./utilities")
|
} = require("./utilities")
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
|
@ -92,8 +92,7 @@ module.exports.processStringSync = (string, context) => {
|
||||||
}
|
}
|
||||||
// take a copy of input incase error
|
// take a copy of input incase error
|
||||||
const input = string
|
const input = string
|
||||||
let clonedContext = removeNull(cloneDeep(context))
|
const clonedContext = removeNull(updateContext(cloneDeep(context)))
|
||||||
clonedContext = addConstants(clonedContext)
|
|
||||||
// remove any null/undefined properties
|
// remove any null/undefined properties
|
||||||
if (typeof string !== "string") {
|
if (typeof string !== "string") {
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
|
|
|
@ -23,11 +23,24 @@ module.exports.removeNull = obj => {
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.addConstants = obj => {
|
module.exports.updateContext = obj => {
|
||||||
if (obj.now == null) {
|
if (obj.now == null) {
|
||||||
obj.now = new Date()
|
obj.now = new Date().toISOString()
|
||||||
|
}
|
||||||
|
function recurse(obj) {
|
||||||
|
for (let key of Object.keys(obj)) {
|
||||||
|
if (!obj[key]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (obj[key] instanceof Date) {
|
||||||
|
obj[key] = obj[key].toISOString()
|
||||||
|
} else if (typeof obj[key] === "object") {
|
||||||
|
obj[key] = recurse(obj[key])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
|
}
|
||||||
|
return recurse(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.removeHandlebarsStatements = string => {
|
module.exports.removeHandlebarsStatements = string => {
|
||||||
|
|
|
@ -107,6 +107,12 @@ describe("check the utility functions", () => {
|
||||||
const property = makePropSafe("thing")
|
const property = makePropSafe("thing")
|
||||||
expect(property).toEqual("[thing]")
|
expect(property).toEqual("[thing]")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to handle an input date object", async () => {
|
||||||
|
const date = new Date()
|
||||||
|
const output = await processString("{{ dateObj }}", { dateObj: date })
|
||||||
|
expect(date.toISOString()).toEqual(output)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("check manifest", () => {
|
describe("check manifest", () => {
|
||||||
|
|
|
@ -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==
|
||||||
|
|
Loading…
Reference in a new issue