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

View File

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

View File

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

View File

@ -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) => {

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

View File

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

View File

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

View File

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

View File

@ -79,10 +79,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
})
})

View File

@ -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,
})
})

View File

@ -1,4 +1,5 @@
import { get } from "svelte/store"
import { SortOrder } from "@budibase/types"
const SuppressErrors = true
@ -104,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
@ -120,7 +121,7 @@ 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
@ -153,7 +154,7 @@ export const initialise = context => {
...$view,
sort: {
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 { 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,
})
}
})

View File

@ -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,
})

View File

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

View File

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

View File

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

View File

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