Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux
This commit is contained in:
commit
6a6ae2c4e7
|
@ -4,11 +4,10 @@ packages/server/runtime_apps/
|
|||
.idea/
|
||||
bb-airgapped.tar.gz
|
||||
*.iml
|
||||
|
||||
packages/server/build/oldClientVersions/**/*
|
||||
packages/builder/src/components/deploy/clientVersions.json
|
||||
|
||||
packages/server/src/integrations/tests/utils/*.lock
|
||||
packages/builder/vite.config.mjs.timestamp*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
|
@ -16,14 +16,17 @@
|
|||
href={url}
|
||||
class="list-item"
|
||||
class:hoverable={hoverable || url != null}
|
||||
class:large={!!subtitle}
|
||||
on:click
|
||||
class:selected
|
||||
>
|
||||
<div class="left">
|
||||
<div class="list-item__left">
|
||||
{#if icon === "StatusLight"}
|
||||
<StatusLight square size="L" color={iconColor} />
|
||||
{:else if icon}
|
||||
<Icon name={icon} color={iconColor} />
|
||||
<div class="list-item__icon">
|
||||
<Icon name={icon} color={iconColor} size={subtitle ? "XL" : "M"} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="list-item__text">
|
||||
{#if title}
|
||||
|
@ -38,7 +41,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="list-item__right">
|
||||
<slot name="right" />
|
||||
{#if showArrow}
|
||||
<Icon name="ChevronRight" />
|
||||
|
@ -54,9 +57,12 @@
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: background 130ms ease-out;
|
||||
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||
gap: var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.list-item:not(:first-child) {
|
||||
border-top: none;
|
||||
|
@ -74,27 +80,72 @@
|
|||
}
|
||||
.hoverable:not(.selected):hover {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.selected {
|
||||
background: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
/* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
|
||||
.list-item.selected {
|
||||
background-color: var(--spectrum-global-color-blue-100);
|
||||
border-color: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
.list-item.selected:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid var(--spectrum-global-color-blue-400);
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Large icons */
|
||||
.list-item.large .list-item__icon {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: background-color 130ms ease-out, border-color 130ms ease-out,
|
||||
color 130ms ease-out;
|
||||
}
|
||||
.list-item.large.hoverable:not(.selected):hover .list-item__icon {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.list-item.large.selected .list-item__icon {
|
||||
background-color: var(--spectrum-global-color-blue-400);
|
||||
color: white;
|
||||
border-color: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
|
||||
/* Internal layout */
|
||||
.list-item__left,
|
||||
.list-item__right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.left {
|
||||
.list-item.large .list-item__left,
|
||||
.list-item.large .list-item__right {
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.list-item__left {
|
||||
width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.right {
|
||||
.list-item__right {
|
||||
flex: 0 0 auto;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
/* Text */
|
||||
.list-item__text {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
|
@ -106,6 +157,7 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
.list-item__subtitle {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -147,6 +147,9 @@
|
|||
.spectrum-Dialog--extraLarge {
|
||||
width: 1000px;
|
||||
}
|
||||
.spectrum-Dialog--medium {
|
||||
width: 540px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
|
|
|
@ -578,7 +578,9 @@
|
|||
break
|
||||
}
|
||||
}
|
||||
return utils.processSearchFilters(filters)
|
||||
return Array.isArray(filters)
|
||||
? utils.processSearchFilters(filters)
|
||||
: filters
|
||||
}
|
||||
|
||||
function saveFilters(key) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { getUserBindings } from "dataBinding"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { search } from "@budibase/frontend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { tables } from "stores/builder"
|
||||
|
||||
export let schema
|
||||
|
@ -17,19 +16,16 @@
|
|||
|
||||
let drawer
|
||||
|
||||
$: localFilters = utils.processSearchFilters(filters)
|
||||
|
||||
$: localFilters = filters
|
||||
$: schemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{ allowLinks: true }
|
||||
)
|
||||
|
||||
$: filterCount =
|
||||
localFilters?.groups?.reduce((acc, group) => {
|
||||
return (acc += group.filters.filter(filter => filter.field).length)
|
||||
}, 0) || 0
|
||||
|
||||
$: bindings = [
|
||||
{
|
||||
type: "context",
|
||||
|
@ -61,7 +57,7 @@
|
|||
title="Filtering"
|
||||
on:drawerHide
|
||||
on:drawerShow={() => {
|
||||
localFilters = utils.processSearchFilters(filters)
|
||||
localFilters = filters
|
||||
}}
|
||||
forceModal
|
||||
>
|
||||
|
|
|
@ -9,13 +9,11 @@
|
|||
let anchor
|
||||
|
||||
$: columnOptions = $columns
|
||||
.filter(col => canBeSortColumn(col.schema))
|
||||
.map(col => ({
|
||||
label: col.label || col.name,
|
||||
value: col.name,
|
||||
type: col.schema?.type,
|
||||
related: col.related,
|
||||
}))
|
||||
.filter(col => canBeSortColumn(col))
|
||||
$: orderOptions = getOrderOptions($sort.column, columnOptions)
|
||||
|
||||
const getOrderOptions = (column, columnOptions) => {
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
<script>
|
||||
import {
|
||||
ActionButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
Icon,
|
||||
Multiselect,
|
||||
} from "@budibase/bbui"
|
||||
import { CalculationType, canGroupBy, FieldType } from "@budibase/types"
|
||||
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { definition, datasource, rows } = getContext("grid")
|
||||
const calculationTypeOptions = [
|
||||
{
|
||||
label: "Average",
|
||||
value: CalculationType.AVG,
|
||||
},
|
||||
{
|
||||
label: "Sum",
|
||||
value: CalculationType.SUM,
|
||||
},
|
||||
{
|
||||
label: "Minimum",
|
||||
value: CalculationType.MIN,
|
||||
},
|
||||
{
|
||||
label: "Maximum",
|
||||
value: CalculationType.MAX,
|
||||
},
|
||||
{
|
||||
label: "Count",
|
||||
value: CalculationType.COUNT,
|
||||
},
|
||||
]
|
||||
|
||||
let modal
|
||||
let calculations = []
|
||||
let groupBy = []
|
||||
let schema = {}
|
||||
|
||||
$: schema = $definition?.schema || {}
|
||||
$: count = extractCalculations($definition?.schema || {}).length
|
||||
$: groupByOptions = getGroupByOptions(schema)
|
||||
|
||||
const open = () => {
|
||||
calculations = extractCalculations(schema)
|
||||
groupBy = calculations.length ? extractGroupBy(schema) : []
|
||||
modal?.show()
|
||||
}
|
||||
|
||||
const extractCalculations = schema => {
|
||||
if (!schema) {
|
||||
return []
|
||||
}
|
||||
return Object.keys(schema)
|
||||
.filter(field => {
|
||||
return schema[field].calculationType != null
|
||||
})
|
||||
.map(field => ({
|
||||
type: schema[field].calculationType,
|
||||
field: schema[field].field,
|
||||
}))
|
||||
}
|
||||
|
||||
const extractGroupBy = schema => {
|
||||
if (!schema) {
|
||||
return []
|
||||
}
|
||||
return Object.keys(schema).filter(field => {
|
||||
return schema[field].calculationType == null && schema[field].visible
|
||||
})
|
||||
}
|
||||
|
||||
// Gets the available types for a given calculation
|
||||
const getTypeOptions = (self, calculations) => {
|
||||
return calculationTypeOptions.filter(option => {
|
||||
return !calculations.some(
|
||||
calc =>
|
||||
calc !== self &&
|
||||
calc.field === self.field &&
|
||||
calc.type === option.value
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Gets the available fields for a given calculation
|
||||
const getFieldOptions = (self, calculations, schema) => {
|
||||
return Object.entries(schema)
|
||||
.filter(([field, fieldSchema]) => {
|
||||
// Only allow numeric fields that are not calculations themselves
|
||||
if (
|
||||
fieldSchema.calculationType ||
|
||||
fieldSchema.type !== FieldType.NUMBER
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Don't allow duplicates
|
||||
return !calculations.some(calc => {
|
||||
return (
|
||||
calc !== self && calc.type === self.type && calc.field === field
|
||||
)
|
||||
})
|
||||
})
|
||||
.map(([field]) => field)
|
||||
}
|
||||
|
||||
// Gets the available fields to group by
|
||||
const getGroupByOptions = schema => {
|
||||
return Object.entries(schema)
|
||||
.filter(([_, fieldSchema]) => {
|
||||
// Don't allow grouping by calculations
|
||||
if (fieldSchema.calculationType) {
|
||||
return false
|
||||
}
|
||||
// Don't allow complex types
|
||||
return canGroupBy(fieldSchema.type)
|
||||
})
|
||||
.map(([field]) => field)
|
||||
}
|
||||
|
||||
const addCalc = () => {
|
||||
calculations = [...calculations, { type: CalculationType.AVG }]
|
||||
}
|
||||
|
||||
const deleteCalc = idx => {
|
||||
calculations = calculations.toSpliced(idx, 1)
|
||||
|
||||
// Remove any grouping if clearing the last calculation
|
||||
if (!calculations.length) {
|
||||
groupBy = []
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
let newSchema = {}
|
||||
|
||||
// Add calculations
|
||||
for (let calc of calculations) {
|
||||
if (!calc.type || !calc.field) {
|
||||
continue
|
||||
}
|
||||
const typeOption = calculationTypeOptions.find(x => x.value === calc.type)
|
||||
const name = `${typeOption.label} ${calc.field}`
|
||||
newSchema[name] = {
|
||||
calculationType: calc.type,
|
||||
field: calc.field,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Add other fields
|
||||
for (let field of Object.keys(schema)) {
|
||||
if (schema[field].calculationType) {
|
||||
continue
|
||||
}
|
||||
newSchema[field] = {
|
||||
...schema[field],
|
||||
visible: groupBy.includes(field),
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure primary display is valid
|
||||
let primaryDisplay = $definition.primaryDisplay
|
||||
if (!primaryDisplay || !newSchema[primaryDisplay]?.visible) {
|
||||
primaryDisplay = groupBy[0]
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await datasource.actions.saveDefinition({
|
||||
...$definition,
|
||||
primaryDisplay,
|
||||
schema: newSchema,
|
||||
})
|
||||
await rows.actions.refreshData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="WebPage" quiet on:click={open}>
|
||||
Configure calculations{count ? `: ${count}` : ""}
|
||||
</ActionButton>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
title="Calculations"
|
||||
confirmText="Save"
|
||||
size="M"
|
||||
onConfirm={save}
|
||||
>
|
||||
{#if calculations.length}
|
||||
<div class="calculations">
|
||||
{#each calculations as calc, idx}
|
||||
<span>{idx === 0 ? "Calculate" : "and"} the</span>
|
||||
<Select
|
||||
options={getTypeOptions(calc, calculations)}
|
||||
bind:value={calc.type}
|
||||
placeholder={false}
|
||||
/>
|
||||
<span>of</span>
|
||||
<Select
|
||||
options={getFieldOptions(calc, calculations, schema)}
|
||||
bind:value={calc.field}
|
||||
placeholder="Column"
|
||||
/>
|
||||
<Icon
|
||||
hoverable
|
||||
name="Delete"
|
||||
size="S"
|
||||
on:click={() => deleteCalc(idx)}
|
||||
color="var(--spectrum-global-color-gray-700)"
|
||||
/>
|
||||
{/each}
|
||||
<span>Group by</span>
|
||||
<div class="group-by">
|
||||
<Multiselect
|
||||
options={groupByOptions}
|
||||
bind:value={groupBy}
|
||||
placeholder="None"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="buttons">
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Add"
|
||||
on:click={addCalc}
|
||||
disabled={calculations.length >= 5}
|
||||
>
|
||||
Add calculation
|
||||
</ActionButton>
|
||||
</div>
|
||||
<InfoDisplay
|
||||
icon="Help"
|
||||
quiet
|
||||
body="Calculations only work with numeric columns and a maximum of 5 calculations can be added at once."
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.calculations {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-m);
|
||||
row-gap: var(--spacing-m);
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.group-by {
|
||||
grid-column: 2 / 5;
|
||||
}
|
||||
span {
|
||||
}
|
||||
</style>
|
|
@ -92,6 +92,7 @@
|
|||
disabled={error ||
|
||||
!name ||
|
||||
(rows.length && (!allValid || displayColumn == null))}
|
||||
size="M"
|
||||
>
|
||||
<Input
|
||||
thin
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
export let title
|
||||
export let align = "left"
|
||||
export let showPopover
|
||||
export let width
|
||||
|
||||
let popover
|
||||
let anchor
|
||||
|
@ -22,8 +23,8 @@
|
|||
<Popover
|
||||
bind:this={popover}
|
||||
bind:open
|
||||
minWidth={400}
|
||||
maxWidth={400}
|
||||
minWidth={width || 400}
|
||||
maxWidth={width || 400}
|
||||
{anchor}
|
||||
{align}
|
||||
{showPopover}
|
||||
|
|
|
@ -13,14 +13,18 @@
|
|||
import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte"
|
||||
import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.svelte"
|
||||
import GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte"
|
||||
import GridViewCalculationButton from "components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte"
|
||||
import { ViewV2Type } from "@budibase/types"
|
||||
|
||||
let generateButton
|
||||
|
||||
$: id = $viewsV2.selected?.id
|
||||
$: view = $viewsV2.selected
|
||||
$: calculation = view?.type === ViewV2Type.CALCULATION
|
||||
$: id = view?.id
|
||||
$: datasource = {
|
||||
type: "viewV2",
|
||||
id,
|
||||
tableId: $viewsV2.selected?.tableId,
|
||||
tableId: view?.tableId,
|
||||
}
|
||||
$: buttons = makeRowActionButtons($rowActions[id])
|
||||
$: rowActions.refreshRowActions(id)
|
||||
|
@ -56,13 +60,18 @@
|
|||
buttonsCollapsed
|
||||
>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if calculation}
|
||||
<GridViewCalculationButton />
|
||||
{/if}
|
||||
<GridManageAccessButton />
|
||||
<GridFilterButton />
|
||||
<GridSortButton />
|
||||
<GridSizeButton />
|
||||
<GridColumnsSettingButton />
|
||||
<GridRowActionsButton />
|
||||
<GridScreensButton on:generate={() => generateButton?.show()} />
|
||||
{#if !calculation}
|
||||
<GridColumnsSettingButton />
|
||||
<GridRowActionsButton />
|
||||
<GridScreensButton on:generate={() => generateButton?.show()} />
|
||||
{/if}
|
||||
<GridGenerateButton bind:this={generateButton} />
|
||||
</svelte:fragment>
|
||||
<GridCreateEditRowModal />
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||
import { Input, notifications, Button, Icon } from "@budibase/bbui"
|
||||
import { Input, notifications, Button, Icon, ListItem } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { viewsV2 } from "stores/builder"
|
||||
import { ViewV2Type } from "@budibase/types"
|
||||
|
||||
export let table
|
||||
export let firstView = false
|
||||
|
||||
let calculation = false
|
||||
let name
|
||||
let popover
|
||||
|
||||
|
@ -37,8 +39,9 @@
|
|||
const newView = await viewsV2.create({
|
||||
name: trimmedName,
|
||||
tableId: table._id,
|
||||
schema: enrichSchema(table.schema),
|
||||
primaryDisplay: table.primaryDisplay,
|
||||
schema: calculation ? {} : enrichSchema(table.schema),
|
||||
primaryDisplay: calculation ? undefined : table.primaryDisplay,
|
||||
type: calculation ? ViewV2Type.CALCULATION : undefined,
|
||||
})
|
||||
notifications.success(`View ${name} created`)
|
||||
$goto(`./${newView.id}`)
|
||||
|
@ -52,6 +55,7 @@
|
|||
title="Create view"
|
||||
bind:this={popover}
|
||||
on:open={() => (name = null)}
|
||||
width={540}
|
||||
>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
{#if firstView}
|
||||
|
@ -66,6 +70,28 @@
|
|||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="options">
|
||||
<div>
|
||||
<ListItem
|
||||
title="Table"
|
||||
subtitle="Create a subset of your data"
|
||||
hoverable
|
||||
on:click={() => (calculation = false)}
|
||||
selected={!calculation}
|
||||
icon="Rail"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ListItem
|
||||
title="Calculation"
|
||||
subtitle="Calculate groups of rows"
|
||||
hoverable
|
||||
on:click={() => (calculation = true)}
|
||||
selected={calculation}
|
||||
icon="123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label="Name"
|
||||
thin
|
||||
|
@ -81,6 +107,11 @@
|
|||
</DetailPopover>
|
||||
|
||||
<style>
|
||||
.options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.icon {
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
export let title
|
||||
export let body
|
||||
export let icon = "HelpOutline"
|
||||
export let quiet = false
|
||||
export let warning = false
|
||||
export let error = false
|
||||
</script>
|
||||
|
||||
<div class="info" class:noTitle={!title} class:warning class:error>
|
||||
<div class="info" class:noTitle={!title} class:warning class:error class:quiet>
|
||||
{#if title}
|
||||
<div class="title">
|
||||
<Icon name={icon} />
|
||||
|
@ -58,7 +59,22 @@
|
|||
.icon {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--background-alt);
|
||||
padding: var(--spacing-m) var(--spacing-l) var(--spacing-m) var(--spacing-l);
|
||||
border-radius: var(--border-radius-s);
|
||||
font-size: 13px;
|
||||
}
|
||||
.quiet {
|
||||
background: none;
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
padding: 0;
|
||||
}
|
||||
.noTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.info :global(a) {
|
||||
color: inherit;
|
||||
transition: color 130ms ease-out;
|
||||
|
|
|
@ -207,7 +207,6 @@ export class ComponentStore extends BudiStore {
|
|||
)
|
||||
for (let setting of filterableTypes || []) {
|
||||
const isLegacy = Array.isArray(enrichedComponent[setting.key])
|
||||
|
||||
if (isLegacy) {
|
||||
const processedSetting = utils.processSearchFilters(
|
||||
enrichedComponent[setting.key]
|
||||
|
|
|
@ -1,30 +1,6 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tables } from "./tables"
|
||||
import { API } from "api"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
function convertToSearchFilters(view) {
|
||||
// convert from UISearchFilter type
|
||||
if (view?.query) {
|
||||
return {
|
||||
...view,
|
||||
queryUI: view.query,
|
||||
query: dataFilters.buildQuery(view.query),
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
function convertToUISearchFilter(view) {
|
||||
if (view?.queryUI) {
|
||||
return {
|
||||
...view,
|
||||
query: view.queryUI,
|
||||
queryUI: undefined,
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
export function createViewsV2Store() {
|
||||
const store = writable({
|
||||
|
@ -36,7 +12,7 @@ export function createViewsV2Store() {
|
|||
const views = Object.values(table?.views || {}).filter(view => {
|
||||
return view.version === 2
|
||||
})
|
||||
list = list.concat(views.map(view => convertToUISearchFilter(view)))
|
||||
list = list.concat(views)
|
||||
})
|
||||
return {
|
||||
...$store,
|
||||
|
@ -58,7 +34,6 @@ export function createViewsV2Store() {
|
|||
}
|
||||
|
||||
const create = async view => {
|
||||
view = convertToSearchFilters(view)
|
||||
const savedViewResponse = await API.viewV2.create(view)
|
||||
const savedView = savedViewResponse.data
|
||||
replaceView(savedView.id, savedView)
|
||||
|
@ -66,7 +41,6 @@ export function createViewsV2Store() {
|
|||
}
|
||||
|
||||
const save = async view => {
|
||||
view = convertToSearchFilters(view)
|
||||
const res = await API.viewV2.update(view)
|
||||
const savedView = res?.data
|
||||
replaceView(view.id, savedView)
|
||||
|
@ -77,7 +51,6 @@ export function createViewsV2Store() {
|
|||
if (!viewId) {
|
||||
return
|
||||
}
|
||||
view = convertToUISearchFilter(view)
|
||||
const existingView = get(derivedStore).list.find(view => view.id === viewId)
|
||||
const tableIndex = get(tables).list.findIndex(table => {
|
||||
return table._id === view?.tableId || table._id === existingView?.tableId
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
import { getContext, createEventDispatcher } from "svelte"
|
||||
import FilterField from "./FilterField.svelte"
|
||||
import ConditionField from "./ConditionField.svelte"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const {
|
||||
|
@ -42,8 +43,7 @@
|
|||
export let toReadable
|
||||
export let toRuntime
|
||||
|
||||
$: editableFilters = filters ? Helpers.cloneDeep(filters) : null
|
||||
|
||||
$: editableFilters = migrateFilters(filters)
|
||||
$: {
|
||||
if (
|
||||
tables.find(
|
||||
|
@ -57,6 +57,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
// We still may need to migrate this even though the backend does it automatically now
|
||||
// for query definitions. This is because we might be editing saved filter definitions
|
||||
// from old screens, which will still be of type LegacyFilter[].
|
||||
const migrateFilters = filters => {
|
||||
if (Array.isArray(filters)) {
|
||||
return utils.processSearchFilters(filters)
|
||||
}
|
||||
return Helpers.cloneDeep(filters)
|
||||
}
|
||||
|
||||
const filterOperatorOptions = Object.values(FilterOperator).map(entry => {
|
||||
return { value: entry, label: Helpers.capitalise(entry) }
|
||||
})
|
||||
|
@ -72,10 +82,12 @@
|
|||
|
||||
const context = getContext("context")
|
||||
|
||||
$: fieldOptions = (schemaFields || []).map(field => ({
|
||||
label: field.displayName || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
$: fieldOptions = (schemaFields || [])
|
||||
.filter(field => !field.calculationType)
|
||||
.map(field => ({
|
||||
label: field.displayName || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
|
||||
const onFieldChange = filter => {
|
||||
const previousType = filter.type
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { getColumnIcon } from "../../../utils/schema"
|
||||
import MigrationModal from "../controls/MigrationModal.svelte"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
import { FieldType, FormulaType } from "@budibase/types"
|
||||
import { FieldType, FormulaType, SortOrder } from "@budibase/types"
|
||||
import { TableNames } from "../../../constants"
|
||||
import GridPopover from "../overlays/GridPopover.svelte"
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
$: sortedBy = column.name === $sort.column
|
||||
$: canMoveLeft = orderable && idx > 0
|
||||
$: canMoveRight = orderable && idx < $scrollableColumns.length - 1
|
||||
$: sortingLabels = getSortingLabels(column.schema?.type)
|
||||
$: sortingLabels = getSortingLabels(column)
|
||||
$: searchable = isColumnSearchable(column)
|
||||
$: resetSearchValue(column.name)
|
||||
$: searching = searchValue != null
|
||||
|
@ -66,8 +66,14 @@
|
|||
editIsOpen = false
|
||||
}
|
||||
|
||||
const getSortingLabels = type => {
|
||||
switch (type) {
|
||||
const getSortingLabels = column => {
|
||||
if (column.calculationType) {
|
||||
return {
|
||||
ascending: "low-high",
|
||||
descending: "high-low",
|
||||
}
|
||||
}
|
||||
switch (column?.schema?.type) {
|
||||
case FieldType.NUMBER:
|
||||
case FieldType.BIGINT:
|
||||
return {
|
||||
|
@ -137,7 +143,7 @@
|
|||
const sortAscending = () => {
|
||||
sort.set({
|
||||
column: column.name,
|
||||
order: "ascending",
|
||||
order: SortOrder.ASCENDING,
|
||||
})
|
||||
open = false
|
||||
}
|
||||
|
@ -145,7 +151,7 @@
|
|||
const sortDescending = () => {
|
||||
sort.set({
|
||||
column: column.name,
|
||||
order: "descending",
|
||||
order: SortOrder.DESCENDING,
|
||||
})
|
||||
open = false
|
||||
}
|
||||
|
@ -318,7 +324,7 @@
|
|||
<Icon
|
||||
hoverable
|
||||
size="S"
|
||||
name={$sort.order === "descending"
|
||||
name={$sort.order === SortOrder.DESCENDING
|
||||
? "SortOrderDown"
|
||||
: "SortOrderUp"}
|
||||
/>
|
||||
|
@ -366,7 +372,8 @@
|
|||
icon="SortOrderUp"
|
||||
on:click={sortAscending}
|
||||
disabled={!canBeSortColumn(column.schema) ||
|
||||
(column.name === $sort.column && $sort.order === "ascending")}
|
||||
(column.name === $sort.column &&
|
||||
$sort.order === SortOrder.ASCENDING)}
|
||||
>
|
||||
Sort {sortingLabels.ascending}
|
||||
</MenuItem>
|
||||
|
@ -374,7 +381,8 @@
|
|||
icon="SortOrderDown"
|
||||
on:click={sortDescending}
|
||||
disabled={!canBeSortColumn(column.schema) ||
|
||||
(column.name === $sort.column && $sort.order === "descending")}
|
||||
(column.name === $sort.column &&
|
||||
$sort.order === SortOrder.DESCENDING)}
|
||||
>
|
||||
Sort {sortingLabels.descending}
|
||||
</MenuItem>
|
||||
|
|
|
@ -41,6 +41,9 @@ const TypeComponentMap = {
|
|||
role: RoleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
if (column.calculationType) {
|
||||
return NumberCell
|
||||
}
|
||||
return (
|
||||
TypeComponentMap[column?.schema?.cellRenderType] ||
|
||||
TypeComponentMap[column?.schema?.type] ||
|
||||
|
|
|
@ -161,10 +161,10 @@ export const initialise = context => {
|
|||
order: fieldSchema.order ?? oldColumn?.order,
|
||||
conditions: fieldSchema.conditions,
|
||||
related: fieldSchema.related,
|
||||
calculationType: fieldSchema.calculationType,
|
||||
}
|
||||
// Override a few properties for primary display
|
||||
if (field === primaryDisplay) {
|
||||
column.visible = true
|
||||
column.order = 0
|
||||
column.primaryDisplay = true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { derivedMemo } from "../../../utils"
|
||||
import { derived } from "svelte/store"
|
||||
import { ViewV2Type } from "@budibase/types"
|
||||
|
||||
export const createStores = context => {
|
||||
const { props } = context
|
||||
|
@ -30,18 +31,26 @@ export const createStores = context => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { props, hasNonAutoColumn } = context
|
||||
const { props, definition, hasNonAutoColumn } = context
|
||||
|
||||
// Derive features
|
||||
const config = derived(
|
||||
[props, hasNonAutoColumn],
|
||||
([$props, $hasNonAutoColumn]) => {
|
||||
[props, definition, hasNonAutoColumn],
|
||||
([$props, $definition, $hasNonAutoColumn]) => {
|
||||
let config = { ...$props }
|
||||
const type = $props.datasource?.type
|
||||
|
||||
// Disable some features if we're editing a view
|
||||
if (type === "viewV2") {
|
||||
config.canEditColumns = false
|
||||
|
||||
// Disable features for calculation views
|
||||
if ($definition?.type === ViewV2Type.CALCULATION) {
|
||||
config.canAddRows = false
|
||||
config.canEditRows = false
|
||||
config.canDeleteRows = false
|
||||
config.canExpandRows = false
|
||||
}
|
||||
}
|
||||
|
||||
// Disable adding rows if we don't have any valid columns
|
||||
|
|
|
@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
|
|||
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
||||
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { ViewV2Type } from "@budibase/types"
|
||||
|
||||
export const createStores = () => {
|
||||
const definition = memo(null)
|
||||
|
@ -81,13 +82,20 @@ export const deriveStores = context => {
|
|||
}
|
||||
)
|
||||
|
||||
const hasBudibaseIdentifiers = derived(datasource, $datasource => {
|
||||
let type = $datasource?.type
|
||||
if (type === "provider") {
|
||||
type = $datasource.value?.datasource?.type
|
||||
const hasBudibaseIdentifiers = derived(
|
||||
[datasource, definition],
|
||||
([$datasource, $definition]) => {
|
||||
let type = $datasource?.type
|
||||
if (type === "provider") {
|
||||
type = $datasource.value?.datasource?.type
|
||||
}
|
||||
// Handle calculation views
|
||||
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
|
||||
return false
|
||||
}
|
||||
return ["table", "viewV2", "link"].includes(type)
|
||||
}
|
||||
return ["table", "viewV2", "link"].includes(type)
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
schema,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SortOrder } from "@budibase/types"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const createActions = context => {
|
||||
|
@ -84,7 +85,7 @@ export const initialise = context => {
|
|||
inlineFilters.set([])
|
||||
sort.set({
|
||||
column: get(initialSortColumn),
|
||||
order: get(initialSortOrder) || "ascending",
|
||||
order: get(initialSortOrder) || SortOrder.ASCENDING,
|
||||
})
|
||||
|
||||
// Update fetch when filter changes
|
||||
|
@ -110,7 +111,7 @@ export const initialise = context => {
|
|||
return
|
||||
}
|
||||
$fetch.update({
|
||||
sortOrder: $sort.order || "ascending",
|
||||
sortOrder: $sort.order || SortOrder.ASCENDING,
|
||||
sortColumn: $sort.column,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SortOrder } from "@budibase/types"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const SuppressErrors = true
|
||||
|
@ -93,7 +94,7 @@ export const initialise = context => {
|
|||
inlineFilters.set([])
|
||||
sort.set({
|
||||
column: get(initialSortColumn),
|
||||
order: get(initialSortOrder) || "ascending",
|
||||
order: get(initialSortOrder) || SortOrder.ASCENDING,
|
||||
})
|
||||
|
||||
// Update fetch when filter changes
|
||||
|
@ -119,7 +120,7 @@ export const initialise = context => {
|
|||
return
|
||||
}
|
||||
$fetch.update({
|
||||
sortOrder: $sort.order || "ascending",
|
||||
sortOrder: $sort.order || SortOrder.ASCENDING,
|
||||
sortColumn: $sort.column,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
import { get } from "svelte/store"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
function convertToSearchFilters(view) {
|
||||
// convert from SearchFilterGroup type
|
||||
if (view?.query) {
|
||||
return {
|
||||
...view,
|
||||
queryUI: view.query,
|
||||
query: dataFilters.buildQuery(view.query),
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
import { SortOrder } from "@budibase/types"
|
||||
|
||||
const SuppressErrors = true
|
||||
|
||||
|
@ -19,7 +7,7 @@ export const createActions = context => {
|
|||
const { API, datasource, columns } = context
|
||||
|
||||
const saveDefinition = async newDefinition => {
|
||||
await API.viewV2.update(convertToSearchFilters(newDefinition))
|
||||
await API.viewV2.update(newDefinition)
|
||||
}
|
||||
|
||||
const saveRow = async row => {
|
||||
|
@ -117,7 +105,7 @@ export const initialise = context => {
|
|||
inlineFilters.set([])
|
||||
sort.set({
|
||||
column: get(initialSortColumn),
|
||||
order: get(initialSortOrder) || "ascending",
|
||||
order: get(initialSortOrder) || SortOrder.ASCENDING,
|
||||
})
|
||||
|
||||
// Keep sort and filter state in line with the view definition when in builder
|
||||
|
@ -133,12 +121,12 @@ export const initialise = context => {
|
|||
if (!get(initialSortColumn)) {
|
||||
sort.set({
|
||||
column: $definition.sort?.field,
|
||||
order: $definition.sort?.order || "ascending",
|
||||
order: $definition.sort?.order || SortOrder.ASCENDING,
|
||||
})
|
||||
}
|
||||
// Only override filter state if we don't have an initial filter
|
||||
if (!get(initialFilter)) {
|
||||
filter.set($definition.queryUI || $definition.query)
|
||||
filter.set($definition.queryUI)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -166,7 +154,7 @@ export const initialise = context => {
|
|||
...$view,
|
||||
sort: {
|
||||
field: $sort.column,
|
||||
order: $sort.order || "ascending",
|
||||
order: $sort.order || SortOrder.ASCENDING,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -197,7 +185,7 @@ export const initialise = context => {
|
|||
if (JSON.stringify($filter) !== JSON.stringify($view.queryUI)) {
|
||||
await datasource.actions.saveDefinition({
|
||||
...$view,
|
||||
query: $filter,
|
||||
queryUI: $filter,
|
||||
})
|
||||
|
||||
// Refresh data since view definition changed
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { derived, get } from "svelte/store"
|
||||
import { memo } from "../../../utils"
|
||||
import { SortOrder } from "@budibase/types"
|
||||
|
||||
export const createStores = context => {
|
||||
const { props } = context
|
||||
|
@ -8,7 +9,7 @@ export const createStores = context => {
|
|||
// Initialise to default props
|
||||
const sort = memo({
|
||||
column: $props.initialSortColumn,
|
||||
order: $props.initialSortOrder || "ascending",
|
||||
order: $props.initialSortOrder || SortOrder.ASCENDING,
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -24,7 +25,10 @@ export const initialise = context => {
|
|||
sort.update(state => ({ ...state, column: newSortColumn }))
|
||||
})
|
||||
initialSortOrder.subscribe(newSortOrder => {
|
||||
sort.update(state => ({ ...state, order: newSortOrder || "ascending" }))
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
order: newSortOrder || SortOrder.ASCENDING,
|
||||
}))
|
||||
})
|
||||
|
||||
// Derive if the current sort column exists in the schema
|
||||
|
@ -40,7 +44,7 @@ export const initialise = context => {
|
|||
if (!exists) {
|
||||
sort.set({
|
||||
column: null,
|
||||
order: "ascending",
|
||||
order: SortOrder.ASCENDING,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@ import { writable, derived, get } from "svelte/store"
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { QueryUtils } from "../utils"
|
||||
import { convertJSONSchemaToTableSchema } from "../utils/json"
|
||||
import { FieldType, SortOrder, SortType } from "@budibase/types"
|
||||
|
||||
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
||||
|
||||
|
@ -37,7 +38,7 @@ export default class DataFetch {
|
|||
|
||||
// Sorting config
|
||||
sortColumn: null,
|
||||
sortOrder: "ascending",
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
sortType: null,
|
||||
|
||||
// Pagination config
|
||||
|
@ -162,17 +163,22 @@ export default class DataFetch {
|
|||
// If we don't have a sort column specified then just ensure we don't set
|
||||
// any sorting params
|
||||
if (!this.options.sortColumn) {
|
||||
this.options.sortOrder = "ascending"
|
||||
this.options.sortOrder = SortOrder.ASCENDING
|
||||
this.options.sortType = null
|
||||
} else {
|
||||
// Otherwise determine what sort type to use base on sort column
|
||||
const type = schema?.[this.options.sortColumn]?.type
|
||||
this.options.sortType =
|
||||
type === "number" || type === "bigint" ? "number" : "string"
|
||||
|
||||
this.options.sortType = SortType.STRING
|
||||
const fieldSchema = schema?.[this.options.sortColumn]
|
||||
if (
|
||||
fieldSchema?.type === FieldType.NUMBER ||
|
||||
fieldSchema?.type === FieldType.BIGINT ||
|
||||
fieldSchema?.calculationType
|
||||
) {
|
||||
this.options.sortType = SortType.NUMBER
|
||||
}
|
||||
// If no sort order, default to ascending
|
||||
if (!this.options.sortOrder) {
|
||||
this.options.sortOrder = "ascending"
|
||||
this.options.sortOrder = SortOrder.ASCENDING
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,7 +316,7 @@ export default class DataFetch {
|
|||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "json") {
|
||||
if (fieldSchema?.type === FieldType.JSON) {
|
||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||
squashObjects: true,
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { get } from "svelte/store"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import { SortOrder } from "@budibase/types"
|
||||
|
||||
export default class TableFetch extends DataFetch {
|
||||
determineFeatureFlags() {
|
||||
|
@ -23,7 +24,7 @@ export default class TableFetch extends DataFetch {
|
|||
query,
|
||||
limit,
|
||||
sort: sortColumn,
|
||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||
sortOrder: sortOrder?.toLowerCase() ?? SortOrder.ASCENDING,
|
||||
sortType,
|
||||
paginate,
|
||||
bookmark: cursor,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ViewV2Type } from "@budibase/types"
|
||||
import DataFetch from "./DataFetch.js"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
|
@ -39,6 +40,19 @@ export default class ViewV2Fetch extends DataFetch {
|
|||
this.options
|
||||
const { cursor, query, definition } = get(this.store)
|
||||
|
||||
// If this is a calculation view and we have no calculations, return nothing
|
||||
if (
|
||||
definition.type === ViewV2Type.CALCULATION &&
|
||||
!Object.values(definition.schema || {}).some(x => x.calculationType)
|
||||
) {
|
||||
return {
|
||||
rows: [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
// If sort/filter params are not defined, update options to store the
|
||||
// params built in to this view. This ensures that we can accurately
|
||||
// compare old and new params and skip a redundant API call.
|
||||
|
@ -67,6 +81,7 @@ export default class ViewV2Fetch extends DataFetch {
|
|||
return {
|
||||
rows: [],
|
||||
hasNextPage: false,
|
||||
cursor: null,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,15 +5,15 @@ export const getColumnIcon = column => {
|
|||
if (column.schema.icon) {
|
||||
return column.schema.icon
|
||||
}
|
||||
|
||||
if (column.calculationType) {
|
||||
return "Calculator"
|
||||
}
|
||||
if (column.schema.autocolumn) {
|
||||
return "MagicWand"
|
||||
}
|
||||
|
||||
if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) {
|
||||
return "User"
|
||||
}
|
||||
|
||||
const { type, subtype } = column.schema
|
||||
const result =
|
||||
typeof TypeIconMap[type] === "object" && subtype
|
||||
|
|
|
@ -4,24 +4,24 @@ export function canBeDisplayColumn(column) {
|
|||
if (!sharedCore.canBeDisplayColumn(column.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
if (column.related) {
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function canBeSortColumn(column) {
|
||||
// Allow sorting by calculation columns
|
||||
if (column.calculationType) {
|
||||
return true
|
||||
}
|
||||
if (!sharedCore.canBeSortColumn(column.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
if (column.related) {
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ describe.each(
|
|||
const isOracle = dbName === DatabaseName.ORACLE
|
||||
const isMsSQL = dbName === DatabaseName.SQL_SERVER
|
||||
const isPostgres = dbName === DatabaseName.POSTGRES
|
||||
const mainTableName = "test_table"
|
||||
|
||||
let rawDatasource: Datasource
|
||||
let datasource: Datasource
|
||||
|
@ -71,15 +72,15 @@ describe.each(
|
|||
|
||||
client = await knexClient(rawDatasource)
|
||||
|
||||
await client.schema.dropTableIfExists("test_table")
|
||||
await client.schema.createTable("test_table", table => {
|
||||
await client.schema.dropTableIfExists(mainTableName)
|
||||
await client.schema.createTable(mainTableName, table => {
|
||||
table.increments("id").primary()
|
||||
table.string("name")
|
||||
table.timestamp("birthday")
|
||||
table.integer("number")
|
||||
})
|
||||
|
||||
await client("test_table").insert([
|
||||
await client(mainTableName).insert([
|
||||
{ name: "one" },
|
||||
{ name: "two" },
|
||||
{ name: "three" },
|
||||
|
@ -105,7 +106,7 @@ describe.each(
|
|||
const query = await createQuery({
|
||||
name: "New Query",
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -114,7 +115,7 @@ describe.each(
|
|||
name: "New Query",
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
|
@ -133,7 +134,7 @@ describe.each(
|
|||
it("should be able to update a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -143,7 +144,7 @@ describe.each(
|
|||
...query,
|
||||
name: "Updated Query",
|
||||
fields: {
|
||||
sql: client("test_table").where({ id: 1 }).toString(),
|
||||
sql: client(mainTableName).where({ id: 1 }).toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -152,7 +153,7 @@ describe.each(
|
|||
name: "Updated Query",
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: client("test_table").where({ id: 1 }).toString(),
|
||||
sql: client(mainTableName).where({ id: 1 }).toString(),
|
||||
},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
|
@ -169,7 +170,7 @@ describe.each(
|
|||
it("should be able to delete a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -188,7 +189,7 @@ describe.each(
|
|||
it("should be able to list queries", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -199,7 +200,7 @@ describe.each(
|
|||
it("should strip sensitive fields for prod apps", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("*").toString(),
|
||||
sql: client(mainTableName).select("*").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -217,7 +218,7 @@ describe.each(
|
|||
const jsonStatement = `COALESCE(json_build_object('name', name),'{"name":{}}'::json)`
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.select([
|
||||
"*",
|
||||
client.raw(
|
||||
|
@ -245,7 +246,7 @@ describe.each(
|
|||
datasourceId: datasource._id!,
|
||||
queryVerb: "read",
|
||||
fields: {
|
||||
sql: client("test_table").where({ id: 1 }).toString(),
|
||||
sql: client(mainTableName).where({ id: 1 }).toString(),
|
||||
},
|
||||
parameters: [],
|
||||
transformer: "return data",
|
||||
|
@ -391,7 +392,7 @@ describe.each(
|
|||
it("should work with dynamic variables", async () => {
|
||||
const basedOnQuery = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("name").where({ id: 1 }).toString(),
|
||||
sql: client(mainTableName).select("name").where({ id: 1 }).toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -440,7 +441,7 @@ describe.each(
|
|||
it("should handle the dynamic base query being deleted", async () => {
|
||||
const basedOnQuery = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("name").where({ id: 1 }).toString(),
|
||||
sql: client(mainTableName).select("name").where({ id: 1 }).toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -494,7 +495,7 @@ describe.each(
|
|||
it("should be able to insert with bindings", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").insert({ name: "{{ foo }}" }).toString(),
|
||||
sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(),
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
|
@ -517,7 +518,7 @@ describe.each(
|
|||
},
|
||||
])
|
||||
|
||||
const rows = await client("test_table").where({ name: "baz" }).select()
|
||||
const rows = await client(mainTableName).where({ name: "baz" }).select()
|
||||
expect(rows).toHaveLength(1)
|
||||
for (const row of rows) {
|
||||
expect(row).toMatchObject({ name: "baz" })
|
||||
|
@ -527,7 +528,7 @@ describe.each(
|
|||
it("should not allow handlebars as parameters", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").insert({ name: "{{ foo }}" }).toString(),
|
||||
sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(),
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
|
@ -563,7 +564,7 @@ describe.each(
|
|||
const date = new Date(datetimeStr)
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.insert({
|
||||
name: "foo",
|
||||
birthday: client.raw("{{ birthday }}"),
|
||||
|
@ -585,7 +586,7 @@ describe.each(
|
|||
|
||||
expect(result.data).toEqual([{ created: true }])
|
||||
|
||||
const rows = await client("test_table")
|
||||
const rows = await client(mainTableName)
|
||||
.where({ birthday: datetimeStr })
|
||||
.select()
|
||||
expect(rows).toHaveLength(1)
|
||||
|
@ -601,7 +602,7 @@ describe.each(
|
|||
async notDateStr => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.insert({ name: client.raw("{{ name }}") })
|
||||
.toString(),
|
||||
},
|
||||
|
@ -622,7 +623,7 @@ describe.each(
|
|||
|
||||
expect(result.data).toEqual([{ created: true }])
|
||||
|
||||
const rows = await client("test_table")
|
||||
const rows = await client(mainTableName)
|
||||
.where({ name: notDateStr })
|
||||
.select()
|
||||
expect(rows).toHaveLength(1)
|
||||
|
@ -634,7 +635,7 @@ describe.each(
|
|||
it("should execute a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").select("*").orderBy("id").toString(),
|
||||
sql: client(mainTableName).select("*").orderBy("id").toString(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -677,7 +678,7 @@ describe.each(
|
|||
it("should be able to transform a query", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").where({ id: 1 }).select("*").toString(),
|
||||
sql: client(mainTableName).where({ id: 1 }).select("*").toString(),
|
||||
},
|
||||
transformer: `
|
||||
data[0].id = data[0].id + 1;
|
||||
|
@ -700,7 +701,7 @@ describe.each(
|
|||
it("should coerce numeric bindings", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.where({ id: client.raw("{{ id }}") })
|
||||
.select("*")
|
||||
.toString(),
|
||||
|
@ -734,7 +735,7 @@ describe.each(
|
|||
it("should be able to update rows", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.update({ name: client.raw("{{ name }}") })
|
||||
.where({ id: client.raw("{{ id }}") })
|
||||
.toString(),
|
||||
|
@ -759,7 +760,7 @@ describe.each(
|
|||
},
|
||||
})
|
||||
|
||||
const rows = await client("test_table").where({ id: 1 }).select()
|
||||
const rows = await client(mainTableName).where({ id: 1 }).select()
|
||||
expect(rows).toEqual([
|
||||
{ id: 1, name: "foo", birthday: null, number: null },
|
||||
])
|
||||
|
@ -768,7 +769,7 @@ describe.each(
|
|||
it("should be able to execute an update that updates no rows", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.update({ name: "updated" })
|
||||
.where({ id: 100 })
|
||||
.toString(),
|
||||
|
@ -778,7 +779,7 @@ describe.each(
|
|||
|
||||
await config.api.query.execute(query._id!)
|
||||
|
||||
const rows = await client("test_table").select()
|
||||
const rows = await client(mainTableName).select()
|
||||
for (const row of rows) {
|
||||
expect(row.name).not.toEqual("updated")
|
||||
}
|
||||
|
@ -787,14 +788,14 @@ describe.each(
|
|||
it("should be able to execute a delete that deletes no rows", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table").where({ id: 100 }).delete().toString(),
|
||||
sql: client(mainTableName).where({ id: 100 }).delete().toString(),
|
||||
},
|
||||
queryVerb: "delete",
|
||||
})
|
||||
|
||||
await config.api.query.execute(query._id!)
|
||||
|
||||
const rows = await client("test_table").select()
|
||||
const rows = await client(mainTableName).select()
|
||||
expect(rows).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
@ -803,7 +804,7 @@ describe.each(
|
|||
it("should be able to delete rows", async () => {
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.where({ id: client.raw("{{ id }}") })
|
||||
.delete()
|
||||
.toString(),
|
||||
|
@ -823,7 +824,7 @@ describe.each(
|
|||
},
|
||||
})
|
||||
|
||||
const rows = await client("test_table").where({ id: 1 }).select()
|
||||
const rows = await client(mainTableName).where({ id: 1 }).select()
|
||||
expect(rows).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
@ -831,7 +832,7 @@ describe.each(
|
|||
|
||||
describe("query through datasource", () => {
|
||||
it("should be able to query the datasource", async () => {
|
||||
const entityId = "test_table"
|
||||
const entityId = mainTableName
|
||||
await config.api.datasource.update({
|
||||
...datasource,
|
||||
entities: {
|
||||
|
@ -876,7 +877,7 @@ describe.each(
|
|||
beforeAll(async () => {
|
||||
queryParams = {
|
||||
fields: {
|
||||
sql: client("test_table")
|
||||
sql: client(mainTableName)
|
||||
.insert({
|
||||
name: client.raw("{{ bindingName }}"),
|
||||
number: client.raw("{{ bindingNumber }}"),
|
||||
|
@ -929,4 +930,34 @@ describe.each(
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should find rows with a binding containing a slash", async () => {
|
||||
const slashValue = "1/10"
|
||||
await client(mainTableName).insert([{ name: slashValue }])
|
||||
|
||||
const query = await createQuery({
|
||||
fields: {
|
||||
sql: client(mainTableName)
|
||||
.select("*")
|
||||
.where("name", "=", client.raw("{{ bindingName }}"))
|
||||
.toString(),
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: "bindingName",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
queryVerb: "read",
|
||||
})
|
||||
const results = await config.api.query.execute(query._id!, {
|
||||
parameters: {
|
||||
bindingName: slashValue,
|
||||
},
|
||||
})
|
||||
expect(results).toBeDefined()
|
||||
expect(results.data.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -24,8 +24,7 @@ import {
|
|||
checkExternalTables,
|
||||
HOST_ADDRESS,
|
||||
} from "./utils"
|
||||
import dayjs from "dayjs"
|
||||
import { NUMBER_REGEX } from "../utilities"
|
||||
import { isDate, NUMBER_REGEX } from "../utilities"
|
||||
import { MySQLColumn } from "./base/types"
|
||||
import { getReadableErrorMessage } from "./base/errorMapping"
|
||||
import { sql } from "@budibase/backend-core"
|
||||
|
@ -129,11 +128,7 @@ export function bindingTypeCoerce(bindings: SqlQueryBinding) {
|
|||
}
|
||||
// if not a number, see if it is a date - important to do in this order as any
|
||||
// integer will be considered a valid date
|
||||
else if (
|
||||
/^\d/.test(binding) &&
|
||||
dayjs(binding).isValid() &&
|
||||
!binding.includes(",")
|
||||
) {
|
||||
else if (isDate(binding)) {
|
||||
let value: any
|
||||
value = new Date(binding)
|
||||
if (isNaN(value)) {
|
||||
|
@ -439,8 +434,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
dumpContent.push(createTableStatement)
|
||||
}
|
||||
|
||||
const schema = dumpContent.join("\n")
|
||||
return schema
|
||||
return dumpContent.join("\n")
|
||||
} finally {
|
||||
this.disconnect()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ import env from "../environment"
|
|||
import { context } from "@budibase/backend-core"
|
||||
import { generateMetadataID } from "../db/utils"
|
||||
import { Document } from "@budibase/types"
|
||||
import dayjs from "dayjs"
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat"
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
export function wait(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
@ -10,6 +14,28 @@ export function wait(ms: number) {
|
|||
export const isDev = env.isDev
|
||||
|
||||
export const NUMBER_REGEX = /^[+-]?([0-9]*[.])?[0-9]+$/g
|
||||
const ACCEPTED_DATE_FORMATS = [
|
||||
"MM/DD/YYYY",
|
||||
"MM/DD/YY",
|
||||
"DD/MM/YYYY",
|
||||
"DD/MM/YY",
|
||||
"YYYY/MM/DD",
|
||||
"YYYY-MM-DD",
|
||||
"YYYY-MM-DDTHH:mm",
|
||||
"YYYY-MM-DDTHH:mm:ss",
|
||||
"YYYY-MM-DDTHH:mm:ss[Z]",
|
||||
"YYYY-MM-DDTHH:mm:ss.SSS[Z]",
|
||||
]
|
||||
|
||||
export function isDate(str: string) {
|
||||
// checks for xx/xx/xx or ISO date timestamp formats
|
||||
for (const format of ACCEPTED_DATE_FORMATS) {
|
||||
if (dayjs(str, format, true).isValid()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function removeFromArray(array: any[], element: any) {
|
||||
const index = array.indexOf(element)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { isDate } from "../"
|
||||
|
||||
describe("isDate", () => {
|
||||
it("should handle DD/MM/YYYY", () => {
|
||||
expect(isDate("01/01/2001")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should handle DD/MM/YY", () => {
|
||||
expect(isDate("01/01/01")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should handle ISO format YYYY-MM-DD", () => {
|
||||
expect(isDate("2001-01-01")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should handle ISO format with time (YYYY-MM-DDTHH:MM)", () => {
|
||||
expect(isDate("2001-01-01T12:30")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should handle ISO format with full timestamp (YYYY-MM-DDTHH:MM:SS)", () => {
|
||||
expect(isDate("2001-01-01T12:30:45")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should handle complete ISO format", () => {
|
||||
expect(isDate("2001-01-01T12:30:00.000Z")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return false for invalid formats", () => {
|
||||
expect(isDate("")).toEqual(false)
|
||||
expect(isDate("1/10")).toEqual(false)
|
||||
expect(isDate("random string")).toEqual(false)
|
||||
expect(isDate("123456")).toEqual(false)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue