Merge pull request #14778 from Budibase/view-calculation-ui

Calculation Views UI
This commit is contained in:
Andrew Kingston 2024-10-24 11:32:22 +01:00 committed by GitHub
commit 7bbe1c2ec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 504 additions and 77 deletions

3
.gitignore vendored
View File

@ -4,11 +4,10 @@ packages/server/runtime_apps/
.idea/ .idea/
bb-airgapped.tar.gz bb-airgapped.tar.gz
*.iml *.iml
packages/server/build/oldClientVersions/**/* packages/server/build/oldClientVersions/**/*
packages/builder/src/components/deploy/clientVersions.json packages/builder/src/components/deploy/clientVersions.json
packages/server/src/integrations/tests/utils/*.lock packages/server/src/integrations/tests/utils/*.lock
packages/builder/vite.config.mjs.timestamp*
# Logs # Logs
logs logs

View File

@ -16,14 +16,17 @@
href={url} href={url}
class="list-item" class="list-item"
class:hoverable={hoverable || url != null} class:hoverable={hoverable || url != null}
class:large={!!subtitle}
on:click on:click
class:selected class:selected
> >
<div class="left"> <div class="list-item__left">
{#if icon === "StatusLight"} {#if icon === "StatusLight"}
<StatusLight square size="L" color={iconColor} /> <StatusLight square size="L" color={iconColor} />
{:else if icon} {:else if icon}
<Icon name={icon} color={iconColor} /> <div class="list-item__icon">
<Icon name={icon} color={iconColor} size={subtitle ? "XL" : "M"} />
</div>
{/if} {/if}
<div class="list-item__text"> <div class="list-item__text">
{#if title} {#if title}
@ -38,7 +41,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="right"> <div class="list-item__right">
<slot name="right" /> <slot name="right" />
{#if showArrow} {#if showArrow}
<Icon name="ChevronRight" /> <Icon name="ChevronRight" />
@ -54,9 +57,12 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--spectrum-global-color-gray-300); 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); gap: var(--spacing-m);
color: var(--spectrum-global-color-gray-800); color: var(--spectrum-global-color-gray-800);
cursor: pointer;
position: relative;
box-sizing: border-box;
} }
.list-item:not(:first-child) { .list-item:not(:first-child) {
border-top: none; border-top: none;
@ -74,27 +80,72 @@
} }
.hoverable:not(.selected):hover { .hoverable:not(.selected):hover {
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);
border-color: var(--spectrum-global-color-gray-400);
} }
.selected { .selected {
background: var(--spectrum-global-color-blue-100); background: var(--spectrum-global-color-blue-100);
} }
.left, /* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
.right { .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; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: var(--spacing-m); 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; width: 0;
flex: 1 1 auto; flex: 1 1 auto;
} }
.right { .list-item__right {
flex: 0 0 auto; flex: 0 0 auto;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
} }
/* Text */
.list-item__text { .list-item__text {
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
@ -106,6 +157,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.list-item__subtitle { .list-item__subtitle {
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-700);
font-size: 12px;
} }
</style> </style>

View File

@ -147,6 +147,9 @@
.spectrum-Dialog--extraLarge { .spectrum-Dialog--extraLarge {
width: 1000px; width: 1000px;
} }
.spectrum-Dialog--medium {
width: 540px;
}
.content-grid { .content-grid {
display: grid; display: grid;

View File

@ -9,13 +9,11 @@
let anchor let anchor
$: columnOptions = $columns $: columnOptions = $columns
.filter(col => canBeSortColumn(col.schema))
.map(col => ({ .map(col => ({
label: col.label || col.name, label: col.label || col.name,
value: col.name, value: col.name,
type: col.schema?.type,
related: col.related,
})) }))
.filter(col => canBeSortColumn(col))
$: orderOptions = getOrderOptions($sort.column, columnOptions) $: orderOptions = getOrderOptions($sort.column, columnOptions)
const getOrderOptions = (column, columnOptions) => { const getOrderOptions = (column, columnOptions) => {

View File

@ -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>

View File

@ -4,6 +4,7 @@
export let title export let title
export let align = "left" export let align = "left"
export let showPopover export let showPopover
export let width
let popover let popover
let anchor let anchor
@ -22,8 +23,8 @@
<Popover <Popover
bind:this={popover} bind:this={popover}
bind:open bind:open
minWidth={400} minWidth={width || 400}
maxWidth={400} maxWidth={width || 400}
{anchor} {anchor}
{align} {align}
{showPopover} {showPopover}

View File

@ -13,14 +13,18 @@
import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte" import GridGenerateButton from "components/backend/DataTable/buttons/grid/GridGenerateButton.svelte"
import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.svelte" import GridScreensButton from "components/backend/DataTable/buttons/grid/GridScreensButton.svelte"
import GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.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 let generateButton
$: id = $viewsV2.selected?.id $: view = $viewsV2.selected
$: calculation = view?.type === ViewV2Type.CALCULATION
$: id = view?.id
$: datasource = { $: datasource = {
type: "viewV2", type: "viewV2",
id, id,
tableId: $viewsV2.selected?.tableId, tableId: view?.tableId,
} }
$: buttons = makeRowActionButtons($rowActions[id]) $: buttons = makeRowActionButtons($rowActions[id])
$: rowActions.refreshRowActions(id) $: rowActions.refreshRowActions(id)
@ -56,13 +60,18 @@
buttonsCollapsed buttonsCollapsed
> >
<svelte:fragment slot="controls"> <svelte:fragment slot="controls">
{#if calculation}
<GridViewCalculationButton />
{/if}
<GridManageAccessButton /> <GridManageAccessButton />
<GridFilterButton /> <GridFilterButton />
<GridSortButton /> <GridSortButton />
<GridSizeButton /> <GridSizeButton />
<GridColumnsSettingButton /> {#if !calculation}
<GridRowActionsButton /> <GridColumnsSettingButton />
<GridScreensButton on:generate={() => generateButton?.show()} /> <GridRowActionsButton />
<GridScreensButton on:generate={() => generateButton?.show()} />
{/if}
<GridGenerateButton bind:this={generateButton} /> <GridGenerateButton bind:this={generateButton} />
</svelte:fragment> </svelte:fragment>
<GridCreateEditRowModal /> <GridCreateEditRowModal />

View File

@ -1,12 +1,14 @@
<script> <script>
import DetailPopover from "components/common/DetailPopover.svelte" 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 { goto } from "@roxi/routify"
import { viewsV2 } from "stores/builder" import { viewsV2 } from "stores/builder"
import { ViewV2Type } from "@budibase/types"
export let table export let table
export let firstView = false export let firstView = false
let calculation = false
let name let name
let popover let popover
@ -37,8 +39,9 @@
const newView = await viewsV2.create({ const newView = await viewsV2.create({
name: trimmedName, name: trimmedName,
tableId: table._id, tableId: table._id,
schema: enrichSchema(table.schema), schema: calculation ? {} : enrichSchema(table.schema),
primaryDisplay: table.primaryDisplay, primaryDisplay: calculation ? undefined : table.primaryDisplay,
type: calculation ? ViewV2Type.CALCULATION : undefined,
}) })
notifications.success(`View ${name} created`) notifications.success(`View ${name} created`)
$goto(`./${newView.id}`) $goto(`./${newView.id}`)
@ -52,6 +55,7 @@
title="Create view" title="Create view"
bind:this={popover} bind:this={popover}
on:open={() => (name = null)} on:open={() => (name = null)}
width={540}
> >
<svelte:fragment slot="anchor" let:open> <svelte:fragment slot="anchor" let:open>
{#if firstView} {#if firstView}
@ -66,6 +70,28 @@
</div> </div>
{/if} {/if}
</svelte:fragment> </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 <Input
label="Name" label="Name"
thin thin
@ -81,6 +107,11 @@
</DetailPopover> </DetailPopover>
<style> <style>
.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-m);
}
.icon { .icon {
height: 32px; height: 32px;
padding: 0 8px; padding: 0 8px;

View File

@ -4,11 +4,12 @@
export let title export let title
export let body export let body
export let icon = "HelpOutline" export let icon = "HelpOutline"
export let quiet = false
export let warning = false export let warning = false
export let error = false export let error = false
</script> </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} {#if title}
<div class="title"> <div class="title">
<Icon name={icon} /> <Icon name={icon} />
@ -58,7 +59,22 @@
.icon { .icon {
color: var(--spectrum-global-color-gray-600); 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) { .info :global(a) {
color: inherit; color: inherit;
transition: color 130ms ease-out; transition: color 130ms ease-out;

View File

@ -79,10 +79,12 @@
const context = getContext("context") const context = getContext("context")
$: fieldOptions = (schemaFields || []).map(field => ({ $: fieldOptions = (schemaFields || [])
label: field.displayName || field.name, .filter(field => !field.calculationType)
value: field.name, .map(field => ({
})) label: field.displayName || field.name,
value: field.name,
}))
const onFieldChange = filter => { const onFieldChange = filter => {
const previousType = filter.type const previousType = filter.type

View File

@ -6,7 +6,7 @@
import { getColumnIcon } from "../../../utils/schema" import { getColumnIcon } from "../../../utils/schema"
import MigrationModal from "../controls/MigrationModal.svelte" import MigrationModal from "../controls/MigrationModal.svelte"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
import { FieldType, FormulaType } from "@budibase/types" import { FieldType, FormulaType, SortOrder } from "@budibase/types"
import { TableNames } from "../../../constants" import { TableNames } from "../../../constants"
import GridPopover from "../overlays/GridPopover.svelte" import GridPopover from "../overlays/GridPopover.svelte"
@ -52,7 +52,7 @@
$: sortedBy = column.name === $sort.column $: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && idx > 0 $: canMoveLeft = orderable && idx > 0
$: canMoveRight = orderable && idx < $scrollableColumns.length - 1 $: canMoveRight = orderable && idx < $scrollableColumns.length - 1
$: sortingLabels = getSortingLabels(column.schema?.type) $: sortingLabels = getSortingLabels(column)
$: searchable = isColumnSearchable(column) $: searchable = isColumnSearchable(column)
$: resetSearchValue(column.name) $: resetSearchValue(column.name)
$: searching = searchValue != null $: searching = searchValue != null
@ -66,8 +66,14 @@
editIsOpen = false editIsOpen = false
} }
const getSortingLabels = type => { const getSortingLabels = column => {
switch (type) { if (column.calculationType) {
return {
ascending: "low-high",
descending: "high-low",
}
}
switch (column?.schema?.type) {
case FieldType.NUMBER: case FieldType.NUMBER:
case FieldType.BIGINT: case FieldType.BIGINT:
return { return {
@ -137,7 +143,7 @@
const sortAscending = () => { const sortAscending = () => {
sort.set({ sort.set({
column: column.name, column: column.name,
order: "ascending", order: SortOrder.ASCENDING,
}) })
open = false open = false
} }
@ -145,7 +151,7 @@
const sortDescending = () => { const sortDescending = () => {
sort.set({ sort.set({
column: column.name, column: column.name,
order: "descending", order: SortOrder.DESCENDING,
}) })
open = false open = false
} }
@ -318,7 +324,7 @@
<Icon <Icon
hoverable hoverable
size="S" size="S"
name={$sort.order === "descending" name={$sort.order === SortOrder.DESCENDING
? "SortOrderDown" ? "SortOrderDown"
: "SortOrderUp"} : "SortOrderUp"}
/> />
@ -366,7 +372,8 @@
icon="SortOrderUp" icon="SortOrderUp"
on:click={sortAscending} on:click={sortAscending}
disabled={!canBeSortColumn(column.schema) || disabled={!canBeSortColumn(column.schema) ||
(column.name === $sort.column && $sort.order === "ascending")} (column.name === $sort.column &&
$sort.order === SortOrder.ASCENDING)}
> >
Sort {sortingLabels.ascending} Sort {sortingLabels.ascending}
</MenuItem> </MenuItem>
@ -374,7 +381,8 @@
icon="SortOrderDown" icon="SortOrderDown"
on:click={sortDescending} on:click={sortDescending}
disabled={!canBeSortColumn(column.schema) || disabled={!canBeSortColumn(column.schema) ||
(column.name === $sort.column && $sort.order === "descending")} (column.name === $sort.column &&
$sort.order === SortOrder.DESCENDING)}
> >
Sort {sortingLabels.descending} Sort {sortingLabels.descending}
</MenuItem> </MenuItem>

View File

@ -41,6 +41,9 @@ const TypeComponentMap = {
role: RoleCell, role: RoleCell,
} }
export const getCellRenderer = column => { export const getCellRenderer = column => {
if (column.calculationType) {
return NumberCell
}
return ( return (
TypeComponentMap[column?.schema?.cellRenderType] || TypeComponentMap[column?.schema?.cellRenderType] ||
TypeComponentMap[column?.schema?.type] || TypeComponentMap[column?.schema?.type] ||

View File

@ -161,10 +161,10 @@ export const initialise = context => {
order: fieldSchema.order ?? oldColumn?.order, order: fieldSchema.order ?? oldColumn?.order,
conditions: fieldSchema.conditions, conditions: fieldSchema.conditions,
related: fieldSchema.related, related: fieldSchema.related,
calculationType: fieldSchema.calculationType,
} }
// Override a few properties for primary display // Override a few properties for primary display
if (field === primaryDisplay) { if (field === primaryDisplay) {
column.visible = true
column.order = 0 column.order = 0
column.primaryDisplay = true column.primaryDisplay = true
} }

View File

@ -1,5 +1,6 @@
import { derivedMemo } from "../../../utils" import { derivedMemo } from "../../../utils"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { ViewV2Type } from "@budibase/types"
export const createStores = context => { export const createStores = context => {
const { props } = context const { props } = context
@ -30,18 +31,26 @@ export const createStores = context => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { props, hasNonAutoColumn } = context const { props, definition, hasNonAutoColumn } = context
// Derive features // Derive features
const config = derived( const config = derived(
[props, hasNonAutoColumn], [props, definition, hasNonAutoColumn],
([$props, $hasNonAutoColumn]) => { ([$props, $definition, $hasNonAutoColumn]) => {
let config = { ...$props } let config = { ...$props }
const type = $props.datasource?.type const type = $props.datasource?.type
// Disable some features if we're editing a view // Disable some features if we're editing a view
if (type === "viewV2") { if (type === "viewV2") {
config.canEditColumns = false 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 // Disable adding rows if we don't have any valid columns

View File

@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch" import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils" import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { ViewV2Type } from "@budibase/types"
export const createStores = () => { export const createStores = () => {
const definition = memo(null) const definition = memo(null)
@ -81,13 +82,20 @@ export const deriveStores = context => {
} }
) )
const hasBudibaseIdentifiers = derived(datasource, $datasource => { const hasBudibaseIdentifiers = derived(
let type = $datasource?.type [datasource, definition],
if (type === "provider") { ([$datasource, $definition]) => {
type = $datasource.value?.datasource?.type 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 { return {
schema, schema,

View File

@ -1,3 +1,4 @@
import { SortOrder } from "@budibase/types"
import { get } from "svelte/store" import { get } from "svelte/store"
export const createActions = context => { export const createActions = context => {
@ -84,7 +85,7 @@ export const initialise = context => {
inlineFilters.set([]) inlineFilters.set([])
sort.set({ sort.set({
column: get(initialSortColumn), column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending", order: get(initialSortOrder) || SortOrder.ASCENDING,
}) })
// Update fetch when filter changes // Update fetch when filter changes
@ -110,7 +111,7 @@ export const initialise = context => {
return return
} }
$fetch.update({ $fetch.update({
sortOrder: $sort.order || "ascending", sortOrder: $sort.order || SortOrder.ASCENDING,
sortColumn: $sort.column, sortColumn: $sort.column,
}) })
}) })

View File

@ -1,3 +1,4 @@
import { SortOrder } from "@budibase/types"
import { get } from "svelte/store" import { get } from "svelte/store"
const SuppressErrors = true const SuppressErrors = true
@ -93,7 +94,7 @@ export const initialise = context => {
inlineFilters.set([]) inlineFilters.set([])
sort.set({ sort.set({
column: get(initialSortColumn), column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending", order: get(initialSortOrder) || SortOrder.ASCENDING,
}) })
// Update fetch when filter changes // Update fetch when filter changes
@ -119,7 +120,7 @@ export const initialise = context => {
return return
} }
$fetch.update({ $fetch.update({
sortOrder: $sort.order || "ascending", sortOrder: $sort.order || SortOrder.ASCENDING,
sortColumn: $sort.column, sortColumn: $sort.column,
}) })
}) })

View File

@ -1,4 +1,5 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { SortOrder } from "@budibase/types"
const SuppressErrors = true const SuppressErrors = true
@ -104,7 +105,7 @@ export const initialise = context => {
inlineFilters.set([]) inlineFilters.set([])
sort.set({ sort.set({
column: get(initialSortColumn), 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 // Keep sort and filter state in line with the view definition when in builder
@ -120,7 +121,7 @@ export const initialise = context => {
if (!get(initialSortColumn)) { if (!get(initialSortColumn)) {
sort.set({ sort.set({
column: $definition.sort?.field, 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 // Only override filter state if we don't have an initial filter
@ -153,7 +154,7 @@ export const initialise = context => {
...$view, ...$view,
sort: { sort: {
field: $sort.column, field: $sort.column,
order: $sort.order || "ascending", order: $sort.order || SortOrder.ASCENDING,
}, },
}) })
} }

View File

@ -1,5 +1,6 @@
import { derived, get } from "svelte/store" import { derived, get } from "svelte/store"
import { memo } from "../../../utils" import { memo } from "../../../utils"
import { SortOrder } from "@budibase/types"
export const createStores = context => { export const createStores = context => {
const { props } = context const { props } = context
@ -8,7 +9,7 @@ export const createStores = context => {
// Initialise to default props // Initialise to default props
const sort = memo({ const sort = memo({
column: $props.initialSortColumn, column: $props.initialSortColumn,
order: $props.initialSortOrder || "ascending", order: $props.initialSortOrder || SortOrder.ASCENDING,
}) })
return { return {
@ -24,7 +25,10 @@ export const initialise = context => {
sort.update(state => ({ ...state, column: newSortColumn })) sort.update(state => ({ ...state, column: newSortColumn }))
}) })
initialSortOrder.subscribe(newSortOrder => { 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 // Derive if the current sort column exists in the schema
@ -40,7 +44,7 @@ export const initialise = context => {
if (!exists) { if (!exists) {
sort.set({ sort.set({
column: null, column: null,
order: "ascending", order: SortOrder.ASCENDING,
}) })
} }
}) })

View File

@ -2,6 +2,7 @@ import { writable, derived, get } from "svelte/store"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { QueryUtils } from "../utils" import { QueryUtils } from "../utils"
import { convertJSONSchemaToTableSchema } from "../utils/json" import { convertJSONSchemaToTableSchema } from "../utils/json"
import { FieldType, SortOrder, SortType } from "@budibase/types"
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
@ -37,7 +38,7 @@ export default class DataFetch {
// Sorting config // Sorting config
sortColumn: null, sortColumn: null,
sortOrder: "ascending", sortOrder: SortOrder.ASCENDING,
sortType: null, sortType: null,
// Pagination config // 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 // If we don't have a sort column specified then just ensure we don't set
// any sorting params // any sorting params
if (!this.options.sortColumn) { if (!this.options.sortColumn) {
this.options.sortOrder = "ascending" this.options.sortOrder = SortOrder.ASCENDING
this.options.sortType = null this.options.sortType = null
} else { } else {
// Otherwise determine what sort type to use base on sort column // Otherwise determine what sort type to use base on sort column
const type = schema?.[this.options.sortColumn]?.type this.options.sortType = SortType.STRING
this.options.sortType = const fieldSchema = schema?.[this.options.sortColumn]
type === "number" || type === "bigint" ? "number" : "string" if (
fieldSchema?.type === FieldType.NUMBER ||
fieldSchema?.type === FieldType.BIGINT ||
fieldSchema?.calculationType
) {
this.options.sortType = SortType.NUMBER
}
// If no sort order, default to ascending // If no sort order, default to ascending
if (!this.options.sortOrder) { if (!this.options.sortOrder) {
this.options.sortOrder = "ascending" this.options.sortOrder = SortOrder.ASCENDING
} }
} }
@ -310,7 +316,7 @@ export default class DataFetch {
let jsonAdditions = {} let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => { Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey] const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") { if (fieldSchema?.type === FieldType.JSON) {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, { const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true, squashObjects: true,
}) })

View File

@ -1,5 +1,6 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import DataFetch from "./DataFetch.js" import DataFetch from "./DataFetch.js"
import { SortOrder } from "@budibase/types"
export default class TableFetch extends DataFetch { export default class TableFetch extends DataFetch {
determineFeatureFlags() { determineFeatureFlags() {
@ -23,7 +24,7 @@ export default class TableFetch extends DataFetch {
query, query,
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending", sortOrder: sortOrder?.toLowerCase() ?? SortOrder.ASCENDING,
sortType, sortType,
paginate, paginate,
bookmark: cursor, bookmark: cursor,

View File

@ -1,3 +1,4 @@
import { ViewV2Type } from "@budibase/types"
import DataFetch from "./DataFetch.js" import DataFetch from "./DataFetch.js"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -39,6 +40,19 @@ export default class ViewV2Fetch extends DataFetch {
this.options this.options
const { cursor, query, definition } = get(this.store) 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 // If sort/filter params are not defined, update options to store the
// params built in to this view. This ensures that we can accurately // params built in to this view. This ensures that we can accurately
// compare old and new params and skip a redundant API call. // compare old and new params and skip a redundant API call.
@ -67,6 +81,7 @@ export default class ViewV2Fetch extends DataFetch {
return { return {
rows: [], rows: [],
hasNextPage: false, hasNextPage: false,
cursor: null,
error, error,
} }
} }

View File

@ -5,15 +5,15 @@ export const getColumnIcon = column => {
if (column.schema.icon) { if (column.schema.icon) {
return column.schema.icon return column.schema.icon
} }
if (column.calculationType) {
return "Calculator"
}
if (column.schema.autocolumn) { if (column.schema.autocolumn) {
return "MagicWand" return "MagicWand"
} }
if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) { if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) {
return "User" return "User"
} }
const { type, subtype } = column.schema const { type, subtype } = column.schema
const result = const result =
typeof TypeIconMap[type] === "object" && subtype typeof TypeIconMap[type] === "object" && subtype

View File

@ -4,24 +4,24 @@ export function canBeDisplayColumn(column) {
if (!sharedCore.canBeDisplayColumn(column.type)) { if (!sharedCore.canBeDisplayColumn(column.type)) {
return false 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 (column.related) {
// If it's a related column (only available in the frontend), don't allow using it as display column
return false return false
} }
return true return true
} }
export function canBeSortColumn(column) { export function canBeSortColumn(column) {
// Allow sorting by calculation columns
if (column.calculationType) {
return true
}
if (!sharedCore.canBeSortColumn(column.type)) { if (!sharedCore.canBeSortColumn(column.type)) {
return false 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 (column.related) {
// If it's a related column (only available in the frontend), don't allow using it as display column
return false return false
} }
return true return true
} }