Merge branch 'fix-oracle-parse' of github.com:Budibase/budibase into fix-oracle-parse

This commit is contained in:
Martin McKeaveney 2024-07-23 09:45:15 +01:00
commit a30caa399b
39 changed files with 795 additions and 108 deletions

View File

@ -40,6 +40,15 @@
{
label: "Colors",
colors: [
"red-100",
"orange-100",
"yellow-100",
"green-100",
"seafoam-100",
"blue-100",
"indigo-100",
"magenta-100",
"red-400",
"orange-400",
"yellow-400",
@ -108,12 +117,17 @@
const getCheckColor = value => {
// Use dynamic color for theme grays
if (value?.includes("gray")) {
if (value?.includes("-gray-")) {
return /^.*(gray-(50|75|100|200|300|400|500))\)$/.test(value)
? "var(--spectrum-global-color-gray-900)"
: "var(--spectrum-global-color-gray-50)"
}
// Use contrasating check for the dim colours
if (value?.includes("-100")) {
return "var(--spectrum-global-color-gray-900)"
}
// Use black check for static white
if (value?.includes("static-black")) {
return "var(--spectrum-global-color-static-gray-50)"

View File

@ -62,7 +62,9 @@
return placeholder || "Choose an option"
}
return getFieldAttribute(getOptionLabel, value, options)
return (
getFieldAttribute(getOptionLabel, value, options) || "Choose an option"
)
}
const selectOption = value => {

View File

@ -111,17 +111,38 @@ a {
/* Custom theme additions */
.spectrum--darkest {
--drop-shadow: rgba(0, 0, 0, 0.6);
--spectrum-global-color-blue-100: rgb(30, 36, 50);
--spectrum-global-color-red-100: #570000;
--spectrum-global-color-orange-100: #481801;
--spectrum-global-color-yellow-100: #352400;
--spectrum-global-color-green-100: #002f07;
--spectrum-global-color-seafoam-100: #122b2a;
--spectrum-global-color-blue-100: #002651;
--spectrum-global-color-indigo-100: #1a1d61;
--spectrum-global-color-magenta-100: #530329;
--translucent-grey: rgba(255, 255, 255, 0.1);
}
.spectrum--dark {
--drop-shadow: rgba(0, 0, 0, 0.3);
--spectrum-global-color-blue-100: rgb(42, 47, 57);
}
.spectrum--light {
--drop-shadow: rgba(0, 0, 0, 0.075);
--spectrum-global-color-blue-100: rgb(240, 245, 255);
--spectrum-global-color-red-100: #7b0000;
--spectrum-global-color-orange-100: #662500;
--spectrum-global-color-yellow-100: #4c3600;
--spectrum-global-color-green-100: #00450a;
--spectrum-global-color-seafoam-100: #12413f;
--spectrum-global-color-blue-100: #003877;
--spectrum-global-color-indigo-100: #282c8c;
--spectrum-global-color-magenta-100: #76003a;
--translucent-grey: rgba(255, 255, 255, 0.065);
}
.spectrum--light,
.spectrum--lightest {
--drop-shadow: rgba(0, 0, 0, 0.05);
--spectrum-global-color-blue-100: rgb(240, 244, 255);
--drop-shadow: rgba(0, 0, 0, 0.075);
--spectrum-global-color-red-100: #ffddd6;
--spectrum-global-color-orange-100: #ffdfad;
--spectrum-global-color-yellow-100: #fbf198;
--spectrum-global-color-green-100: #cef8e0;
--spectrum-global-color-seafoam-100: #cef7f3;
--spectrum-global-color-blue-100: #e0f2ff;
--spectrum-global-color-indigo-100: #edeeff;
--spectrum-global-color-magenta-100: #ffeaf1;
--translucent-grey: rgba(0, 0, 0, 0.085);
}

View File

@ -682,7 +682,7 @@
{errors}
/>
{:else if editableColumn.type === FORMULA_TYPE}
{#if !table.sql}
{#if !externalTable}
<div class="split-label">
<div class="label-length">
<Label size="M">Formula Type</Label>

View File

@ -5,6 +5,7 @@
export let bindings = []
export let value = ""
export let allowHBS = true
export let allowJS = false
export let allowHelpers = true
export let autofocusEditor = false
@ -31,6 +32,7 @@
context={{ ...$previewStore.selectedComponentContext, ...context }}
snippets={$snippets}
{value}
{allowHBS}
{allowJS}
{allowHelpers}
{autofocusEditor}

View File

@ -16,6 +16,7 @@
export let placeholder
export let label
export let disabled = false
export let allowHBS = true
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
@ -100,6 +101,7 @@
value={readableValue}
on:change={event => (tempValue = event.detail)}
{bindings}
{allowHBS}
{allowJS}
{allowHelpers}
{context}

View File

@ -30,6 +30,7 @@ import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte
import FormStepConfiguration from "./controls/FormStepConfiguration.svelte"
import FormStepControls from "./controls/FormStepControls.svelte"
import PaywalledSetting from "./controls/PaywalledSetting.svelte"
import TableConditionEditor from "./controls/TableConditionEditor.svelte"
const componentMap = {
text: DrawerBindableInput,
@ -61,6 +62,7 @@ const componentMap = {
columns: ColumnEditor,
"columns/basic": BasicColumnEditor,
"columns/grid": GridColumnEditor,
tableConditions: TableConditionEditor,
"field/sortable": SortableFieldSelect,
"field/string": FormFieldSelect,
"field/number": FormFieldSelect,

View File

@ -12,6 +12,7 @@
export let listItemKey
export let draggable = true
export let focus
export let bindings = []
let zoneType = generate()
@ -126,6 +127,7 @@
anchor={anchors[draggableItem.id]}
item={draggableItem.item}
{...listTypeProps}
{bindings}
on:change={onItemChanged}
/>
</div>

View File

@ -10,7 +10,6 @@
export let componentBindings
export let bindings
export let parseSettings
export let disabled
const draggable = getContext("draggable")
const dispatch = createEventDispatcher()

View File

@ -4,9 +4,12 @@
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp"
import { FIELDS } from "constants/backend"
import { Constants } from "@budibase/frontend-core"
import { FieldType } from "@budibase/types"
export let item
export let anchor
export let bindings
const dispatch = createEventDispatcher()
@ -28,19 +31,30 @@
}
const parseSettings = settings => {
return settings
let columnSettings = settings
.filter(setting => setting.key !== "field")
.map(setting => {
return { ...setting, nested: true }
})
// Filter out conditions for invalid types.
// Allow formulas as we have all the data already loaded in the table.
if (
Constants.BannedSearchTypes.includes(item.columnType) &&
item.columnType !== FieldType.FORMULA
) {
return columnSettings.filter(x => x.key !== "conditions")
}
return columnSettings
}
</script>
<div class="list-item-body">
<div class="list-item-left">
<EditComponentPopover
{anchor}
componentInstance={item}
{bindings}
{anchor}
{parseSettings}
on:change
>

View File

@ -9,6 +9,7 @@
export let value
export let componentInstance
export let bindings
const dispatch = createEventDispatcher()
let primaryDisplayColumnAnchor
@ -63,6 +64,7 @@
items={columns.sortable}
listItemKey={"_id"}
listType={FieldSetting}
{bindings}
/>
<style>

View File

@ -68,6 +68,7 @@ const toGridFormat = draggableListColumns => {
field: entry.field,
active: entry.active,
width: entry.width,
conditions: entry.conditions,
}))
}
@ -83,6 +84,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
label: column.label,
columnType: schema[column.field].type,
width: column.width,
conditions: column.conditions,
},
{}
)
@ -106,12 +108,12 @@ const getColumns = ({
createComponent,
schema
)
const primary = draggableList.find(
entry => entry.field === primaryDisplayColumnName
)
const sortable = draggableList.filter(
entry => entry.field !== primaryDisplayColumnName
)
const primary = draggableList
.filter(entry => entry.field === primaryDisplayColumnName)
.map(instance => ({ ...instance, schema }))[0]
const sortable = draggableList
.filter(entry => entry.field !== primaryDisplayColumnName)
.map(instance => ({ ...instance, schema }))
return {
primary,

View File

@ -72,6 +72,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "three",
label: "three label",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "two",
@ -81,6 +83,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "two",
label: "two label",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "one",
@ -90,6 +94,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "one",
label: "one",
conditions: undefined,
schema: ctx.schema,
},
])
@ -101,6 +107,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "four",
label: "four",
conditions: undefined,
schema: ctx.schema,
})
})
})
@ -126,6 +134,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "two",
label: "two",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "three",
@ -135,6 +145,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "three",
label: "three",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "one",
@ -144,6 +156,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "one",
label: "one",
conditions: undefined,
schema: ctx.schema,
},
])
@ -155,6 +169,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "four",
label: "four",
conditions: undefined,
schema: ctx.schema,
})
})
@ -188,6 +204,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "three",
label: "three label",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "two",
@ -197,6 +215,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "two",
label: "two",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "one",
@ -206,6 +226,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "one",
label: "one",
conditions: undefined,
schema: ctx.schema,
},
])
@ -217,6 +239,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "four",
label: "four",
conditions: undefined,
schema: ctx.schema,
})
})
})
@ -247,6 +271,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "three",
label: "three label",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "two",
@ -256,6 +282,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "two",
label: "two",
conditions: undefined,
schema: ctx.schema,
},
{
_id: "one",
@ -265,6 +293,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "one",
label: "one",
conditions: undefined,
schema: ctx.schema,
},
])
@ -276,6 +306,8 @@ describe("getColumns", () => {
componentName: "@budibase/standard-components/labelfield",
field: "four",
label: "four",
conditions: undefined,
schema: ctx.schema,
})
})
})

View File

@ -0,0 +1,309 @@
<script>
import {
ActionButton,
Drawer,
Button,
DrawerContent,
Layout,
Select,
Icon,
DatePicker,
Combobox,
Multiselect,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash"
import ColorPicker from "./ColorPicker.svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { QueryUtils, Constants, FilterUsers } from "@budibase/frontend-core"
import { generate } from "shortid"
import { FieldType, FormulaType } from "@budibase/types"
import { dndzone } from "svelte-dnd-action"
import { flip } from "svelte/animate"
export let componentInstance
export let bindings
export let value
const dispatch = createEventDispatcher()
const flipDuration = 130
const targetOptions = [
{
label: "Cell",
value: "cell",
},
{
label: "Row",
value: "row",
},
]
const conditionOptions = [
{
label: "Background color",
value: "backgroundColor",
},
{
label: "Text color",
value: "textColor",
},
]
let tempValue = []
let drawer
let dragDisabled = true
$: count = value?.length
$: conditionText = `${count || "No"} condition${count !== 1 ? "s" : ""} set`
$: type = componentInstance.columnType
$: valueTypeOptions = getValueTypeOptions(type)
$: hasValueOption = type !== FieldType.STRING
$: operatorOptions = QueryUtils.getValidOperatorsForType({
type,
// We can filter on any formula columns here since we already have the data
// on the page, so adding this ensures formula columns get operators
formulaType: FormulaType.STATIC,
})
const getValueTypeOptions = type => {
let options = [
{
label: "Binding",
value: FieldType.STRING,
},
]
if (type !== FieldType.STRING) {
options.push({
label: "Value",
value: type,
})
}
return options
}
const openDrawer = () => {
tempValue = cloneDeep(value || [])
drawer.show()
}
const save = async () => {
dispatch("change", tempValue)
drawer.hide()
}
const addCondition = () => {
const condition = {
id: generate(),
target: targetOptions[0].value,
metadataKey: conditionOptions[0].value,
operator: operatorOptions[0]?.value,
valueType: FieldType.STRING,
}
tempValue = [...tempValue, condition]
}
const duplicateCondition = condition => {
const dupe = { ...condition, id: generate() }
tempValue = [...tempValue, dupe]
}
const removeCondition = condition => {
tempValue = tempValue.filter(c => c.id !== condition.id)
}
const onOperatorChange = (condition, newOperator) => {
const noValueOptions = [
Constants.OperatorOptions.Empty.value,
Constants.OperatorOptions.NotEmpty.value,
]
condition.noValue = noValueOptions.includes(newOperator)
if (condition.noValue) {
condition.referenceValue = null
condition.valueType = "string"
}
}
const onValueTypeChange = condition => {
condition.referenceValue = null
}
const updateConditions = e => {
tempValue = e.detail.items
}
const handleFinalize = e => {
updateConditions(e)
dragDisabled = true
}
</script>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Drawer
bind:this={drawer}
title="{componentInstance.field} conditions"
on:drawerShow
on:drawerHide
>
<Button cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body">
<div class="container">
<Layout noPadding>
Update the appearance of cells and rows based on their value.
{#if tempValue.length}
<div
class="conditions"
use:dndzone={{
items: tempValue,
flipDurationMs: flipDuration,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:consider={updateConditions}
on:finalize={handleFinalize}
>
{#each tempValue as condition (condition.id)}
<div
class="condition"
class:update={condition.action === "update"}
class:with-value-option={hasValueOption}
animate:flip={{ duration: flipDuration }}
>
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
on:mousedown={() => (dragDisabled = false)}
>
<Icon name="DragHandle" size="XL" />
</div>
<span>Update</span>
<Select
placeholder={null}
options={targetOptions}
bind:value={condition.target}
/>
<Select
placeholder={null}
options={conditionOptions}
bind:value={condition.metadataKey}
/>
<span>to</span>
<ColorPicker
value={condition.metadataValue}
on:change={e => (condition.metadataValue = e.detail)}
/>
<span>if value</span>
<Select
placeholder={null}
options={operatorOptions}
bind:value={condition.operator}
on:change={e => onOperatorChange(condition, e.detail)}
/>
{#if hasValueOption}
<Select
disabled={condition.noValue}
options={valueTypeOptions}
bind:value={condition.valueType}
placeholder={null}
on:change={() => onValueTypeChange(condition)}
/>
{/if}
{#if type === FieldType.DATETIME && condition.valueType === type}
<DatePicker
placeholder="Value"
disabled={condition.noValue}
bind:value={condition.referenceValue}
/>
{:else if type === FieldType.BOOLEAN && condition.valueType === type}
<Select
placeholder="Value"
disabled={condition.noValue}
options={["True", "False"]}
bind:value={condition.referenceValue}
/>
{:else if (type === FieldType.OPTIONS || type === FieldType.ARRAY) && condition.valueType === type}
{#if condition.operator === Constants.OperatorOptions.In.value}
<Multiselect
disabled={condition.noValue}
options={componentInstance.schema?.[
componentInstance.field
]?.constraints?.inclusion || []}
bind:value={condition.referenceValue}
/>
{:else}
<Combobox
disabled={condition.noValue}
options={componentInstance.schema?.[
componentInstance.field
]?.constraints?.inclusion || []}
bind:value={condition.referenceValue}
/>
{/if}
{:else if (type === FieldType.BB_REFERENCE || type === FieldType.BB_REFERENCE_SINGLE) && condition.valueType === type}
<FilterUsers
bind:value={condition.referenceValue}
multiselect={[
Constants.OperatorOptions.In.value,
Constants.OperatorOptions.ContainsAny.value,
].includes(condition.operator)}
disabled={condition.noValue}
type={condition.valueType}
/>
{:else}
<DrawerBindableInput
{bindings}
placeholder="Value"
disabled={condition.noValue}
value={condition.referenceValue}
on:change={e => (condition.referenceValue = e.detail)}
/>
{/if}
<Icon
name="Duplicate"
hoverable
size="S"
on:click={() => duplicateCondition(condition)}
/>
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeCondition(condition)}
/>
</div>
{/each}
</div>
{/if}
<div>
<Button secondary icon="Add" on:click={addCondition}>
Add condition
</Button>
</div>
</Layout>
</div>
</DrawerContent>
</Drawer>
<style>
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.conditions {
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
.condition {
display: grid;
grid-template-columns: auto auto 1fr 1fr auto auto auto 1fr 1fr auto auto;
align-items: center;
grid-column-gap: var(--spacing-l);
}
.condition.with-value-option {
grid-template-columns: auto auto 1fr 1fr auto auto auto 1fr 1fr 1fr auto auto;
}
</style>

View File

@ -29,7 +29,7 @@ import { JSONUtils, Constants } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "stores/portal"
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
import { FIELDS } from "constants/backend"
import { FIELDS, DB_TYPE_INTERNAL } from "constants/backend"
import { FieldType } from "@budibase/types"
const { ContextScopes } = Constants
@ -991,7 +991,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
}
// Determine if we should add ID and rev to the schema
const isInternal = table && !table.sql
const isInternal = table && table?.sourceType === DB_TYPE_INTERNAL
const isDSPlus = ["table", "link", "viewV2"].includes(datasource.type)
// ID is part of the readable schema for all tables

View File

@ -8,8 +8,11 @@
} from "@budibase/bbui"
import { componentStore } from "stores/builder"
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
export let componentInstance
export let componentDefinition
export let componentBindings
export let bindings
let tempValue
@ -35,6 +38,19 @@
} set`
</script>
<!--
Load any general settings or sections tagged as "condition"
-->
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
isScreen={false}
showInstanceName={false}
{bindings}
{componentBindings}
tag="condition"
/>
<DetailSummary name={"Conditions"} collapsible={false}>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
</DetailSummary>

View File

@ -2879,6 +2879,11 @@
"placeholder": "Auto",
"min": 80,
"max": 9999
},
{
"type": "tableConditions",
"label": "Conditions",
"key": "conditions"
}
]
},
@ -7303,7 +7308,6 @@
{
"type": "columns/grid",
"key": "columns",
"nested": true,
"resetOn": "table"
}
]

View File

@ -42,7 +42,6 @@
$: currentTheme = $context?.device?.theme
$: darkMode = !currentTheme?.includes("light")
$: parsedColumns = getParsedColumns(columns)
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
$: schemaOverrides = getSchemaOverrides(parsedColumns)
$: enrichedButtons = enrichButtons(buttons)
$: selectedRows = deriveSelectedRows(gridContext)
@ -62,7 +61,13 @@
const goldenRow = generateGoldenSample(rows)
const id = get(component).id
return {
// Not sure what this one is for...
[id]: goldenRow,
// For row conditions context
row: goldenRow,
// For button action context
eventContext: {
row: goldenRow,
},
@ -91,6 +96,8 @@
overrides[column.field] = {
displayName: column.label,
order: idx,
conditions: column.conditions,
visible: !!column.active,
}
if (column.width) {
overrides[column.field].width = column.width
@ -163,7 +170,6 @@
{initialSortColumn}
{initialSortOrder}
{fixedRowHeight}
{columnWhitelist}
{schemaOverrides}
canAddRows={allowAddRows}
canEditRows={allowEditRows}
@ -187,7 +193,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid var(--spectrum-global-color-gray-300);
border: 1px solid var(--spectrum-global-color-gray-200);
border-radius: 4px;
overflow: hidden;
height: 410px;

View File

@ -130,6 +130,10 @@
on:mouseup={stopSelectionCallback}
on:click={handleClick}
width={column.width}
metadata={{
...row.__metadata?.row,
...row.__metadata?.cell[column.name],
}}
>
<svelte:component
this={getCellRenderer(column)}

View File

@ -11,13 +11,20 @@
export let center = false
export let readonly = false
export let hidden = false
export let metadata = null
$: style = getStyle(width, selectedUser)
$: style = getStyle(width, selectedUser, metadata)
const getStyle = (width, selectedUser) => {
const getStyle = (width, selectedUser, metadata) => {
let style = width === "auto" ? "width: auto;" : `flex: 0 0 ${width}px;`
if (selectedUser) {
style += `--user-color:${selectedUser.color};`
style += `--user-color :${selectedUser.color};`
}
if (metadata?.backgroundColor) {
style += `--cell-background: ${metadata.backgroundColor};`
}
if (metadata?.textColor) {
style += `--cell-font-color: ${metadata.textColor};`
}
return style
}
@ -43,7 +50,7 @@
on:mouseup
on:click
on:contextmenu
on:touchstart
on:touchstart|passive
on:touchend
on:touchcancel
on:mouseenter
@ -72,7 +79,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
color: var(--spectrum-global-color-gray-800);
color: var(--cell-font-color);
font-size: var(--cell-font-size);
gap: var(--cell-spacing);
background: var(--cell-background);
@ -94,9 +101,9 @@
}
/* Cell border */
.cell.focused:after,
.cell.error:after,
.cell.selected-other:not(.focused):after {
.cell.focused::after,
.cell.error::after,
.cell.selected-other:not(.focused)::after {
content: " ";
position: absolute;
top: 0;
@ -109,14 +116,30 @@
box-sizing: border-box;
}
/* Cell background overlay */
.cell.selected::before {
content: " ";
position: absolute;
top: 0;
left: 0;
pointer-events: none;
box-sizing: border-box;
height: calc(100% + 1px);
width: calc(100% + 1px);
opacity: 0.16;
background: var(--spectrum-global-color-blue-400);
z-index: 2;
pointer-events: none;
}
/* Cell border for cells with labels */
.cell.error:after {
.cell.error::after {
border-radius: 0 2px 2px 2px;
}
.cell.top.error:after {
.cell.top.error::after {
border-radius: 2px 2px 2px 0;
}
.cell.selected-other:not(.focused):after {
.cell.selected-other:not(.focused)::after {
border-radius: 2px;
}
@ -151,15 +174,10 @@
.cell.focused.readonly {
--cell-color: var(--spectrum-global-color-gray-600);
}
.cell.highlighted:not(.focused),
.cell.highlighted:not(.focused):not(.selected),
.cell.focused.readonly {
--cell-background: var(--cell-background-hover);
}
.cell.selected.focused,
.cell.selected:not(.focused) {
--cell-background: var(--spectrum-global-color-blue-100);
}
/* Label for additional text */
.label {

View File

@ -54,6 +54,7 @@
selected={rowSelected}
{defaultHeight}
rowIdx={row?.__idx}
metadata={row?.__metadata?.row}
>
<div class="gutter">
{#if $$slots.default}
@ -115,7 +116,7 @@
margin: 3px 0 0 0;
}
.number {
color: var(--spectrum-global-color-gray-500);
color: val(--cell-font-color, var(--spectrum-global-color-gray-500));
}
.checkbox.visible,
.number.visible {

View File

@ -70,6 +70,7 @@
rowIdx={row.__idx}
selected={rowSelected}
highlighted={rowHovered || rowFocused}
metadata={row.__metadata?.row}
>
<div class="buttons" class:offset={$showVScrollbar}>
{#each buttons as button}

View File

@ -37,7 +37,6 @@
export let API = null
export let datasource = null
export let schemaOverrides = null
export let columnWhitelist = null
export let canAddRows = true
export let canExpandRows = true
export let canEditRows = true
@ -59,6 +58,7 @@
export let darkMode
export let isCloud = null
export let allowViewReadonlyColumns = false
export let rowConditions = null
// Unique identifier for DOM nodes inside this instance
const gridID = `grid-${Math.random().toString().slice(2)}`
@ -93,7 +93,6 @@
$: props.set({
datasource,
schemaOverrides,
columnWhitelist,
canAddRows,
canExpandRows,
canEditRows,
@ -114,6 +113,8 @@
buttons,
darkMode,
isCloud,
allowViewReadonlyColumns,
rowConditions,
})
// Derive min height and make available in context
@ -231,6 +232,7 @@
--cell-spacing: 4px;
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
--cell-font-size: 14px;
--cell-font-color: var(--spectrum-global-color-gray-800);
flex: 1 1 auto;
display: flex;
flex-direction: column;
@ -286,7 +288,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
border-bottom: var(--cell-border);
padding: var(--cell-padding);
gap: var(--cell-spacing);
background: var(--grid-background-alt);

View File

@ -31,7 +31,7 @@
const generateStyle = (scrollLeft, scrollTop, rowHeight) => {
const offsetX = scrollHorizontally ? -1 * scrollLeft : 0
const offsetY = scrollVertically ? -1 * (scrollTop % rowHeight) : 0
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
return `transform: translate(${offsetX}px, ${offsetY}px);`
}
// Handles a mouse wheel event and updates scroll state

View File

@ -162,7 +162,7 @@
/* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
border-right-color: transparent;
}
.header {
@ -171,6 +171,9 @@
.header :global(.cell) {
background: var(--grid-background-alt);
}
.header :global(.cell::before) {
display: none;
}
.row {
display: flex;
flex-direction: row;

View File

@ -144,6 +144,7 @@ export const initialise = context => {
visible: fieldSchema.visible ?? true,
readonly: fieldSchema.readonly,
order: fieldSchema.order ?? oldColumn?.order,
conditions: fieldSchema.conditions,
}
// Override a few properties for primary display
if (field === primaryDisplay) {

View File

@ -0,0 +1,157 @@
import { writable, get } from "svelte/store"
import { derivedMemo, QueryUtils } from "../../../utils"
import { FieldType, EmptyFilterOption } from "@budibase/types"
export const createStores = () => {
const metadata = writable({})
return {
metadata,
}
}
export const deriveStores = context => {
const { columns } = context
// Derive and memoize the cell conditions present in our columns so that we
// only recompute condition metadata when absolutely necessary
const conditions = derivedMemo(columns, $columns => {
let newConditions = []
for (let column of $columns) {
for (let condition of column.conditions || []) {
newConditions.push({
...condition,
column: column.name,
type: column.schema.type,
})
}
}
return newConditions
})
return {
conditions,
}
}
export const initialise = context => {
const { metadata, conditions, rows } = context
// Recompute all metadata if conditions change
conditions.subscribe($conditions => {
let newMetadata = {}
if ($conditions?.length) {
for (let row of get(rows)) {
newMetadata[row._id] = evaluateConditions(row, $conditions)
}
}
metadata.set(newMetadata)
})
// Recompute metadata for specific rows when they change
rows.subscribe($rows => {
const $conditions = get(conditions)
if (!$conditions?.length) {
return
}
const $metadata = get(metadata)
let metadataUpdates = {}
for (let row of $rows) {
if (!row._rev || $metadata[row._id]?.version !== row._rev) {
metadataUpdates[row._id] = evaluateConditions(row, $conditions)
}
}
if (Object.keys(metadataUpdates).length) {
metadata.update(state => ({
...state,
...metadataUpdates,
}))
}
})
}
const TypeCoercionMap = {
[FieldType.NUMBER]: parseFloat,
[FieldType.DATETIME]: val => {
if (val) {
return new Date(val).toISOString()
}
return null
},
[FieldType.BOOLEAN]: val => {
if (`${val}`.toLowerCase().trim() === "true") {
return true
}
if (`${val}`.toLowerCase().trim() === "false") {
return false
}
return null
},
}
// Evaluates an array of cell conditions against a certain row and returns the
// resultant metadata
const evaluateConditions = (row, conditions) => {
let metadata = {
version: row._rev,
row: {},
cell: {},
}
for (let condition of conditions) {
try {
let {
column,
type,
referenceValue,
operator,
metadataKey,
metadataValue,
target,
} = condition
let value = row[column]
// Coerce values into correct types for primitives
let coercedType = type
if (type === FieldType.FORMULA) {
// For formulas we want to ensure that the reference type matches the
// real type
if (value === true || value === false) {
coercedType = FieldType.BOOLEAN
} else if (typeof value === "number") {
coercedType = FieldType.NUMBER
}
}
const coerce = TypeCoercionMap[coercedType]
if (coerce) {
value = coerce(value)
referenceValue = coerce(referenceValue)
}
// Build lucene compatible condition expression
const luceneFilter = {
operator,
type,
field: "value",
value: referenceValue,
}
let query = QueryUtils.buildQuery([luceneFilter])
query.onEmptyFilter = EmptyFilterOption.RETURN_NONE
const result = QueryUtils.runQuery([{ value }], query)
if (result.length > 0) {
if (target === "row") {
metadata.row = {
...metadata.row,
[metadataKey]: metadataValue,
}
} else {
metadata.cell[column] = {
...metadata.cell[column],
[metadataKey]: metadataValue,
}
}
}
} catch {
// Swallow
}
}
return metadata
}

View File

@ -12,9 +12,9 @@ export const createStores = context => {
const initialFilter = getProp("initialFilter")
const fixedRowHeight = getProp("fixedRowHeight")
const schemaOverrides = getProp("schemaOverrides")
const columnWhitelist = getProp("columnWhitelist")
const notifySuccess = getProp("notifySuccess")
const notifyError = getProp("notifyError")
const rowConditions = getProp("rowConditions")
return {
datasource,
@ -23,9 +23,9 @@ export const createStores = context => {
initialFilter,
fixedRowHeight,
schemaOverrides,
columnWhitelist,
notifySuccess,
notifyError,
rowConditions,
}
}

View File

@ -13,14 +13,8 @@ export const createStores = () => {
}
export const deriveStores = context => {
const {
API,
definition,
schemaOverrides,
columnWhitelist,
datasource,
schemaMutations,
} = context
const { API, definition, schemaOverrides, datasource, schemaMutations } =
context
const schema = derived(definition, $definition => {
let schema = getDatasourceSchema({
@ -46,17 +40,13 @@ export const deriveStores = context => {
// Derives the total enriched schema, made up of the saved schema and any
// prop and user overrides
const enrichedSchema = derived(
[schema, schemaOverrides, schemaMutations, columnWhitelist],
([$schema, $schemaOverrides, $schemaMutations, $columnWhitelist]) => {
[schema, schemaOverrides, schemaMutations],
([$schema, $schemaOverrides, $schemaMutations]) => {
if (!$schema) {
return null
}
let enrichedSchema = {}
Object.keys($schema).forEach(field => {
// Apply whitelist if provided
if ($columnWhitelist?.length && !$columnWhitelist.includes(field)) {
return
}
enrichedSchema[field] = {
...$schema[field],
...$schemaOverrides?.[field],

View File

@ -20,6 +20,7 @@ import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus"
import * as Cache from "./cache"
import * as Conditions from "./conditions"
const DependencyOrderedStores = [
Sort,
@ -33,6 +34,7 @@ const DependencyOrderedStores = [
Scroll,
Validation,
Rows,
Conditions,
UI,
Resize,
Viewport,

View File

@ -5,6 +5,7 @@ import { getCellID, parseCellID } from "../lib/utils"
import { tick } from "svelte"
import { Helpers } from "@budibase/bbui"
import { sleep } from "../../../utils/utils"
import { FieldType } from "@budibase/types"
export const createStores = () => {
const rows = writable([])
@ -17,27 +18,6 @@ export const createStores = () => {
const error = writable(null)
const fetch = writable(null)
// Enrich rows with an index property and any pending changes
const enrichedRows = derived(
[rows, rowChangeCache],
([$rows, $rowChangeCache]) => {
return $rows.map((row, idx) => ({
...row,
...$rowChangeCache[row._id],
__idx: idx,
}))
}
)
// Generate a lookup map to quick find a row by ID
const rowLookupMap = derived(enrichedRows, $enrichedRows => {
let map = {}
for (let i = 0; i < $enrichedRows.length; i++) {
map[$enrichedRows[i]._id] = $enrichedRows[i]
}
return map
})
// Mark loaded as true if we've ever stopped loading
let hasStartedLoading = false
loading.subscribe($loading => {
@ -49,12 +29,8 @@ export const createStores = () => {
})
return {
rows: {
...rows,
subscribe: enrichedRows.subscribe,
},
rows,
fetch,
rowLookupMap,
loaded,
refreshing,
loading,
@ -65,6 +41,35 @@ export const createStores = () => {
}
}
export const deriveStores = context => {
const { rows } = context
// Enrich rows with an index property and any pending changes
const enrichedRows = derived(rows, $rows => {
return $rows.map((row, idx) => ({
...row,
__idx: idx,
}))
})
// Generate a lookup map to quick find a row by ID
const rowLookupMap = derived(enrichedRows, $enrichedRows => {
let map = {}
for (let i = 0; i < $enrichedRows.length; i++) {
map[$enrichedRows[i]._id] = $enrichedRows[i]
}
return map
})
return {
rows: {
...rows,
subscribe: enrichedRows.subscribe,
},
rowLookupMap,
}
}
export const createActions = context => {
const {
rows,
@ -367,7 +372,7 @@ export const createActions = context => {
// Get index of row to check if it exists
const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap)
const index = $rowLookupMap[id].__idx
const index = $rowLookupMap[id]?.__idx
// Process as either an update, addition or deletion
if (row) {
@ -417,8 +422,21 @@ export const createActions = context => {
// valid pending change was made or not
const stashRowChanges = (rowId, changes) => {
const $rowLookupMap = get(rowLookupMap)
const $columnLookupMap = get(columnLookupMap)
const row = $rowLookupMap[rowId]
// Coerce some values into the correct types
for (let column of Object.keys(changes || {})) {
const type = $columnLookupMap[column]?.schema?.type
// Stringify objects
if (type === FieldType.STRING || type == FieldType.LONGFORM) {
if (changes[column] != null && typeof changes[column] !== "string") {
changes[column] = JSON.stringify(changes[column])
}
}
}
// Check this is a valid change
if (!row || !changesAreValid(row, changes)) {
return false
@ -643,6 +661,7 @@ export const createActions = context => {
const cleanRow = row => {
let clone = { ...row }
delete clone.__idx
delete clone.__metadata
if (!get(hasBudibaseIdentifiers)) {
delete clone._id
}

View File

@ -10,6 +10,8 @@ export const deriveStores = context => {
scrollLeft,
width,
height,
rowChangeCache,
metadata,
} = context
// Derive visible rows
@ -19,25 +21,31 @@ export const deriveStores = context => {
[scrollTop, rowHeight],
([$scrollTop, $rowHeight]) => {
return Math.floor($scrollTop / $rowHeight)
},
0
}
)
const visualRowCapacity = derived(
[height, rowHeight],
([$height, $rowHeight]) => {
return Math.ceil($height / $rowHeight) + 1
},
0
}
)
const renderedRows = derived(
[rows, scrolledRowCount, visualRowCapacity],
([$rows, $scrolledRowCount, $visualRowCapacity]) => {
return $rows.slice(
$scrolledRowCount,
$scrolledRowCount + $visualRowCapacity
)
},
[]
[rows, scrolledRowCount, visualRowCapacity, rowChangeCache, metadata],
([
$rows,
$scrolledRowCount,
$visualRowCapacity,
$rowChangeCache,
$metadata,
]) => {
return $rows
.slice($scrolledRowCount, $scrolledRowCount + $visualRowCapacity)
.map(row => ({
...row,
...$rowChangeCache[row._id],
__metadata: $metadata[row._id],
}))
}
)
// Derive visible columns

View File

@ -8,3 +8,4 @@ export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
export { default as FilterBuilder } from "./FilterBuilder.svelte"
export { default as FilterUsers } from "./FilterUsers.svelte"

View File

@ -12,6 +12,7 @@ import { FieldType, BBReferenceFieldSubType } from "@budibase/types"
export const BannedSearchTypes = [
FieldType.LINK,
FieldType.ATTACHMENT_SINGLE,
FieldType.ATTACHMENTS,
FieldType.FORMULA,
FieldType.JSON,

View File

@ -16,5 +16,6 @@
/* Custom additions */
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(36, 44, 64) !important;
--spectrum-global-color-blue-100: hsl(var(--hue), 48%, 24%) !important;
--translucent-grey: rgba(255, 255, 255, 0.075) !important;
}

View File

@ -49,5 +49,6 @@
/* Custom additions */
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
--spectrum-global-color-blue-100: rgb(56, 65, 90) !important;
--spectrum-global-color-blue-100: hsl(213, 36%, 30%) !important;
--translucent-grey: hsla(213, 36%, 80%, 0.11) !important;
}

View File

@ -6,9 +6,11 @@ import {
} from "../../../integrations/tests/utils"
import {
db as dbCore,
context,
MAX_VALID_DATE,
MIN_VALID_DATE,
utils,
SQLITE_DESIGN_DOC_ID,
} from "@budibase/backend-core"
import * as setup from "./utilities"
@ -2524,4 +2526,38 @@ describe.each([
}).toContainExactly([{ [" name"]: "foo" }])
})
})
isSqs &&
describe("duplicate columns", () => {
beforeAll(async () => {
table = await createTable({
name: {
name: "name",
type: FieldType.STRING,
},
})
await context.doInAppContext(config.getAppId(), async () => {
const db = context.getAppDB()
const tableDoc = await db.get<Table>(table._id!)
tableDoc.schema.Name = {
name: "Name",
type: FieldType.STRING,
}
try {
// remove the SQLite definitions so that they can be rebuilt as part of the search
const sqliteDoc = await db.get(SQLITE_DESIGN_DOC_ID)
await db.remove(sqliteDoc)
} catch (err) {
// no-op
}
})
await createRows([{ name: "foo", Name: "bar" }])
})
it("should handle invalid duplicate column names", async () => {
await expectSearch({
query: {},
}).toContainExactly([{ name: "foo" }])
})
})
})

View File

@ -49,6 +49,7 @@ import { dataFilters } from "@budibase/shared-core"
const builder = new sql.Sql(SqlClient.SQL_LITE)
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
const MISSING_TABLE_REGX = new RegExp(`no such table: .+`)
const DUPLICATE_COLUMN_REGEX = new RegExp(`duplicate column name: .+`)
function buildInternalFieldList(
table: Table,
@ -237,9 +238,11 @@ function resyncDefinitionsRequired(status: number, message: string) {
// pre data_ prefix on column names, need to resync
return (
// there are tables missing - try a resync
(status === 400 && message.match(MISSING_TABLE_REGX)) ||
(status === 400 && message?.match(MISSING_TABLE_REGX)) ||
// there are columns missing - try a resync
(status === 400 && message.match(MISSING_COLUMN_REGEX)) ||
(status === 400 && message?.match(MISSING_COLUMN_REGEX)) ||
// duplicate column name in definitions - need to re-run definition sync
(status === 400 && message?.match(DUPLICATE_COLUMN_REGEX)) ||
// no design document found, needs a full sync
(status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID))
)

View File

@ -94,6 +94,9 @@ export function mapToUserColumn(key: string) {
function mapTable(table: Table): SQLiteTables {
const tables: SQLiteTables = {}
const fields: Record<string, { field: string; type: SQLiteType }> = {}
// a list to make sure no duplicates - the fields are mapped by SQS with case sensitivity
// but need to make sure there are no duplicate columns
const usedColumns: string[] = []
for (let [key, column] of Object.entries(table.schema)) {
// relationships should be handled differently
if (column.type === FieldType.LINK) {
@ -106,6 +109,12 @@ function mapTable(table: Table): SQLiteTables {
if (!FieldTypeMap[column.type]) {
throw new Error(`Unable to map type "${column.type}" to SQLite type`)
}
const lcKey = key.toLowerCase()
// ignore duplicates
if (usedColumns.includes(lcKey)) {
continue
}
usedColumns.push(lcKey)
fields[mapToUserColumn(key)] = {
field: key,
type: FieldTypeMap[column.type],