+
+
+
+
+ {item.field}
+
+
+
{item.label || item.field}
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
new file mode 100644
index 0000000000..72fdbe4108
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
@@ -0,0 +1,129 @@
+const modernize = columns => {
+ if (!columns) {
+ return []
+ }
+ // If the first element has no active key then it's safe to assume all elements are in the old format
+ if (columns?.[0] && columns[0].active === undefined) {
+ return columns.map(column => ({
+ label: column.displayName,
+ field: column.name,
+ active: true,
+ }))
+ }
+
+ return columns
+}
+
+const removeInvalidAddMissing = (
+ columns = [],
+ defaultColumns,
+ primaryDisplayColumnName
+) => {
+ const defaultColumnNames = defaultColumns.map(column => column.field)
+ const columnNames = columns.map(column => column.field)
+
+ const validColumns = columns.filter(column =>
+ defaultColumnNames.includes(column.field)
+ )
+ let missingColumns = defaultColumns.filter(
+ defaultColumn => !columnNames.includes(defaultColumn.field)
+ )
+
+ // If the user already has fields selected, any appended missing fields should be disabled by default
+ if (validColumns.length) {
+ missingColumns = missingColumns.map(field => ({ ...field, active: false }))
+ }
+
+ const combinedColumns = [...validColumns, ...missingColumns]
+
+ // Ensure the primary display column is always visible
+ const primaryDisplayIndex = combinedColumns.findIndex(
+ column => column.field === primaryDisplayColumnName
+ )
+ if (primaryDisplayIndex > -1) {
+ combinedColumns[primaryDisplayIndex].active = true
+ }
+
+ return combinedColumns
+}
+
+const getDefault = (schema = {}) => {
+ const defaultValues = Object.values(schema)
+ .filter(column => !column.nestedJSON)
+ .map(column => ({
+ label: column.name,
+ field: column.name,
+ active: column.visible ?? true,
+ order: column.visible ? column.order ?? -1 : Number.MAX_SAFE_INTEGER,
+ }))
+
+ defaultValues.sort((a, b) => a.order - b.order)
+
+ return defaultValues
+}
+
+const toGridFormat = draggableListColumns => {
+ return draggableListColumns.map(entry => ({
+ label: entry.label,
+ field: entry.field,
+ active: entry.active,
+ }))
+}
+
+const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
+ return gridFormatColumns.map(column => {
+ return createComponent(
+ "@budibase/standard-components/labelfield",
+ {
+ _instanceName: column.field,
+ active: column.active,
+ field: column.field,
+ label: column.label,
+ columnType: schema[column.field].type,
+ },
+ {}
+ )
+ })
+}
+
+const getColumns = ({
+ columns,
+ schema,
+ primaryDisplayColumnName,
+ onChange,
+ createComponent,
+}) => {
+ const validatedColumns = removeInvalidAddMissing(
+ modernize(columns),
+ getDefault(schema),
+ primaryDisplayColumnName
+ )
+ const draggableList = toDraggableListFormat(
+ validatedColumns,
+ createComponent,
+ schema
+ )
+ const primary = draggableList.find(
+ entry => entry.field === primaryDisplayColumnName
+ )
+ const sortable = draggableList.filter(
+ entry => entry.field !== primaryDisplayColumnName
+ )
+
+ return {
+ primary,
+ sortable,
+ updateSortable: newDraggableList => {
+ onChange(toGridFormat(newDraggableList.concat(primary)))
+ },
+ update: newEntry => {
+ const newDraggableList = draggableList.map(entry => {
+ return newEntry.field === entry.field ? newEntry : entry
+ })
+
+ onChange(toGridFormat(newDraggableList))
+ },
+ }
+}
+
+export default getColumns
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js
new file mode 100644
index 0000000000..d7092a2c52
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js
@@ -0,0 +1,374 @@
+import { it, expect, describe, beforeEach, vi } from "vitest"
+import getColumns from "./getColumns"
+
+describe("getColumns", () => {
+ beforeEach(ctx => {
+ ctx.schema = {
+ one: { name: "one", visible: false, order: 0, type: "foo" },
+ two: { name: "two", visible: true, order: 1, type: "foo" },
+ three: { name: "three", visible: true, order: 2, type: "foo" },
+ four: { name: "four", visible: false, order: 3, type: "foo" },
+ five: {
+ name: "excluded",
+ visible: true,
+ order: 4,
+ type: "foo",
+ nestedJSON: true,
+ },
+ }
+
+ ctx.primaryDisplayColumnName = "four"
+ ctx.onChange = vi.fn()
+ ctx.createComponent = (componentName, props) => {
+ return { componentName, ...props }
+ }
+ })
+
+ describe("nested json fields", () => {
+ beforeEach(ctx => {
+ ctx.columns = getColumns({
+ columns: null,
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+ })
+
+ it("does not return nested json fields, as the grid cannot display them", ctx => {
+ expect(ctx.columns.sortable).not.toContainEqual({
+ name: "excluded",
+ visible: true,
+ order: 4,
+ type: "foo",
+ nestedJSON: true,
+ })
+ })
+ })
+
+ describe("using the old grid column format", () => {
+ beforeEach(ctx => {
+ const oldGridFormatColumns = [
+ { displayName: "three label", name: "three" },
+ { displayName: "two label", name: "two" },
+ ]
+
+ ctx.columns = getColumns({
+ columns: oldGridFormatColumns,
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+ })
+
+ it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
+ expect(ctx.columns.sortable).toEqual([
+ {
+ _instanceName: "three",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "three",
+ label: "three label",
+ },
+ {
+ _instanceName: "two",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "two",
+ label: "two label",
+ },
+ {
+ _instanceName: "one",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "one",
+ label: "one",
+ },
+ ])
+
+ expect(ctx.columns.primary).toEqual({
+ _instanceName: "four",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "four",
+ label: "four",
+ })
+ })
+ })
+
+ describe("default columns", () => {
+ beforeEach(ctx => {
+ ctx.columns = getColumns({
+ columns: undefined,
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+ })
+
+ it("returns all columns, with non-hidden columns automatically selected", ctx => {
+ expect(ctx.columns.sortable).toEqual([
+ {
+ _instanceName: "two",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "two",
+ label: "two",
+ },
+ {
+ _instanceName: "three",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "three",
+ label: "three",
+ },
+ {
+ _instanceName: "one",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "one",
+ label: "one",
+ },
+ ])
+
+ expect(ctx.columns.primary).toEqual({
+ _instanceName: "four",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "four",
+ label: "four",
+ })
+ })
+
+ it("Unselected columns should be placed at the end", ctx => {
+ expect(ctx.columns.sortable[2].field).toEqual("one")
+ })
+ })
+
+ describe("missing columns", () => {
+ beforeEach(ctx => {
+ const gridFormatColumns = [
+ { label: "three label", field: "three", active: true },
+ ]
+
+ ctx.columns = getColumns({
+ columns: gridFormatColumns,
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+ })
+
+ it("returns all columns, including those missing from the initial data", ctx => {
+ expect(ctx.columns.sortable).toEqual([
+ {
+ _instanceName: "three",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "three",
+ label: "three label",
+ },
+ {
+ _instanceName: "two",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "two",
+ label: "two",
+ },
+ {
+ _instanceName: "one",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "one",
+ label: "one",
+ },
+ ])
+
+ expect(ctx.columns.primary).toEqual({
+ _instanceName: "four",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "four",
+ label: "four",
+ })
+ })
+ })
+
+ describe("invalid columns", () => {
+ beforeEach(ctx => {
+ const gridFormatColumns = [
+ { label: "three label", field: "three", active: true },
+ { label: "some nonsense", field: "some nonsense", active: true },
+ ]
+
+ ctx.columns = getColumns({
+ columns: gridFormatColumns,
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+ })
+
+ it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
+ expect(ctx.columns.sortable).toEqual([
+ {
+ _instanceName: "three",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "three",
+ label: "three label",
+ },
+ {
+ _instanceName: "two",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "two",
+ label: "two",
+ },
+ {
+ _instanceName: "one",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "one",
+ label: "one",
+ },
+ ])
+
+ expect(ctx.columns.primary).toEqual({
+ _instanceName: "four",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "four",
+ label: "four",
+ })
+ })
+ })
+
+ describe("methods", () => {
+ beforeEach(ctx => {
+ const { update, updateSortable } = getColumns({
+ columns: [],
+ schema: ctx.schema,
+ primaryDisplayColumnName: ctx.primaryDisplayColumnName,
+ onChange: ctx.onChange,
+ createComponent: ctx.createComponent,
+ })
+
+ ctx.update = update
+ ctx.updateSortable = updateSortable
+ })
+
+ describe("update", () => {
+ beforeEach(ctx => {
+ ctx.update({
+ field: "one",
+ label: "a new label",
+ active: true,
+ })
+ })
+
+ it("calls the callback with the updated columns", ctx => {
+ expect(ctx.onChange).toHaveBeenCalledTimes(1)
+ expect(ctx.onChange).toHaveBeenCalledWith([
+ {
+ field: "two",
+ label: "two",
+ active: true,
+ },
+ {
+ field: "three",
+ label: "three",
+ active: true,
+ },
+ {
+ field: "one",
+ label: "a new label",
+ active: true,
+ },
+ {
+ field: "four",
+ label: "four",
+ active: true,
+ },
+ ])
+ })
+ })
+
+ describe("updateSortable", () => {
+ beforeEach(ctx => {
+ ctx.updateSortable([
+ {
+ _instanceName: "three",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "three",
+ label: "three",
+ },
+ {
+ _instanceName: "one",
+ active: true,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "one",
+ label: "one",
+ },
+ {
+ _instanceName: "two",
+ active: false,
+ columnType: "foo",
+ componentName: "@budibase/standard-components/labelfield",
+ field: "two",
+ label: "two",
+ },
+ ])
+ })
+
+ it("calls the callback with the updated columns", ctx => {
+ expect(ctx.onChange).toHaveBeenCalledTimes(1)
+ expect(ctx.onChange).toHaveBeenCalledWith([
+ {
+ field: "three",
+ label: "three",
+ active: true,
+ },
+ {
+ field: "one",
+ label: "one",
+ active: true,
+ },
+ {
+ field: "two",
+ label: "two",
+ active: false,
+ },
+ {
+ field: "four",
+ label: "four",
+ active: true,
+ },
+ ])
+ })
+ })
+ })
+})
diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
index a67c2d3c61..c3bb5171a4 100644
--- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
@@ -781,7 +781,7 @@
{/if}
{:else}
-
@@ -808,19 +808,21 @@
: Constants.BudibaseRoleOptionsNew.filter(
option => option.value !== Constants.BudibaseRoles.Admin
)}
- label="Role"
+ label="Access"
/>
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
-
+
+
+
{/if}
{#if creationRoleType === Constants.BudibaseRoles.Admin}
@@ -847,6 +849,13 @@