diff --git a/.gitignore b/.gitignore index 32d1416f4a..bac643e5df 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte index 699df2d456..e979b2b684 100644 --- a/packages/bbui/src/List/ListItem.svelte +++ b/packages/bbui/src/List/ListItem.svelte @@ -16,14 +16,17 @@ href={url} class="list-item" class:hoverable={hoverable || url != null} + class:large={!!subtitle} on:click class:selected > -
+
{#if icon === "StatusLight"} {:else if icon} - +
+ +
{/if}
{#if title} @@ -38,7 +41,7 @@ {/if}
-
+
{#if showArrow} @@ -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; } diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 22a14c358f..8a80b2fdb9 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -147,6 +147,9 @@ .spectrum-Dialog--extraLarge { width: 1000px; } + .spectrum-Dialog--medium { + width: 540px; + } .content-grid { display: grid; diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte index 5a1f6b221a..4591b22b78 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte @@ -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) => { diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte new file mode 100644 index 0000000000..72216f3b3b --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte @@ -0,0 +1,259 @@ + + + + Configure calculations{count ? `: ${count}` : ""} + + + + + {#if calculations.length} +
+ {#each calculations as calc, idx} + {idx === 0 ? "Calculate" : "and"} the + + deleteCalc(idx)} + color="var(--spectrum-global-color-gray-700)" + /> + {/each} + Group by +
+ +
+
+ {/if} +
+ = 5} + > + Add calculation + +
+ +
+
+ + diff --git a/packages/builder/src/components/common/DetailPopover.svelte b/packages/builder/src/components/common/DetailPopover.svelte index bc891bfe42..c437804b33 100644 --- a/packages/builder/src/components/common/DetailPopover.svelte +++ b/packages/builder/src/components/common/DetailPopover.svelte @@ -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 @@ + {#if calculation} + + {/if} - - - generateButton?.show()} /> + {#if !calculation} + + + generateButton?.show()} /> + {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte index 8af5007022..ecc85e7622 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte @@ -1,12 +1,14 @@ -
+
{#if title}
@@ -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; diff --git a/packages/frontend-core/src/components/CoreFilterBuilder.svelte b/packages/frontend-core/src/components/CoreFilterBuilder.svelte index 9401d8e384..a28cffd85d 100644 --- a/packages/frontend-core/src/components/CoreFilterBuilder.svelte +++ b/packages/frontend-core/src/components/CoreFilterBuilder.svelte @@ -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 diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 6c1c025fcd..93db2b6317 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -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 @@ @@ -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} @@ -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} diff --git a/packages/frontend-core/src/components/grid/lib/renderers.js b/packages/frontend-core/src/components/grid/lib/renderers.js index 70700f9417..a860d01b53 100644 --- a/packages/frontend-core/src/components/grid/lib/renderers.js +++ b/packages/frontend-core/src/components/grid/lib/renderers.js @@ -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] || diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index f23a17f14c..5f2800ba7a 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -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 } diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index fc0435f92d..4a60370690 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -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 diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 869cd56730..6aa607f7ed 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -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, diff --git a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js index f3b8a6e02d..ea558d6236 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js @@ -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, }) }) diff --git a/packages/frontend-core/src/components/grid/stores/datasources/table.js b/packages/frontend-core/src/components/grid/stores/datasources/table.js index 2ba95e3a74..e415f5914b 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/table.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/table.js @@ -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, }) }) diff --git a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js index 24170a59b3..4a4a91658c 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js @@ -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, }, }) } diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 336570d012..9ab393b11f 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -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, }) } }) diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index a056cdff5d..fb1dbd5885 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -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, }) diff --git a/packages/frontend-core/src/fetch/TableFetch.js b/packages/frontend-core/src/fetch/TableFetch.js index a13b1bd186..ed17c20c79 100644 --- a/packages/frontend-core/src/fetch/TableFetch.js +++ b/packages/frontend-core/src/fetch/TableFetch.js @@ -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, diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.js b/packages/frontend-core/src/fetch/ViewV2Fetch.js index 40135746df..6bfef0927f 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.js +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.js @@ -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, } } diff --git a/packages/frontend-core/src/utils/schema.js b/packages/frontend-core/src/utils/schema.js index aac9854e69..e4e26dce39 100644 --- a/packages/frontend-core/src/utils/schema.js +++ b/packages/frontend-core/src/utils/schema.js @@ -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 diff --git a/packages/frontend-core/src/utils/table.js b/packages/frontend-core/src/utils/table.js index 193848ec4d..b7abd572c7 100644 --- a/packages/frontend-core/src/utils/table.js +++ b/packages/frontend-core/src/utils/table.js @@ -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 }