diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index c2bd08760a..b740247294 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -20,7 +20,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" -import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte" +import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte" import BarButtonList from "./controls/BarButtonList.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte" @@ -28,6 +28,7 @@ import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte const componentMap = { text: DrawerBindableInput, + plainText: Input, select: Select, radio: RadioGroup, dataSource: DataSourceSelect, diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte deleted file mode 100644 index 291a1b61a8..0000000000 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/FieldSetting.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/FieldSetting.svelte new file mode 100644 index 0000000000..da1fa27ba4 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/FieldSetting.svelte @@ -0,0 +1,91 @@ + + +
+
+ +
+ + {item.field} +
+
+
{item.label || item.field}
+
+
+ +
+
+ + diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte new file mode 100644 index 0000000000..4286328367 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte @@ -0,0 +1,107 @@ + + +{#if columns.primary} +
+
+
+ columns.update(e.detail)} + /> +
+
+
+{/if} + columns.updateSortable(e.detail)} + on:itemChange={e => columns.update(e.detail)} + items={columns.sortable} + listItemKey={"_id"} + listType={FieldSetting} +/> + + diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte new file mode 100644 index 0000000000..1cb29ac6e7 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte @@ -0,0 +1,100 @@ + + +
+
+ +
+ + {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/client/manifest.json b/packages/client/manifest.json index 07645d874a..c7a207fc28 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2698,6 +2698,22 @@ } ] }, + "labelfield": { + "name": "Text Field", + "icon": "Text", + "editable": true, + "size": { + "width": 400, + "height": 32 + }, + "settings": [ + { + "type": "plainText", + "label": "Label", + "key": "label" + } + ] + }, "stringfield": { "name": "Text Field", "icon": "Text", @@ -6308,19 +6324,6 @@ "key": "table", "required": true }, - { - "type": "columns/grid", - "label": "Columns", - "key": "columns", - "dependsOn": [ - "table", - { - "setting": "table.type", - "value": "custom", - "invert": true - } - ] - }, { "type": "filter", "label": "Filtering", @@ -6417,6 +6420,18 @@ "key": "stripeRows", "defaultValue": false }, + { + "section": true, + "name": "Columns", + "settings": [ + { + "type": "columns/grid", + "key": "columns", + "nested": true, + "resetOn": "table" + } + ] + }, { "section": true, "name": "Buttons", diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 0b1c12524a..30040bfe9c 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -19,6 +19,22 @@ export let onRowClick = null export let buttons = null + // parses columns to fix older formats + const getParsedColumns = columns => { + // If the first element has an active key all elements should be in the new format + if (columns?.length && columns[0]?.active !== undefined) { + return columns + } + + return columns?.map(column => ({ + label: column.displayName || column.name, + field: column.name, + active: true, + })) + } + + $: parsedColumns = getParsedColumns(columns) + const context = getContext("context") const component = getContext("component") const { @@ -33,16 +49,17 @@ let grid - $: columnWhitelist = columns?.map(col => col.name) - $: schemaOverrides = getSchemaOverrides(columns) + $: columnWhitelist = parsedColumns + ?.filter(col => col.active) + ?.map(col => col.field) + $: schemaOverrides = getSchemaOverrides(parsedColumns) $: enrichedButtons = enrichButtons(buttons) const getSchemaOverrides = columns => { let overrides = {} columns?.forEach(column => { - overrides[column.name] = { - displayName: column.displayName || column.name, - visible: true, + overrides[column.field] = { + displayName: column.label, } }) return overrides diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 7ee3a19b8a..7b95fe7430 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -55,11 +55,20 @@ export const deriveStores = context => { // Apply whitelist if specified if ($columnWhitelist?.length) { - Object.keys(enrichedSchema).forEach(key => { - if (!$columnWhitelist.includes(key)) { - delete enrichedSchema[key] + const sortedColumns = {} + + $columnWhitelist.forEach((columnKey, idx) => { + const enrichedColumn = enrichedSchema[columnKey] + if (enrichedColumn) { + sortedColumns[columnKey] = { + ...enrichedColumn, + order: idx, + visible: true, + } } }) + + return sortedColumns } return enrichedSchema