Merge branch 'master' of github.com:Budibase/budibase into feature/audit-log-sqs
This commit is contained in:
commit
b18ca2670a
29 changed files with 615 additions and 349 deletions
|
@ -22,4 +22,4 @@
|
||||||
"loadEnvFiles": false
|
"loadEnvFiles": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,8 +231,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const contains = (mode: object, any: boolean = false) => {
|
const contains = (mode: object, any: boolean = false) => {
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const rawFnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||||
const rawFnc = `${fnc}Raw`
|
|
||||||
const not = mode === filters?.notContains ? "NOT " : ""
|
const not = mode === filters?.notContains ? "NOT " : ""
|
||||||
function stringifyArray(value: Array<any>, quoteStyle = '"'): string {
|
function stringifyArray(value: Array<any>, quoteStyle = '"'): string {
|
||||||
for (let i in value) {
|
for (let i in value) {
|
||||||
|
@ -245,24 +244,24 @@ class InternalBuilder {
|
||||||
if (this.client === SqlClient.POSTGRES) {
|
if (this.client === SqlClient.POSTGRES) {
|
||||||
iterate(mode, (key: string, value: Array<any>) => {
|
iterate(mode, (key: string, value: Array<any>) => {
|
||||||
const wrap = any ? "" : "'"
|
const wrap = any ? "" : "'"
|
||||||
const containsOp = any ? "\\?| array" : "@>"
|
const op = any ? "\\?| array" : "@>"
|
||||||
const fieldNames = key.split(/\./g)
|
const fieldNames = key.split(/\./g)
|
||||||
const tableName = fieldNames[0]
|
const table = fieldNames[0]
|
||||||
const columnName = fieldNames[1]
|
const col = fieldNames[1]
|
||||||
// @ts-ignore
|
|
||||||
query = query[rawFnc](
|
query = query[rawFnc](
|
||||||
`${not}"${tableName}"."${columnName}"::jsonb ${containsOp} ${wrap}${stringifyArray(
|
`${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(
|
||||||
value,
|
value,
|
||||||
any ? "'" : '"'
|
any ? "'" : '"'
|
||||||
)}${wrap}`
|
)}${wrap}, FALSE)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else if (this.client === SqlClient.MY_SQL) {
|
} else if (this.client === SqlClient.MY_SQL) {
|
||||||
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
||||||
iterate(mode, (key: string, value: Array<any>) => {
|
iterate(mode, (key: string, value: Array<any>) => {
|
||||||
// @ts-ignore
|
|
||||||
query = query[rawFnc](
|
query = query[rawFnc](
|
||||||
`${not}${jsonFnc}(${key}, '${stringifyArray(value)}')`
|
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
|
||||||
|
value
|
||||||
|
)}'), FALSE)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -277,7 +276,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
statement +=
|
statement +=
|
||||||
(statement ? andOr : "") +
|
(statement ? andOr : "") +
|
||||||
`LOWER(${likeKey(this.client, key)}) LIKE ?`
|
`COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statement === "") {
|
if (statement === "") {
|
||||||
|
@ -342,14 +341,34 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
if (filters.equal) {
|
if (filters.equal) {
|
||||||
iterate(filters.equal, (key, value) => {
|
iterate(filters.equal, (key, value) => {
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||||
query = query[fnc]({ [key]: value })
|
if (this.client === SqlClient.MS_SQL) {
|
||||||
|
query = query[fnc](
|
||||||
|
`CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`,
|
||||||
|
[value]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query[fnc](
|
||||||
|
`COALESCE(${likeKey(this.client, key)} = ?, FALSE)`,
|
||||||
|
[value]
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.notEqual) {
|
if (filters.notEqual) {
|
||||||
iterate(filters.notEqual, (key, value) => {
|
iterate(filters.notEqual, (key, value) => {
|
||||||
const fnc = allOr ? "orWhereNot" : "whereNot"
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||||
query = query[fnc]({ [key]: value })
|
if (this.client === SqlClient.MS_SQL) {
|
||||||
|
query = query[fnc](
|
||||||
|
`CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`,
|
||||||
|
[value]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query[fnc](
|
||||||
|
`COALESCE(${likeKey(this.client, key)} != ?, TRUE)`,
|
||||||
|
[value]
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.empty) {
|
if (filters.empty) {
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
useAnchorWidth={!autoWidth}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : null}
|
||||||
customHeight={customPopoverHeight}
|
customHeight={customPopoverHeight}
|
||||||
maxHeight={240}
|
maxHeight={360}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
<script>
|
|
||||||
import { flip } from "svelte/animate"
|
|
||||||
import { dndzone } from "svelte-dnd-action"
|
|
||||||
import Icon from "../Icon/Icon.svelte"
|
|
||||||
import Popover from "../Popover/Popover.svelte"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
const flipDurationMs = 150
|
|
||||||
|
|
||||||
export let constraints
|
|
||||||
export let optionColors = {}
|
|
||||||
let options = []
|
|
||||||
|
|
||||||
let colorPopovers = []
|
|
||||||
let anchors = []
|
|
||||||
|
|
||||||
let colorsArray = [
|
|
||||||
"hsla(0, 90%, 75%, 0.3)",
|
|
||||||
"hsla(50, 80%, 75%, 0.3)",
|
|
||||||
"hsla(120, 90%, 75%, 0.3)",
|
|
||||||
"hsla(200, 90%, 75%, 0.3)",
|
|
||||||
"hsla(240, 90%, 75%, 0.3)",
|
|
||||||
"hsla(320, 90%, 75%, 0.3)",
|
|
||||||
]
|
|
||||||
const removeInput = idx => {
|
|
||||||
delete optionColors[options[idx].name]
|
|
||||||
constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx)
|
|
||||||
options = options.filter((e, i) => i !== idx)
|
|
||||||
colorPopovers.pop(undefined)
|
|
||||||
anchors.pop(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addNewInput = () => {
|
|
||||||
options = [
|
|
||||||
...options,
|
|
||||||
{ name: `Option ${constraints.inclusion.length + 1}`, id: Math.random() },
|
|
||||||
]
|
|
||||||
constraints.inclusion = [
|
|
||||||
...constraints.inclusion,
|
|
||||||
`Option ${constraints.inclusion.length + 1}`,
|
|
||||||
]
|
|
||||||
|
|
||||||
colorPopovers.push(undefined)
|
|
||||||
anchors.push(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDndConsider = e => {
|
|
||||||
options = e.detail.items
|
|
||||||
}
|
|
||||||
const handleDndFinalize = e => {
|
|
||||||
options = e.detail.items
|
|
||||||
constraints.inclusion = options.map(option => option.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleColorChange = (optionName, color, idx) => {
|
|
||||||
optionColors[optionName] = color
|
|
||||||
colorPopovers[idx].hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNameChange = (optionName, idx, value) => {
|
|
||||||
constraints.inclusion[idx] = value
|
|
||||||
options[idx].name = value
|
|
||||||
optionColors[value] = optionColors[optionName]
|
|
||||||
delete optionColors[optionName]
|
|
||||||
}
|
|
||||||
|
|
||||||
const openColorPickerPopover = (optionIdx, target) => {
|
|
||||||
colorPopovers[optionIdx].show()
|
|
||||||
anchors[optionIdx] = target
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Initialize anchor arrays on mount, assuming 'options' is already populated
|
|
||||||
colorPopovers = constraints.inclusion.map(() => undefined)
|
|
||||||
anchors = constraints.inclusion.map(() => undefined)
|
|
||||||
|
|
||||||
options = constraints.inclusion.map(value => ({
|
|
||||||
name: value,
|
|
||||||
id: Math.random(),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="actions"
|
|
||||||
use:dndzone={{
|
|
||||||
items: options,
|
|
||||||
flipDurationMs,
|
|
||||||
dropTargetStyle: { outline: "none" },
|
|
||||||
}}
|
|
||||||
on:consider={handleDndConsider}
|
|
||||||
on:finalize={handleDndFinalize}
|
|
||||||
>
|
|
||||||
{#each options as option, idx (option.id)}
|
|
||||||
<div
|
|
||||||
class="no-border action-container"
|
|
||||||
animate:flip={{ duration: flipDurationMs }}
|
|
||||||
>
|
|
||||||
<div class="child drag-handle-spacing">
|
|
||||||
<Icon name="DragHandle" size="L" />
|
|
||||||
</div>
|
|
||||||
<div class="child color-picker">
|
|
||||||
<div
|
|
||||||
id="color-picker"
|
|
||||||
bind:this={anchors[idx]}
|
|
||||||
style="--color:{optionColors?.[option.name] ||
|
|
||||||
'hsla(0, 1%, 50%, 0.3)'}"
|
|
||||||
class="circle"
|
|
||||||
on:click={e => openColorPickerPopover(idx, e.target)}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
bind:this={colorPopovers[idx]}
|
|
||||||
anchor={anchors[idx]}
|
|
||||||
align="left"
|
|
||||||
offset={0}
|
|
||||||
style=""
|
|
||||||
popoverTarget={document.getElementById(`color-picker`)}
|
|
||||||
animate={false}
|
|
||||||
>
|
|
||||||
<div class="colors">
|
|
||||||
{#each colorsArray as color}
|
|
||||||
<div
|
|
||||||
on:click={() => handleColorChange(option.name, color, idx)}
|
|
||||||
style="--color:{color};"
|
|
||||||
class="circle circle-hover"
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="child">
|
|
||||||
<input
|
|
||||||
class="input-field"
|
|
||||||
type="text"
|
|
||||||
on:change={e => handleNameChange(option.name, idx, e.target.value)}
|
|
||||||
value={option.name}
|
|
||||||
placeholder="Option name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="child">
|
|
||||||
<Icon name="Close" hoverable size="S" on:click={removeInput(idx)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div on:click={addNewInput} class="add-option">
|
|
||||||
<Icon hoverable name="Add" />
|
|
||||||
<div>Add option</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-container {
|
|
||||||
background-color: var(--spectrum-alias-background-color-primary);
|
|
||||||
border-radius: 0px;
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
|
||||||
border-color 130ms ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.no-border {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container:last-child {
|
|
||||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.child {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
.child:hover,
|
|
||||||
.child:focus {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
.add-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.child input[type="text"] {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field:hover,
|
|
||||||
.input-field:focus {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container > :nth-child(1) {
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container > :nth-child(2) {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container > :nth-child(3) {
|
|
||||||
flex-grow: 4;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.action-container > :nth-child(4) {
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background-color: var(--color);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-hover:hover {
|
|
||||||
border: 1px solid var(--spectrum-global-color-blue-400);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colors {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
justify-items: center;
|
|
||||||
margin: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -89,7 +89,6 @@ export { default as ListItem } from "./List/ListItem.svelte"
|
||||||
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
||||||
export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.svelte"
|
|
||||||
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
|
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
|
||||||
export { TooltipPosition, TooltipType } from "./Tooltip/AbsTooltip.svelte"
|
export { TooltipPosition, TooltipType } from "./Tooltip/AbsTooltip.svelte"
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "bb_reference"}
|
{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
linkedRows={value[field]}
|
linkedRows={value[field]}
|
||||||
{schema}
|
{schema}
|
||||||
|
|
|
@ -92,7 +92,6 @@
|
||||||
/>
|
/>
|
||||||
{:else if type === "attachment"}
|
{:else if type === "attachment"}
|
||||||
<Dropzone
|
<Dropzone
|
||||||
compact
|
|
||||||
{label}
|
{label}
|
||||||
{error}
|
{error}
|
||||||
{value}
|
{value}
|
||||||
|
@ -102,7 +101,6 @@
|
||||||
/>
|
/>
|
||||||
{:else if type === "attachment_single"}
|
{:else if type === "attachment_single"}
|
||||||
<Dropzone
|
<Dropzone
|
||||||
compact
|
|
||||||
{label}
|
{label}
|
||||||
{error}
|
{error}
|
||||||
value={value ? [value] : []}
|
value={value ? [value] : []}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
OptionSelectDnD,
|
|
||||||
Layout,
|
Layout,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
@ -42,6 +41,7 @@
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
import { RowUtils } from "@budibase/frontend-core"
|
import { RowUtils } from "@budibase/frontend-core"
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import OptionsEditor from "./OptionsEditor.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = FieldType.AUTO
|
const AUTO_TYPE = FieldType.AUTO
|
||||||
const FORMULA_TYPE = FieldType.FORMULA
|
const FORMULA_TYPE = FieldType.FORMULA
|
||||||
|
@ -95,6 +95,7 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
let autoColumnInfo = getAutoColumnInformation()
|
let autoColumnInfo = getAutoColumnInformation()
|
||||||
|
let optionsValid = true
|
||||||
|
|
||||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||||
$: if (primaryDisplay) {
|
$: if (primaryDisplay) {
|
||||||
|
@ -138,7 +139,8 @@
|
||||||
$: invalid =
|
$: invalid =
|
||||||
!editableColumn?.name ||
|
!editableColumn?.name ||
|
||||||
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
||||||
Object.keys(errors).length !== 0
|
Object.keys(errors).length !== 0 ||
|
||||||
|
!optionsValid
|
||||||
$: errors = checkErrors(editableColumn)
|
$: errors = checkErrors(editableColumn)
|
||||||
$: datasource = $datasources.list.find(
|
$: datasource = $datasources.list.find(
|
||||||
source => source._id === table?.sourceId
|
source => source._id === table?.sourceId
|
||||||
|
@ -559,9 +561,10 @@
|
||||||
bind:value={editableColumn.constraints.length.maximum}
|
bind:value={editableColumn.constraints.length.maximum}
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === FieldType.OPTIONS}
|
{:else if editableColumn.type === FieldType.OPTIONS}
|
||||||
<OptionSelectDnD
|
<OptionsEditor
|
||||||
bind:constraints={editableColumn.constraints}
|
bind:constraints={editableColumn.constraints}
|
||||||
bind:optionColors={editableColumn.optionColors}
|
bind:optionColors={editableColumn.optionColors}
|
||||||
|
bind:valid={optionsValid}
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === FieldType.LONGFORM}
|
{:else if editableColumn.type === FieldType.LONGFORM}
|
||||||
<div>
|
<div>
|
||||||
|
@ -582,9 +585,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if editableColumn.type === FieldType.ARRAY}
|
{:else if editableColumn.type === FieldType.ARRAY}
|
||||||
<OptionSelectDnD
|
<OptionsEditor
|
||||||
bind:constraints={editableColumn.constraints}
|
bind:constraints={editableColumn.constraints}
|
||||||
bind:optionColors={editableColumn.optionColors}
|
bind:optionColors={editableColumn.optionColors}
|
||||||
|
bind:valid={optionsValid}
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn}
|
{:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn}
|
||||||
<div class="split-label">
|
<div class="split-label">
|
||||||
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
<script>
|
||||||
|
import { flip } from "svelte/animate"
|
||||||
|
import { dndzone } from "svelte-dnd-action"
|
||||||
|
import { Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { tick } from "svelte"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import { getSequentialName } from "helpers/duplicate"
|
||||||
|
import { derived, writable } from "svelte/store"
|
||||||
|
|
||||||
|
export let constraints
|
||||||
|
export let optionColors = {}
|
||||||
|
export let valid = true
|
||||||
|
|
||||||
|
const flipDurationMs = 130
|
||||||
|
const { OptionColours } = Constants
|
||||||
|
const getDefaultColor = idx => OptionColours[idx % OptionColours.length]
|
||||||
|
const options = writable(
|
||||||
|
constraints.inclusion.map((value, idx) => ({
|
||||||
|
id: Math.random(),
|
||||||
|
name: value,
|
||||||
|
color: optionColors?.[value] || getDefaultColor(idx),
|
||||||
|
invalid: false,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const enrichedOptions = derived(options, $options => {
|
||||||
|
let enriched = []
|
||||||
|
$options.forEach(option => {
|
||||||
|
enriched.push({
|
||||||
|
...option,
|
||||||
|
valid: option.name && !enriched.some(opt => opt.name === option.name),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return enriched
|
||||||
|
})
|
||||||
|
|
||||||
|
let openOption = null
|
||||||
|
let anchor = null
|
||||||
|
|
||||||
|
$: options.subscribe(updateConstraints)
|
||||||
|
$: valid = $enrichedOptions.every(option => option.valid)
|
||||||
|
|
||||||
|
const updateConstraints = options => {
|
||||||
|
constraints.inclusion = options.map(option => option.name)
|
||||||
|
optionColors = options.reduce(
|
||||||
|
(colors, option) => ({ ...colors, [option.name]: option.color }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewInput = async () => {
|
||||||
|
const newId = Math.random()
|
||||||
|
const newName = getSequentialName($options, "Option ", {
|
||||||
|
numberFirstItem: true,
|
||||||
|
getName: option => option.name,
|
||||||
|
})
|
||||||
|
options.update(state => {
|
||||||
|
return [
|
||||||
|
...state,
|
||||||
|
{
|
||||||
|
name: newName,
|
||||||
|
id: newId,
|
||||||
|
color: getDefaultColor(state.length),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Focus new option
|
||||||
|
await tick()
|
||||||
|
document.getElementById(`option-${newId}`)?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeInput = id => {
|
||||||
|
options.update(state => state.filter(option => option.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const openColorPicker = id => {
|
||||||
|
anchor = document.getElementById(`color-${id}`)
|
||||||
|
openOption = id
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColorChange = (id, color) => {
|
||||||
|
options.update(state => {
|
||||||
|
state.find(option => option.id === id).color = color
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
openOption = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNameChange = (id, name) => {
|
||||||
|
options.update(state => {
|
||||||
|
state.find(option => option.id === id).name = name
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="wrapper">
|
||||||
|
<div
|
||||||
|
class="options"
|
||||||
|
use:dndzone={{
|
||||||
|
items: $options,
|
||||||
|
flipDurationMs,
|
||||||
|
dropTargetStyle: { outline: "none" },
|
||||||
|
}}
|
||||||
|
on:consider={e => options.set(e.detail.items)}
|
||||||
|
on:finalize={e => options.set(e.detail.items)}
|
||||||
|
>
|
||||||
|
{#each $enrichedOptions as option (option.id)}
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
animate:flip={{ duration: flipDurationMs }}
|
||||||
|
class:invalid={!option.valid}
|
||||||
|
>
|
||||||
|
<div class="drag-handle">
|
||||||
|
<Icon name="DragHandle" size="L" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="color-{option.id}"
|
||||||
|
class="color-picker"
|
||||||
|
on:click={() => openColorPicker(option.id)}
|
||||||
|
>
|
||||||
|
<div class="circle" style="--color:{option.color}">
|
||||||
|
<Popover
|
||||||
|
open={openOption === option.id}
|
||||||
|
{anchor}
|
||||||
|
align="left"
|
||||||
|
offset={0}
|
||||||
|
animate={false}
|
||||||
|
resizable={false}
|
||||||
|
>
|
||||||
|
<div class="colors" data-ignore-click-outside="true">
|
||||||
|
{#each OptionColours as colorOption}
|
||||||
|
<div
|
||||||
|
on:click={() => handleColorChange(option.id, colorOption)}
|
||||||
|
style="--color:{colorOption};"
|
||||||
|
class="circle"
|
||||||
|
class:selected={colorOption === option.color}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="option-name"
|
||||||
|
type="text"
|
||||||
|
value={option.name}
|
||||||
|
placeholder="Option name"
|
||||||
|
id="option-{option.id}"
|
||||||
|
on:input={e => handleNameChange(option.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => removeInput(option.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div on:click={addNewInput} class="add-option">
|
||||||
|
<Icon name="Add" />
|
||||||
|
<div>Add option</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Container */
|
||||||
|
.wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
}
|
||||||
|
.options > *,
|
||||||
|
.add-option {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Options row */
|
||||||
|
.option {
|
||||||
|
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||||
|
border-color 130ms ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
padding: 0 var(--spacing-m) 0 var(--spacing-s);
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.option.invalid {
|
||||||
|
border: 1px solid var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
.option:not(.invalid):hover,
|
||||||
|
.option:not(.invalid):focus {
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Option row components */
|
||||||
|
.color-picker {
|
||||||
|
align-self: stretch;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.circle {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: var(--color);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: border 130ms ease-out;
|
||||||
|
}
|
||||||
|
.circle:hover,
|
||||||
|
.circle.selected {
|
||||||
|
border: 1px solid var(--spectrum-global-color-blue-600);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.colors {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
justify-items: center;
|
||||||
|
margin: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.option-name {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add option */
|
||||||
|
.add-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.add-option:hover {
|
||||||
|
cursor: pointer !important;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -43,7 +43,7 @@
|
||||||
<b>{linkedTable.name}</b>
|
<b>{linkedTable.name}</b>
|
||||||
table.
|
table.
|
||||||
</Label>
|
</Label>
|
||||||
{:else if schema.relationshipType === "one-to-many"}
|
{:else if schema.relationshipType === "one-to-many" || schema.type === "bb_reference_single"}
|
||||||
<Select
|
<Select
|
||||||
value={linkedIds?.[0]}
|
value={linkedIds?.[0]}
|
||||||
options={rows}
|
options={rows}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { AbsTooltip, Icon } from "@budibase/bbui"
|
import { Icon, TooltipType, TooltipPosition } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
@ -114,9 +114,14 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<div class="icon" class:right={rightAlignIcon}>
|
<div class="icon" class:right={rightAlignIcon}>
|
||||||
<AbsTooltip type="info" position="right" text={iconTooltip}>
|
<Icon
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
color={iconColor}
|
||||||
</AbsTooltip>
|
size="S"
|
||||||
|
name={icon}
|
||||||
|
tooltip={iconTooltip}
|
||||||
|
tooltipType={TooltipType.Info}
|
||||||
|
tooltipPosition={TooltipPosition.Right}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text" title={showTooltip ? text : null}>
|
<div class="text" title={showTooltip ? text : null}>
|
||||||
|
|
|
@ -70,13 +70,18 @@ export const duplicateName = (name, allNames) => {
|
||||||
* @param getName optional function to extract the name for an item, if not a
|
* @param getName optional function to extract the name for an item, if not a
|
||||||
* flat array of strings
|
* flat array of strings
|
||||||
*/
|
*/
|
||||||
export const getSequentialName = (items, prefix, getName = x => x) => {
|
export const getSequentialName = (
|
||||||
|
items,
|
||||||
|
prefix,
|
||||||
|
{ getName = x => x, numberFirstItem = false } = {}
|
||||||
|
) => {
|
||||||
if (!prefix?.length || !getName) {
|
if (!prefix?.length || !getName) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const trimmedPrefix = prefix.trim()
|
const trimmedPrefix = prefix.trim()
|
||||||
|
const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix
|
||||||
if (!items?.length) {
|
if (!items?.length) {
|
||||||
return trimmedPrefix
|
return firstName
|
||||||
}
|
}
|
||||||
let max = 0
|
let max = 0
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
|
@ -96,5 +101,5 @@ export const getSequentialName = (items, prefix, getName = x => x) => {
|
||||||
max = num
|
max = num
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return max === 0 ? trimmedPrefix : `${prefix}${max + 1}`
|
return max === 0 ? firstName : `${prefix}${max + 1}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,61 +43,71 @@ describe("duplicate", () => {
|
||||||
|
|
||||||
describe("getSequentialName", () => {
|
describe("getSequentialName", () => {
|
||||||
it("handles nullish items", async () => {
|
it("handles nullish items", async () => {
|
||||||
const name = getSequentialName(null, "foo", () => {})
|
const name = getSequentialName(null, "foo")
|
||||||
expect(name).toBe("foo")
|
expect(name).toBe("foo")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles nullish prefix", async () => {
|
it("handles nullish prefix", async () => {
|
||||||
const name = getSequentialName([], null, () => {})
|
const name = getSequentialName([], null)
|
||||||
expect(name).toBe(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("handles nullish getName function", async () => {
|
|
||||||
const name = getSequentialName([], "foo", null)
|
|
||||||
expect(name).toBe(null)
|
expect(name).toBe(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles just the prefix", async () => {
|
it("handles just the prefix", async () => {
|
||||||
const name = getSequentialName(["foo"], "foo", x => x)
|
const name = getSequentialName(["foo"], "foo")
|
||||||
expect(name).toBe("foo2")
|
expect(name).toBe("foo2")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles continuous ranges", async () => {
|
it("handles continuous ranges", async () => {
|
||||||
const name = getSequentialName(["foo", "foo2", "foo3"], "foo", x => x)
|
const name = getSequentialName(["foo", "foo2", "foo3"], "foo")
|
||||||
expect(name).toBe("foo4")
|
expect(name).toBe("foo4")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles discontinuous ranges", async () => {
|
it("handles discontinuous ranges", async () => {
|
||||||
const name = getSequentialName(["foo", "foo3"], "foo", x => x)
|
const name = getSequentialName(["foo", "foo3"], "foo")
|
||||||
expect(name).toBe("foo4")
|
expect(name).toBe("foo4")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles a space inside the prefix", async () => {
|
it("handles a space inside the prefix", async () => {
|
||||||
const name = getSequentialName(["foo", "foo 2", "foo 3"], "foo ", x => x)
|
const name = getSequentialName(["foo", "foo 2", "foo 3"], "foo ")
|
||||||
expect(name).toBe("foo 4")
|
expect(name).toBe("foo 4")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles a space inside the prefix with just the prefix", async () => {
|
it("handles a space inside the prefix with just the prefix", async () => {
|
||||||
const name = getSequentialName(["foo"], "foo ", x => x)
|
const name = getSequentialName(["foo"], "foo ")
|
||||||
expect(name).toBe("foo 2")
|
expect(name).toBe("foo 2")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles no matches", async () => {
|
it("handles no matches", async () => {
|
||||||
const name = getSequentialName(["aaa", "bbb"], "foo", x => x)
|
const name = getSequentialName(["aaa", "bbb"], "foo")
|
||||||
expect(name).toBe("foo")
|
expect(name).toBe("foo")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles similar names", async () => {
|
it("handles similar names", async () => {
|
||||||
const name = getSequentialName(
|
const name = getSequentialName(["fooo1", "2foo", "a3foo4", "5foo5"], "foo")
|
||||||
["fooo1", "2foo", "a3foo4", "5foo5"],
|
|
||||||
"foo",
|
|
||||||
x => x
|
|
||||||
)
|
|
||||||
expect(name).toBe("foo")
|
expect(name).toBe("foo")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles non-string names", async () => {
|
it("handles non-string names", async () => {
|
||||||
const name = getSequentialName([null, 4123, [], {}], "foo", x => x)
|
const name = getSequentialName([null, 4123, [], {}], "foo")
|
||||||
expect(name).toBe("foo")
|
expect(name).toBe("foo")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("handles deep getters", async () => {
|
||||||
|
const name = getSequentialName([{ a: "foo 1" }], "foo ", {
|
||||||
|
getName: x => x.a,
|
||||||
|
})
|
||||||
|
expect(name).toBe("foo 2")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles a mixture of spaces and not", async () => {
|
||||||
|
const name = getSequentialName(["foo", "foo 1", "foo 2"], "foo")
|
||||||
|
expect(name).toBe("foo3")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles numbering the first item", async () => {
|
||||||
|
const name = getSequentialName(["foo1", "foo2", "foo"], "foo ", {
|
||||||
|
numberFirstItem: true,
|
||||||
|
})
|
||||||
|
expect(name).toBe("foo 3")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,7 +48,9 @@
|
||||||
...navItems,
|
...navItems,
|
||||||
{
|
{
|
||||||
id: generate(),
|
id: generate(),
|
||||||
text: getSequentialName(navItems, "Nav Item ", x => x.text),
|
text: getSequentialName(navItems, "Nav Item ", {
|
||||||
|
getName: x => x.text,
|
||||||
|
}),
|
||||||
url: "",
|
url: "",
|
||||||
roleId: Constants.Roles.BASIC,
|
roleId: Constants.Roles.BASIC,
|
||||||
type: "link",
|
type: "link",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
filter,
|
filter,
|
||||||
inlineFilters,
|
inlineFilters,
|
||||||
|
keyboardBlocked,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
const searchableTypes = [
|
const searchableTypes = [
|
||||||
|
@ -57,6 +58,8 @@
|
||||||
$: searching = searchValue != null
|
$: searching = searchValue != null
|
||||||
$: debouncedUpdateFilter(searchValue)
|
$: debouncedUpdateFilter(searchValue)
|
||||||
$: orderable = !column.primaryDisplay
|
$: orderable = !column.primaryDisplay
|
||||||
|
$: editable = $config.canEditColumns && !column.schema.disabled
|
||||||
|
$: keyboardBlocked.set(open)
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
open = false
|
open = false
|
||||||
|
@ -231,6 +234,14 @@
|
||||||
}
|
}
|
||||||
const debouncedUpdateFilter = debounce(updateFilter, 250)
|
const debouncedUpdateFilter = debounce(updateFilter, 250)
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
if (!editable || searching) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
open = true
|
||||||
|
editColumn()
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => subscribe("close-edit-column", close))
|
onMount(() => subscribe("close-edit-column", close))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -241,14 +252,15 @@
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
|
bind:this={anchor}
|
||||||
class="header-cell"
|
class="header-cell"
|
||||||
|
style="flex: 0 0 {column.width}px;"
|
||||||
class:open
|
class:open
|
||||||
class:searchable
|
class:searchable
|
||||||
class:searching
|
class:searching
|
||||||
style="flex: 0 0 {column.width}px;"
|
|
||||||
bind:this={anchor}
|
|
||||||
class:disabled={$isReordering || $isResizing}
|
class:disabled={$isReordering || $isResizing}
|
||||||
class:sticky={idx === "sticky"}
|
class:sticky={idx === "sticky"}
|
||||||
|
on:dblclick={handleDoubleClick}
|
||||||
>
|
>
|
||||||
<GridCell
|
<GridCell
|
||||||
on:mousedown={onMouseDown}
|
on:mousedown={onMouseDown}
|
||||||
|
@ -311,7 +323,7 @@
|
||||||
{#if open}
|
{#if open}
|
||||||
<GridPopover
|
<GridPopover
|
||||||
{anchor}
|
{anchor}
|
||||||
align="right"
|
align="left"
|
||||||
on:close={close}
|
on:close={close}
|
||||||
maxHeight={null}
|
maxHeight={null}
|
||||||
resizable
|
resizable
|
||||||
|
@ -322,11 +334,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem
|
<MenuItem icon="Edit" on:click={editColumn} disabled={!editable}>
|
||||||
icon="Edit"
|
|
||||||
on:click={editColumn}
|
|
||||||
disabled={!$config.canEditColumns || column.schema.disabled}
|
|
||||||
>
|
|
||||||
Edit column
|
Edit column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { getColor } from "../lib/utils"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import GridPopover from "../overlays/GridPopover.svelte"
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
import { OptionColours } from "../../../constants"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -13,6 +13,8 @@
|
||||||
export let api
|
export let api
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
|
|
||||||
|
const InvalidColor = "hsla(0, 0%, 70%, 0.3)"
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let focusedOptionIdx = null
|
let focusedOptionIdx = null
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -38,8 +40,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptionColor = value => {
|
const getOptionColor = value => {
|
||||||
const index = value ? options.indexOf(value) : null
|
let idx = value ? options.indexOf(value) : null
|
||||||
return getColor(index)
|
if (idx == null || idx === -1) {
|
||||||
|
return InvalidColor
|
||||||
|
}
|
||||||
|
return OptionColours[idx % OptionColours.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleOption = option => {
|
const toggleOption = option => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { getColor } from "../lib/utils"
|
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { Icon, Input, ProgressCircle } from "@budibase/bbui"
|
import { Icon, Input, ProgressCircle } from "@budibase/bbui"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
import GridPopover from "../overlays/GridPopover.svelte"
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
import { OptionColours } from "../../../constants"
|
||||||
|
|
||||||
const { API, cache } = getContext("grid")
|
const { API, cache } = getContext("grid")
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
export let hideCounter = false
|
export let hideCounter = false
|
||||||
|
|
||||||
const color = getColor(0)
|
const color = OptionColours[0]
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let searchResults
|
let searchResults
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import GridPopover from "../overlays/GridPopover.svelte"
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
const { visibleColumns, scroll, width, subscribe, ui } = getContext("grid")
|
const { visibleColumns, scroll, width, subscribe, ui, keyboardBlocked } =
|
||||||
|
getContext("grid")
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
)
|
)
|
||||||
$: end = columnsWidth - 1 - $scroll.left
|
$: end = columnsWidth - 1 - $scroll.left
|
||||||
$: left = Math.min($width - 40, end)
|
$: left = Math.min($width - 40, end)
|
||||||
|
$: keyboardBlocked.set(isOpen)
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
ui.actions.blur()
|
ui.actions.blur()
|
||||||
|
|
|
@ -209,7 +209,7 @@
|
||||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $visibleColumns as column}
|
{#each $visibleColumns as column}
|
||||||
{@const cellId = `new-${column.name}`}
|
{@const cellId = getCellID(NewRowID, column.name)}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
{column}
|
{column}
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
<div class="content">
|
||||||
<GridScrollWrapper scrollVertically attachHandlers>
|
<GridScrollWrapper scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
|
|
|
@ -18,13 +18,6 @@ export const getCellID = (rowId, fieldName) => {
|
||||||
return `${rowId}${JOINING_CHARACTER}${fieldName}`
|
return `${rowId}${JOINING_CHARACTER}${fieldName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getColor = (idx, opacity = 0.3) => {
|
|
||||||
if (idx == null || idx === -1) {
|
|
||||||
idx = 0
|
|
||||||
}
|
|
||||||
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getColumnIcon = column => {
|
export const getColumnIcon = column => {
|
||||||
if (column.schema.autocolumn) {
|
if (column.schema.autocolumn) {
|
||||||
return "MagicWand"
|
return "MagicWand"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
config,
|
config,
|
||||||
menu,
|
menu,
|
||||||
gridFocused,
|
gridFocused,
|
||||||
|
keyboardBlocked,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
const ignoredOriginSelectors = [
|
const ignoredOriginSelectors = [
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
// Global key listener which intercepts all key events
|
// Global key listener which intercepts all key events
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
// Ignore completely if the grid is not focused
|
// Ignore completely if the grid is not focused
|
||||||
if (!$gridFocused) {
|
if (!$gridFocused || $keyboardBlocked) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const createStores = context => {
|
||||||
const previousFocusedRowId = writable(null)
|
const previousFocusedRowId = writable(null)
|
||||||
const previousFocusedCellId = writable(null)
|
const previousFocusedCellId = writable(null)
|
||||||
const gridFocused = writable(false)
|
const gridFocused = writable(false)
|
||||||
|
const keyboardBlocked = writable(false)
|
||||||
const isDragging = writable(false)
|
const isDragging = writable(false)
|
||||||
const buttonColumnWidth = writable(0)
|
const buttonColumnWidth = writable(0)
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ export const createStores = context => {
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
rowHeight,
|
rowHeight,
|
||||||
gridFocused,
|
gridFocused,
|
||||||
|
keyboardBlocked,
|
||||||
isDragging,
|
isDragging,
|
||||||
buttonColumnWidth,
|
buttonColumnWidth,
|
||||||
selectedRows: {
|
selectedRows: {
|
||||||
|
|
|
@ -141,3 +141,7 @@ export const TypeIconMap = {
|
||||||
[BBReferenceFieldSubType.USER]: "User",
|
[BBReferenceFieldSubType.USER]: "User",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const OptionColours = [...new Array(12).keys()].map(idx => {
|
||||||
|
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, 0.3)`
|
||||||
|
})
|
||||||
|
|
|
@ -57,5 +57,5 @@ export function isFormat(format: any): format is Format {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCsvExport<T>(value: string) {
|
export function parseCsvExport<T>(value: string) {
|
||||||
return JSON.parse(value?.replace(/'/g, '"')) as T
|
return JSON.parse(value) as T
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { tableForDatasource } from "../../../tests/utilities/structures"
|
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore, utils } from "@budibase/backend-core"
|
||||||
|
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import {
|
import {
|
||||||
|
@ -87,21 +87,67 @@ describe.each([
|
||||||
class SearchAssertion {
|
class SearchAssertion {
|
||||||
constructor(private readonly query: RowSearchParams) {}
|
constructor(private readonly query: RowSearchParams) {}
|
||||||
|
|
||||||
private popRow(expectedRow: any, foundRows: any[]) {
|
// We originally used _.isMatch to compare rows, but found that when
|
||||||
const row = foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
|
// comparing arrays it would return true if the source array was a subset of
|
||||||
|
// the target array. This would sometimes create false matches. This
|
||||||
|
// function is a more strict version of _.isMatch that only returns true if
|
||||||
|
// the source array is an exact match of the target.
|
||||||
|
//
|
||||||
|
// _.isMatch("100", "1") also returns true which is not what we want.
|
||||||
|
private isMatch<T extends Record<string, any>>(expected: T, found: T) {
|
||||||
|
if (!expected) {
|
||||||
|
throw new Error("Expected is undefined")
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(expected)) {
|
||||||
|
if (Array.isArray(expected[key])) {
|
||||||
|
if (!Array.isArray(found[key])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (expected[key].length !== found[key].length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!_.isMatch(found[key], expected[key])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (typeof expected[key] === "object") {
|
||||||
|
if (!this.isMatch(expected[key], found[key])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (expected[key] !== found[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function exists to ensure that the same row is not matched twice.
|
||||||
|
// When a row gets matched, we make sure to remove it from the list of rows
|
||||||
|
// we're matching against.
|
||||||
|
private popRow<T extends { [key: string]: any }>(
|
||||||
|
expectedRow: T,
|
||||||
|
foundRows: T[]
|
||||||
|
): NonNullable<T> {
|
||||||
|
const row = foundRows.find(row => this.isMatch(expectedRow, row))
|
||||||
if (!row) {
|
if (!row) {
|
||||||
const fields = Object.keys(expectedRow)
|
const fields = Object.keys(expectedRow)
|
||||||
// To make the error message more readable, we only include the fields
|
// To make the error message more readable, we only include the fields
|
||||||
// that are present in the expected row.
|
// that are present in the expected row.
|
||||||
const searchedObjects = foundRows.map(row => _.pick(row, fields))
|
const searchedObjects = foundRows.map(row => _.pick(row, fields))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to find row: ${JSON.stringify(
|
`Failed to find row:\n\n${JSON.stringify(
|
||||||
expectedRow
|
expectedRow,
|
||||||
)} in ${JSON.stringify(searchedObjects)}`
|
null,
|
||||||
|
2
|
||||||
|
)}\n\nin\n\n${JSON.stringify(searchedObjects, null, 2)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensuring the same row is not matched twice
|
|
||||||
foundRows.splice(foundRows.indexOf(row), 1)
|
foundRows.splice(foundRows.indexOf(row), 1)
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
@ -1055,6 +1101,7 @@ describe.each([
|
||||||
describe("notEqual", () => {
|
describe("notEqual", () => {
|
||||||
it("successfully finds a row", () =>
|
it("successfully finds a row", () =>
|
||||||
expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([
|
expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([
|
||||||
|
{ timeid: NULL_TIME__ID },
|
||||||
{ time: "10:45:00" },
|
{ time: "10:45:00" },
|
||||||
{ time: "12:00:00" },
|
{ time: "12:00:00" },
|
||||||
{ time: "15:30:00" },
|
{ time: "15:30:00" },
|
||||||
|
@ -1064,6 +1111,7 @@ describe.each([
|
||||||
it("return all when requesting non-existing", () =>
|
it("return all when requesting non-existing", () =>
|
||||||
expectQuery({ notEqual: { time: UNEXISTING_TIME } }).toContainExactly(
|
expectQuery({ notEqual: { time: UNEXISTING_TIME } }).toContainExactly(
|
||||||
[
|
[
|
||||||
|
{ timeid: NULL_TIME__ID },
|
||||||
{ time: "10:00:00" },
|
{ time: "10:00:00" },
|
||||||
{ time: "10:45:00" },
|
{ time: "10:45:00" },
|
||||||
{ time: "12:00:00" },
|
{ time: "12:00:00" },
|
||||||
|
@ -1530,14 +1578,169 @@ describe.each([
|
||||||
await createRows([{ "1:name": "bar" }, { "1:name": "foo" }])
|
await createRows([{ "1:name": "bar" }, { "1:name": "foo" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ equal: { "1:1:name": "bar" } }).toContainExactly([
|
||||||
|
{ "1:name": "bar" },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("user", () => {
|
||||||
|
let user1: User
|
||||||
|
let user2: User
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
user1 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||||||
|
user2 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||||||
|
|
||||||
|
table = await createTable({
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await createRows([
|
||||||
|
{ user: JSON.stringify(user1) },
|
||||||
|
{ user: JSON.stringify(user2) },
|
||||||
|
{ user: null },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
describe("equal", () => {
|
describe("equal", () => {
|
||||||
it("successfully finds a row", () =>
|
it("successfully finds a row", () =>
|
||||||
expectQuery({ equal: { "1:1:name": "bar" } }).toContainExactly([
|
expectQuery({ equal: { user: user1._id } }).toContainExactly([
|
||||||
{ "1:name": "bar" },
|
{ user: { _id: user1._id } },
|
||||||
]))
|
]))
|
||||||
|
|
||||||
it("fails to find nonexistent row", () =>
|
it("fails to find nonexistent row", () =>
|
||||||
expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing())
|
expectQuery({ equal: { user: "us_none" } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notEqual", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ notEqual: { user: user1._id } }).toContainExactly([
|
||||||
|
{ user: { _id: user2._id } },
|
||||||
|
{},
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ notEqual: { user: "us_none" } }).toContainExactly([
|
||||||
|
{ user: { _id: user1._id } },
|
||||||
|
{ user: { _id: user2._id } },
|
||||||
|
{},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("oneOf", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ oneOf: { user: [user1._id] } }).toContainExactly([
|
||||||
|
{ user: { _id: user1._id } },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("empty", () => {
|
||||||
|
it("finds empty rows", () =>
|
||||||
|
expectQuery({ empty: { user: null } }).toContainExactly([{}]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notEmpty", () => {
|
||||||
|
it("finds non-empty rows", () =>
|
||||||
|
expectQuery({ notEmpty: { user: null } }).toContainExactly([
|
||||||
|
{ user: { _id: user1._id } },
|
||||||
|
{ user: { _id: user2._id } },
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("multi user", () => {
|
||||||
|
let user1: User
|
||||||
|
let user2: User
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
user1 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||||||
|
user2 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||||||
|
|
||||||
|
table = await createTable({
|
||||||
|
users: {
|
||||||
|
name: "users",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
constraints: { type: "array" },
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
name: "number",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await createRows([
|
||||||
|
{ number: 1, users: JSON.stringify([user1]) },
|
||||||
|
{ number: 2, users: JSON.stringify([user2]) },
|
||||||
|
{ number: 3, users: JSON.stringify([user1, user2]) },
|
||||||
|
{ number: 4, users: JSON.stringify([]) },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("contains", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ contains: { users: [user1._id] } }).toContainExactly([
|
||||||
|
{ users: [{ _id: user1._id }] },
|
||||||
|
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ contains: { users: ["us_none"] } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notContains", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ notContains: { users: [user1._id] } }).toContainExactly([
|
||||||
|
{ users: [{ _id: user2._id }] },
|
||||||
|
{},
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ notContains: { users: ["us_none"] } }).toContainExactly([
|
||||||
|
{ users: [{ _id: user1._id }] },
|
||||||
|
{ users: [{ _id: user2._id }] },
|
||||||
|
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||||||
|
{},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("containsAny", () => {
|
||||||
|
it("successfully finds rows", () =>
|
||||||
|
expectQuery({
|
||||||
|
containsAny: { users: [user1._id, user2._id] },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ users: [{ _id: user1._id }] },
|
||||||
|
{ users: [{ _id: user2._id }] },
|
||||||
|
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ containsAny: { users: ["us_none"] } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("multi-column equals", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({
|
||||||
|
equal: { number: 1 },
|
||||||
|
contains: { users: [user1._id] },
|
||||||
|
}).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({
|
||||||
|
equal: { number: 2 },
|
||||||
|
contains: { users: [user1._id] },
|
||||||
|
}).toFindNothing())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -191,7 +191,7 @@ describe("SQL query builder", () => {
|
||||||
)
|
)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["%20%", "%25%", `%"john"%`, `%"mary"%`, limit],
|
bindings: ["%20%", "%25%", `%"john"%`, `%"mary"%`, limit],
|
||||||
sql: `select * from (select * from (select * from "test" where (LOWER("test"."age") LIKE :1 AND LOWER("test"."age") LIKE :2) and (LOWER("test"."name") LIKE :3 AND LOWER("test"."name") LIKE :4)) where rownum <= :5) "test"`,
|
sql: `select * from (select * from (select * from "test" where (COALESCE(LOWER("test"."age"), '') LIKE :1 AND COALESCE(LOWER("test"."age"), '') LIKE :2) and (COALESCE(LOWER("test"."name"), '') LIKE :3 AND COALESCE(LOWER("test"."name"), '') LIKE :4)) where rownum <= :5) "test"`,
|
||||||
})
|
})
|
||||||
|
|
||||||
query = new Sql(SqlClient.ORACLE, limit)._query(
|
query = new Sql(SqlClient.ORACLE, limit)._query(
|
||||||
|
|
|
@ -79,7 +79,7 @@ describe("Captures of real examples", () => {
|
||||||
"b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
|
"b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
|
||||||
from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a"
|
from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a"
|
||||||
left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
|
left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
|
||||||
left join "tasks" as "b" on "b"."taskid" = "c"."taskid" where "b"."taskname" = $2
|
left join "tasks" as "b" on "b"."taskid" = "c"."taskid" where COALESCE("b"."taskname" = $2, FALSE)
|
||||||
order by "a"."productname" asc nulls first limit $3`),
|
order by "a"."productname" asc nulls first limit $3`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -139,12 +139,12 @@ describe("Captures of real examples", () => {
|
||||||
"c"."city" as "c.city", "c"."lastname" as "c.lastname", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
|
"c"."city" as "c.city", "c"."lastname" as "c.lastname", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
|
||||||
"c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
|
"c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
|
||||||
"c"."city" as "c.city", "c"."lastname" as "c.lastname"
|
"c"."city" as "c.city", "c"."lastname" as "c.lastname"
|
||||||
from (select * from "tasks" as "a" where not "a"."completed" = $1
|
from (select * from "tasks" as "a" where COALESCE("a"."completed" != $1, TRUE)
|
||||||
order by "a"."taskname" asc nulls first limit $2) as "a"
|
order by "a"."taskname" asc nulls first limit $2) as "a"
|
||||||
left join "products_tasks" as "d" on "a"."taskid" = "d"."taskid"
|
left join "products_tasks" as "d" on "a"."taskid" = "d"."taskid"
|
||||||
left join "products" as "b" on "b"."productid" = "d"."productid"
|
left join "products" as "b" on "b"."productid" = "d"."productid"
|
||||||
left join "persons" as "c" on "a"."executorid" = "c"."personid" or "a"."qaid" = "c"."personid"
|
left join "persons" as "c" on "a"."executorid" = "c"."personid" or "a"."qaid" = "c"."personid"
|
||||||
where "c"."year" between $3 and $4 and "b"."productname" = $5 order by "a"."taskname" asc nulls first limit $6`),
|
where "c"."year" between $3 and $4 and COALESCE("b"."productname" = $5, FALSE) order by "a"."taskname" asc nulls first limit $6`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -156,7 +156,7 @@ describe("Captures of real examples", () => {
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
|
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
|
||||||
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
|
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
|
||||||
"type" = $5, "city" = $6, "lastname" = $7 where "a"."personid" = $8 returning *`),
|
"type" = $5, "city" = $6, "lastname" = $7 where COALESCE("a"."personid" = $8, FALSE) returning *`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ describe("Captures of real examples", () => {
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
|
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
|
||||||
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
|
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
|
||||||
"type" = $5, "city" = $6, "lastname" = $7 where "a"."personid" = $8 returning *`),
|
"type" = $5, "city" = $6, "lastname" = $7 where COALESCE("a"."personid" = $8, FALSE) returning *`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -177,8 +177,9 @@ describe("Captures of real examples", () => {
|
||||||
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
|
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["ddd", ""],
|
bindings: ["ddd", ""],
|
||||||
sql: multiline(`delete from "compositetable" as "a" where "a"."keypartone" = $1 and "a"."keyparttwo" = $2
|
sql: multiline(`delete from "compositetable" as "a"
|
||||||
returning "a"."keyparttwo" as "a.keyparttwo", "a"."keypartone" as "a.keypartone", "a"."name" as "a.name"`),
|
where COALESCE("a"."keypartone" = $1, FALSE) and COALESCE("a"."keyparttwo" = $2, FALSE)
|
||||||
|
returning "a"."keyparttwo" as "a.keyparttwo", "a"."keypartone" as "a.keypartone", "a"."name" as "a.name"`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -199,7 +200,7 @@ describe("Captures of real examples", () => {
|
||||||
returningQuery = input
|
returningQuery = input
|
||||||
}, queryJson)
|
}, queryJson)
|
||||||
expect(returningQuery).toEqual({
|
expect(returningQuery).toEqual({
|
||||||
sql: "select * from (select top (@p0) * from [people] where [people].[name] = @p1 and [people].[age] = @p2 order by [people].[name] asc) as [people]",
|
sql: "select * from (select top (@p0) * from [people] where CASE WHEN [people].[name] = @p1 THEN 1 ELSE 0 END = 1 and CASE WHEN [people].[age] = @p2 THEN 1 ELSE 0 END = 1 order by [people].[name] asc) as [people]",
|
||||||
bindings: [1, "Test", 22],
|
bindings: [1, "Test", 22],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -117,6 +117,11 @@ async function runSqlQuery(json: QueryJson, tables: Table[]) {
|
||||||
// quick hack for docIds
|
// quick hack for docIds
|
||||||
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
|
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
|
||||||
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
|
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
|
||||||
|
|
||||||
|
if (Array.isArray(query)) {
|
||||||
|
throw new Error("SQS cannot currently handle multiple queries")
|
||||||
|
}
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
return await db.sql<Row>(sql, bindings)
|
return await db.sql<Row>(sql, bindings)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue