- Sign in to {company}
+ Sign in to {company}
{#if loaded}
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 1f29012197..23967f188a 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -9,7 +9,8 @@
"messagePassing": true,
"rowSelection": true,
"continueIfAction": true,
- "showNotificationAction": true
+ "showNotificationAction": true,
+ "sidePanel": true
},
"layout": {
"name": "Layout",
@@ -3669,7 +3670,7 @@
"Ascending",
"Descending"
],
- "defaultValue": "Descending"
+ "defaultValue": "Ascending"
},
{
"type": "number",
@@ -3736,12 +3737,6 @@
"key": "dataProvider",
"required": true
},
- {
- "type": "number",
- "label": "Row count",
- "key": "rowCount",
- "defaultValue": 8
- },
{
"type": "columns",
"label": "Columns",
@@ -3765,6 +3760,12 @@
}
]
},
+ {
+ "type": "number",
+ "label": "Row count",
+ "key": "rowCount",
+ "defaultValue": 8
+ },
{
"type": "boolean",
"label": "Quiet",
@@ -3775,12 +3776,6 @@
"label": "Compact",
"key": "compact"
},
- {
- "type": "boolean",
- "label": "Show auto columns",
- "key": "showAutoColumns",
- "defaultValue": false
- },
{
"type": "boolean",
"label": "Allow row selection",
@@ -3788,30 +3783,20 @@
"defaultValue": false,
"info": "Row selection is only compatible with internal or SQL tables"
},
- {
- "type": "boolean",
- "label": "Link table rows",
- "key": "linkRows"
- },
- {
- "type": "boolean",
- "label": "Open link screens in modal",
- "key": "linkPeek"
- },
- {
- "type": "url",
- "label": "Link screen",
- "key": "linkURL"
- },
+
{
"section": true,
- "name": "Advanced",
+ "name": "On Row Click",
"settings": [
{
- "type": "field",
- "label": "ID column for linking (appended to URL)",
- "key": "linkColumn",
- "placeholder": "Default"
+ "type": "event",
+ "key": "onClick",
+ "context": [
+ {
+ "label": "Clicked row",
+ "key": "row"
+ }
+ ]
}
]
}
@@ -4463,70 +4448,15 @@
"label": "Title",
"key": "title"
},
- {
- "type": "dataSource",
- "label": "Data",
- "key": "dataSource",
- "required": true
- },
- {
- "type": "searchfield",
- "label": "Search Columns",
- "key": "searchColumns",
- "placeholder": "Choose search columns",
- "info": "Only the first 5 search columns will be used"
- },
- {
- "type": "filter",
- "label": "Filtering",
- "key": "filter"
- },
- {
- "type": "field/sortable",
- "label": "Sort Column",
- "key": "sortColumn"
- },
- {
- "type": "select",
- "label": "Sort Order",
- "key": "sortOrder",
- "options": [
- "Ascending",
- "Descending"
- ],
- "defaultValue": "Descending"
- },
- {
- "type": "select",
- "label": "Size",
- "key": "size",
- "defaultValue": "spectrum--medium",
- "options": [
- {
- "label": "Medium",
- "value": "spectrum--medium"
- },
- {
- "label": "Large",
- "value": "spectrum--large"
- }
- ]
- },
- {
- "type": "boolean",
- "label": "Paginate",
- "key": "paginate",
- "defaultValue": true
- },
{
"section": true,
"name": "Table",
"settings": [
{
- "type": "number",
- "label": "Scroll Limit",
- "key": "rowCount",
- "defaultValue": 8
+ "type": "dataSource",
+ "label": "Data",
+ "key": "dataSource",
+ "required": true
},
{
"type": "columns",
@@ -4536,9 +4466,52 @@
"placeholder": "All columns",
"nested": true
},
+ {
+ "type": "field/sortable",
+ "label": "Sort By",
+ "key": "sortColumn"
+ },
+ {
+ "type": "select",
+ "label": "Sort Order",
+ "key": "sortOrder",
+ "options": [
+ "Ascending",
+ "Descending"
+ ],
+ "defaultValue": "Ascending"
+ },
+ {
+ "type": "select",
+ "label": "Size",
+ "key": "size",
+ "defaultValue": "spectrum--medium",
+ "options": [
+ {
+ "label": "Medium",
+ "value": "spectrum--medium"
+ },
+ {
+ "label": "Large",
+ "value": "spectrum--large"
+ }
+ ]
+ },
+ {
+ "type": "number",
+ "label": "Scroll Limit",
+ "key": "rowCount",
+ "defaultValue": 8
+ },
{
"type": "boolean",
- "label": "Quiet table variant",
+ "label": "Paginate",
+ "key": "paginate",
+ "defaultValue": true
+ },
+ {
+ "type": "boolean",
+ "label": "Quiet",
"key": "quiet"
},
{
@@ -4546,11 +4519,6 @@
"label": "Compact",
"key": "compact"
},
- {
- "type": "boolean",
- "label": "Show auto columns",
- "key": "showAutoColumns"
- },
{
"type": "boolean",
"label": "Allow row selection",
@@ -4558,58 +4526,100 @@
"info": "Row selection is only compatible with internal or SQL tables"
},
{
- "type": "boolean",
- "label": "Link table rows",
- "key": "linkRows"
+ "type": "filter",
+ "label": "Filtering",
+ "key": "filter"
},
{
- "type": "boolean",
- "label": "Open link in modal",
- "key": "linkPeek"
- },
- {
- "type": "url",
- "label": "Link screen",
- "key": "linkURL"
+ "type": "searchfield",
+ "label": "Search Fields",
+ "key": "searchColumns",
+ "placeholder": "Choose search fields",
+ "info": "Only the first 5 search fields will be used"
}
]
},
{
"section": true,
- "name": "Title button",
+ "name": "On row click",
+ "settings": [
+ {
+ "type": "radio",
+ "key": "clickBehaviour",
+ "sendEvents": true,
+ "defaultValue": "actions",
+ "info": "Details side panel is only compatible with internal or SQL tables",
+ "options": [
+ {
+ "label": "Run actions",
+ "value": "actions"
+ },
+ {
+ "label": "Show details side panel",
+ "value": "details"
+ }
+ ]
+ },
+ {
+ "type": "event",
+ "key": "onClick",
+ "nested": true,
+ "dependsOn": {
+ "setting": "clickBehaviour",
+ "value": "actions"
+ },
+ "context": [
+ {
+ "label": "Clicked row",
+ "key": "row"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "section": true,
+ "name": "Button",
"settings": [
{
"type": "boolean",
"key": "showTitleButton",
- "label": "Show link button",
+ "label": "Show button above table",
"defaultValue": false
},
- {
- "type": "boolean",
- "label": "Open link in modal",
- "key": "titleButtonPeek"
- },
{
"type": "text",
"key": "titleButtonText",
- "label": "Button text"
+ "label": "Text",
+ "defaultValue": "Create row",
+ "dependsOn": "showTitleButton"
},
{
- "type": "url",
- "label": "Button link",
- "key": "titleButtonURL"
- }
- ]
- },
- {
- "section": true,
- "name": "Advanced",
- "settings": [
+ "type": "radio",
+ "key": "titleButtonClickBehaviour",
+ "label": "On Click",
+ "dependsOn": "showTitleButton",
+ "defaultValue": "actions",
+ "info": "New row side panel is only compatible with internal or SQL tables",
+ "options": [
+ {
+ "label": "Run actions",
+ "value": "actions"
+ },
+ {
+ "label": "Show new row side panel",
+ "value": "new"
+ }
+ ]
+ },
{
- "type": "field",
- "label": "ID column for linking (appended to URL)",
- "key": "linkColumn",
- "placeholder": "Default"
+ "type": "event",
+ "key": "onClickTitleButton",
+ "nested": true,
+ "dependsOn": {
+ "setting": "titleButtonClickBehaviour",
+ "value": "actions"
+ }
}
]
}
@@ -5189,6 +5199,17 @@
}
]
},
+ "sidepanel": {
+ "name": "Side Panel",
+ "icon": "RailRight",
+ "hasChildren": true,
+ "illegalChildren": [
+ "section"
+ ],
+ "showEmptyState": false,
+ "draggable": false,
+ "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."
+ },
"rowexplorer": {
"block": true,
"name": "Row Explorer Block",
diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte
index c23a31b8b9..841aeb219a 100644
--- a/packages/client/src/components/BlockComponent.svelte
+++ b/packages/client/src/components/BlockComponent.svelte
@@ -8,6 +8,7 @@
export let props
export let styles
export let context
+ export let name
export let order = 0
export let containsSlot = false
@@ -26,7 +27,7 @@
_blockElementHasChildren: $$slots?.default ?? false,
_component: `@budibase/standard-components/${type}`,
_id: id,
- _instanceName: type[0].toUpperCase() + type.slice(1),
+ _instanceName: name || type[0].toUpperCase() + type.slice(1),
_styles: {
...styles,
normal: styles?.normal || {},
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index c44225cd71..e5fe100372 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -190,6 +190,7 @@
},
empty: emptyState,
selected,
+ inSelectedPath,
name,
editing,
type: instance._component,
diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte
index a317da1f0f..6582384569 100644
--- a/packages/client/src/components/app/Layout.svelte
+++ b/packages/client/src/components/app/Layout.svelte
@@ -1,7 +1,7 @@
+
+
+
+
+
+
diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte
index f75a71a3ee..9df81635db 100644
--- a/packages/client/src/components/app/blocks/TableBlock.svelte
+++ b/packages/client/src/components/app/blocks/TableBlock.svelte
@@ -1,5 +1,6 @@
{#if schemaLoaded}
@@ -129,7 +183,7 @@
+ {#if clickBehaviour === "details"}
+
+
+
+ {/if}
+ {#if showTitleButton && titleButtonClickBehaviour === "new"}
+
+
+
+ {/if}
{/if}
diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
index c4e9b0941c..da5345e6ad 100644
--- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
+++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte
@@ -43,6 +43,20 @@
{
"##eventHandlerType": "Close Screen Modal",
},
+ {
+ "##eventHandlerType": "Close Side Panel",
+ },
+ // Clear a create form once submitted
+ ...(actionType !== "Create"
+ ? []
+ : [
+ {
+ "##eventHandlerType": "Clear Form",
+ parameters: {
+ componentId: formId,
+ },
+ },
+ ]),
{
"##eventHandlerType": "Navigate To",
parameters: {
@@ -63,6 +77,9 @@
{
"##eventHandlerType": "Close Screen Modal",
},
+ {
+ "##eventHandlerType": "Close Side Panel",
+ },
{
"##eventHandlerType": "Navigate To",
parameters: {
diff --git a/packages/client/src/components/app/forms/Form.svelte b/packages/client/src/components/app/forms/Form.svelte
index 8eddc11fa5..8950661bd8 100644
--- a/packages/client/src/components/app/forms/Form.svelte
+++ b/packages/client/src/components/app/forms/Form.svelte
@@ -66,7 +66,7 @@
$: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString(
- JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
+ JSON.stringify(initialValues) + JSON.stringify(dataSource) + disabled
)
diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js
index b64e074115..70074790ac 100644
--- a/packages/client/src/components/app/index.js
+++ b/packages/client/src/components/app/index.js
@@ -35,10 +35,10 @@ export { default as tag } from "./Tag.svelte"
export { default as markdownviewer } from "./MarkdownViewer.svelte"
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
export { default as grid } from "./Grid.svelte"
+export { default as sidepanel } from "./SidePanel.svelte"
export * from "./charts"
export * from "./forms"
export * from "./table"
-
export * from "./blocks"
export * from "./dynamic-filter"
diff --git a/packages/client/src/components/app/table/Table.svelte b/packages/client/src/components/app/table/Table.svelte
index 901659233d..6e5cd6c5cf 100644
--- a/packages/client/src/components/app/table/Table.svelte
+++ b/packages/client/src/components/app/table/Table.svelte
@@ -7,20 +7,16 @@
export let dataProvider
export let columns
- export let showAutoColumns
export let rowCount
export let quiet
export let size
- export let linkRows
- export let linkURL
- export let linkColumn
- export let linkPeek
export let allowSelectRows
export let compact
+ export let onClick
const loading = getContext("loading")
const component = getContext("component")
- const { styleable, getAction, ActionTypes, routeStore, rowSelectionStore } =
+ const { styleable, getAction, ActionTypes, rowSelectionStore } =
getContext("sdk")
const customColumnKey = `custom-${Math.random()}`
const customRenderers = [
@@ -29,18 +25,19 @@
component: SlotRenderer,
},
]
+
let selectedRows = []
+
$: hasChildren = $component.children
$: data = dataProvider?.rows || []
$: fullSchema = dataProvider?.schema ?? {}
- $: fields = getFields(fullSchema, columns, showAutoColumns)
+ $: fields = getFields(fullSchema, columns, false)
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
$: setSorting = getAction(
dataProvider?.id,
ActionTypes.SetDataProviderSorting
)
$: table = dataProvider?.datasource?.type === "table"
-
$: {
rowSelectionStore.actions.updateSelection(
$component.id,
@@ -118,17 +115,10 @@
})
}
- const onClick = e => {
- if (!linkRows || !linkURL) {
- return
+ const handleClick = e => {
+ if (onClick) {
+ onClick({ row: e.detail })
}
- const col = linkColumn || "_id"
- const id = e.detail?.[col]
- if (!id) {
- return
- }
- const split = linkURL.split("/:")
- routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
}
onDestroy(() => {
@@ -153,7 +143,7 @@
disableSorting
autoSortColumns={!columns?.length}
on:sort={onSort}
- on:click={onClick}
+ on:click={handleClick}
>
diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte
index 1855166e0c..e93c42b863 100644
--- a/packages/client/src/components/preview/DNDHandler.svelte
+++ b/packages/client/src/components/preview/DNDHandler.svelte
@@ -36,8 +36,7 @@
// Util to get the inner DOM node by a component ID
const getDOMNode = id => {
- const component = document.getElementsByClassName(id)[0]
- return [...component.children][0]
+ return document.getElementsByClassName(`${id}-dom`)[0]
}
// Util to calculate the variance of a set of data
diff --git a/packages/client/src/components/preview/IndicatorSet.svelte b/packages/client/src/components/preview/IndicatorSet.svelte
index 51a60ce981..ff0c7d71d6 100644
--- a/packages/client/src/components/preview/IndicatorSet.svelte
+++ b/packages/client/src/components/preview/IndicatorSet.svelte
@@ -43,7 +43,8 @@
if (callbackCount >= observers.length) {
return
}
- nextIndicators[idx].visible = entries[0].isIntersecting
+ nextIndicators[idx].visible =
+ nextIndicators[idx].isSidePanel || entries[0].isIntersecting
if (++callbackCount === observers.length) {
indicators = nextIndicators
updating = false
@@ -91,8 +92,9 @@
// Extract valid children
// Sanity limit of 100 active indicators
- const children = Array.from(parents)
- .map(parent => parent?.children?.[0])
+ const children = Array.from(
+ document.getElementsByClassName(`${componentId}-dom`)
+ )
.filter(x => x != null)
.slice(0, 100)
@@ -121,6 +123,7 @@
width: elBounds.width + 4,
height: elBounds.height + 4,
visible: false,
+ isSidePanel: child.classList.contains("side-panel"),
})
})
}
diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js
index 1afeea0055..264cc85626 100644
--- a/packages/client/src/sdk.js
+++ b/packages/client/src/sdk.js
@@ -10,6 +10,8 @@ import {
componentStore,
currentRole,
environmentStore,
+ sidePanelStore,
+ dndIsDragging,
} from "stores"
import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable"
@@ -30,6 +32,8 @@ export default {
uploadStore,
componentStore,
environmentStore,
+ sidePanelStore,
+ dndIsDragging,
currentRole,
styleable,
linkable,
diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js
index 173f3ad4fe..7548d31817 100644
--- a/packages/client/src/stores/index.js
+++ b/packages/client/src/stores/index.js
@@ -24,6 +24,7 @@ export {
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
+export { sidePanelStore } from "./sidePanel.js"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"
diff --git a/packages/client/src/stores/sidePanel.js b/packages/client/src/stores/sidePanel.js
new file mode 100644
index 0000000000..327fcfae82
--- /dev/null
+++ b/packages/client/src/stores/sidePanel.js
@@ -0,0 +1,37 @@
+import { writable, derived } from "svelte/store"
+
+export const createSidePanelStore = () => {
+ const initialState = {
+ contentId: null,
+ }
+ const store = writable(initialState)
+ const derivedStore = derived(store, $store => {
+ return {
+ ...$store,
+ open: $store.contentId != null,
+ }
+ })
+
+ const open = id => {
+ store.update(state => {
+ state.contentId = id
+ return state
+ })
+ }
+ const close = () => {
+ store.update(state => {
+ state.contentId = null
+ return state
+ })
+ }
+
+ return {
+ subscribe: derivedStore.subscribe,
+ actions: {
+ open,
+ close,
+ },
+ }
+}
+
+export const sidePanelStore = createSidePanelStore()
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 6fcef37182..ffdea28c58 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -10,6 +10,7 @@ import {
dataSourceStore,
uploadStore,
rowSelectionStore,
+ sidePanelStore,
} from "stores"
import { API } from "api"
import { ActionTypes } from "constants"
@@ -312,6 +313,17 @@ const showNotificationHandler = action => {
notificationStore.actions[type]?.(message, autoDismiss)
}
+const OpenSidePanelHandler = action => {
+ const { id } = action.parameters
+ if (id) {
+ sidePanelStore.actions.open(id)
+ }
+}
+
+const CloseSidePanelHandler = () => {
+ sidePanelStore.actions.close()
+}
+
const handlerMap = {
["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler,
@@ -331,6 +343,8 @@ const handlerMap = {
["Export Data"]: exportDataHandler,
["Continue if / Stop if"]: continueIfHandler,
["Show Notification"]: showNotificationHandler,
+ ["Open Side Panel"]: OpenSidePanelHandler,
+ ["Close Side Panel"]: CloseSidePanelHandler,
}
const confirmTextMap = {
diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js
index 9ad17ceff0..4a6f1c0d7c 100644
--- a/packages/client/src/utils/styleable.js
+++ b/packages/client/src/utils/styleable.js
@@ -25,6 +25,8 @@ export const styleable = (node, styles = {}) => {
// Creates event listeners and applies initial styles
const setupStyles = (newStyles = {}) => {
+ node.classList.add(`${newStyles.id}-dom`)
+
let baseStyles = {}
if (newStyles.empty) {
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js
index 31007121f1..37e300e354 100644
--- a/packages/frontend-core/src/fetch/DataFetch.js
+++ b/packages/frontend-core/src/fetch/DataFetch.js
@@ -117,7 +117,7 @@ export default class DataFetch {
* Fetches a fresh set of data from the server, resetting pagination
*/
async getInitialData() {
- const { datasource, filter, sortColumn, paginate } = this.options
+ const { datasource, filter, paginate } = this.options
// Fetch datasource definition and determine feature flags
const definition = await this.getDefinition(datasource)
@@ -135,6 +135,17 @@ export default class DataFetch {
return
}
+ // If no sort order, default to descending
+ if (!this.options.sortOrder) {
+ this.options.sortOrder = "ascending"
+ }
+
+ // If no sort column, use the first field in the schema
+ if (!this.options.sortColumn) {
+ this.options.sortColumn = Object.keys(schema)[0]
+ }
+ const { sortColumn } = this.options
+
// Determine what sort type to use
let sortType = "string"
if (sortColumn) {
diff --git a/packages/server/src/migrations/functions/tableSettings.ts b/packages/server/src/migrations/functions/tableSettings.ts
new file mode 100644
index 0000000000..2db3df0d0f
--- /dev/null
+++ b/packages/server/src/migrations/functions/tableSettings.ts
@@ -0,0 +1,145 @@
+import { getScreenParams } from "../../db/utils"
+import { Screen } from "@budibase/types"
+import { makePropSafe as safe } from "@budibase/string-templates"
+/**
+ * Date:
+ * November 2022
+ *
+ * Description:
+ * Update table settings to use actions instead of links. We do not remove the
+ * legacy values here as we cannot guarantee that their apps are up-t-date.
+ * It is safe to simply save both the new and old structure in the definition.
+ *
+ * Migration 1:
+ * Legacy "linkRows", "linkURL", "linkPeek" and "linkColumn" settings on tables
+ * and table blocks are migrated into a "Navigate To" action under the new
+ * "onClick" setting.
+ *
+ * Migration 2:
+ * Legacy "titleButtonURL" and "titleButtonPeek" settings on table blocks are
+ * migrated into a "Navigate To" action under the new "onClickTitleButton"
+ * setting.
+ */
+export const run = async (appDb: any) => {
+ // Get all app screens
+ let screens: Screen[]
+ try {
+ screens = (
+ await appDb.allDocs(
+ getScreenParams(null, {
+ include_docs: true,
+ })
+ )
+ ).rows.map((row: any) => row.doc)
+ } catch (e) {
+ // sometimes the metadata document doesn't exist
+ // exit early instead of failing the migration
+ console.error("Error retrieving app metadata. Skipping", e)
+ return
+ }
+
+ // Recursively update any relevant components and mutate the screen docs
+ for (let screen of screens) {
+ const changed = migrateTableSettings(screen.props)
+
+ // Save screen if we updated it
+ if (changed) {
+ await appDb.put(screen)
+ console.log(
+ `Screen ${screen.routing?.route} contained table settings which were migrated`
+ )
+ }
+ }
+}
+
+// Recursively searches and mutates a screen doc to migrate table component
+// and table block settings
+const migrateTableSettings = (component: any) => {
+ let changed = false
+ if (!component) {
+ return changed
+ }
+
+ // Migration 1: migrate table row click settings
+ if (
+ component._component.endsWith("/table") ||
+ component._component.endsWith("/tableblock")
+ ) {
+ const { linkRows, linkURL, linkPeek, linkColumn, onClick } = component
+ if (linkRows && !onClick) {
+ const column = linkColumn || "_id"
+ const action = convertLinkSettingToAction(linkURL, !!linkPeek, column)
+ if (action) {
+ changed = true
+ component.onClick = action
+ if (component._component.endsWith("/tableblock")) {
+ component.clickBehaviour = "actions"
+ }
+ }
+ }
+ }
+
+ // Migration 2: migrate table block title button settings
+ if (component._component.endsWith("/tableblock")) {
+ const {
+ showTitleButton,
+ titleButtonURL,
+ titleButtonPeek,
+ onClickTitleButton,
+ } = component
+ if (showTitleButton && !onClickTitleButton) {
+ const action = convertLinkSettingToAction(
+ titleButtonURL,
+ !!titleButtonPeek
+ )
+ if (action) {
+ changed = true
+ component.onClickTitleButton = action
+ component.titleButtonClickBehaviour = "actions"
+ }
+ }
+ }
+
+ // Recurse down the tree as needed
+ component._children?.forEach((child: any) => {
+ const childChanged = migrateTableSettings(child)
+ changed = changed || childChanged
+ })
+ return changed
+}
+
+// Util ti convert the legacy settings into a navigation action structure
+const convertLinkSettingToAction = (
+ linkURL: string,
+ linkPeek: boolean,
+ linkColumn?: string
+) => {
+ // Sanity check we have a URL
+ if (!linkURL) {
+ return null
+ }
+
+ // Default URL to the old URL setting
+ let url = linkURL
+
+ // If we enriched the old URL with a column, update the url
+ if (linkColumn && linkURL.includes("/:")) {
+ // Convert old link URL setting, which is a screen URL, into a valid
+ // binding using the new clicked row binding
+ const split = linkURL.split("/:")
+ const col = linkColumn || "_id"
+ const binding = `{{ ${safe("eventContext")}.${safe("row")}.${safe(col)} }}`
+ url = `${split[0]}/${binding}`
+ }
+
+ // Create action structure
+ return [
+ {
+ "##eventHandlerType": "Navigate To",
+ parameters: {
+ url,
+ peek: linkPeek,
+ },
+ },
+ ]
+}
diff --git a/packages/server/src/migrations/functions/tests/tableSettings.spec.ts b/packages/server/src/migrations/functions/tests/tableSettings.spec.ts
new file mode 100644
index 0000000000..8d28a43322
--- /dev/null
+++ b/packages/server/src/migrations/functions/tests/tableSettings.spec.ts
@@ -0,0 +1,144 @@
+import { App, Screen } from "@budibase/types"
+
+import { db as dbCore } from "@budibase/backend-core"
+import TestConfig from "../../../tests/utilities/TestConfiguration"
+import { run as runMigration } from "../tableSettings"
+
+describe("run", () => {
+ const config = new TestConfig(false)
+ let app: App
+ let screen: Screen
+
+ beforeAll(async () => {
+ await config.init()
+ app = await config.createApp("testApp")
+ screen = await config.createScreen()
+ })
+
+ afterAll(config.end)
+
+ it("migrates table block row on click settings", async () => {
+ // Add legacy table block as first child
+ screen.props._children = [
+ {
+ _instanceName: "Table Block",
+ _styles: {},
+ _component: "@budibase/standard-components/tableblock",
+ _id: "foo",
+ linkRows: true,
+ linkURL: "/rows/:id",
+ linkPeek: true,
+ linkColumn: "name",
+ },
+ ]
+ await config.createScreen(screen)
+
+ // Run migration
+ screen = await dbCore.doWithDB(app.appId, async (db: any) => {
+ await runMigration(db)
+ return await db.get(screen._id)
+ })
+
+ // Verify new "onClick" setting
+ const onClick = screen.props._children?.[0].onClick
+ expect(onClick).toBeDefined()
+ expect(onClick.length).toBe(1)
+ expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
+ expect(onClick[0].parameters.url).toBe(
+ `/rows/{{ [eventContext].[row].[name] }}`
+ )
+ expect(onClick[0].parameters.peek).toBeTruthy()
+ })
+
+ it("migrates table row on click settings", async () => {
+ // Add legacy table block as first child
+ screen.props._children = [
+ {
+ _instanceName: "Table",
+ _styles: {},
+ _component: "@budibase/standard-components/table",
+ _id: "foo",
+ linkRows: true,
+ linkURL: "/rows/:id",
+ linkPeek: true,
+ linkColumn: "name",
+ },
+ ]
+ await config.createScreen(screen)
+
+ // Run migration
+ screen = await dbCore.doWithDB(app.appId, async (db: any) => {
+ await runMigration(db)
+ return await db.get(screen._id)
+ })
+
+ // Verify new "onClick" setting
+ const onClick = screen.props._children?.[0].onClick
+ expect(onClick).toBeDefined()
+ expect(onClick.length).toBe(1)
+ expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
+ expect(onClick[0].parameters.url).toBe(
+ `/rows/{{ [eventContext].[row].[name] }}`
+ )
+ expect(onClick[0].parameters.peek).toBeTruthy()
+ })
+
+ it("migrates table block title button settings", async () => {
+ // Add legacy table block as first child
+ screen.props._children = [
+ {
+ _instanceName: "Table Block",
+ _styles: {},
+ _component: "@budibase/standard-components/tableblock",
+ _id: "foo",
+ showTitleButton: true,
+ titleButtonURL: "/url",
+ titleButtonPeek: true,
+ },
+ ]
+ await config.createScreen(screen)
+
+ // Run migration
+ screen = await dbCore.doWithDB(app.appId, async (db: any) => {
+ await runMigration(db)
+ return await db.get(screen._id)
+ })
+
+ // Verify new "onClickTitleButton" setting
+ const onClick = screen.props._children?.[0].onClickTitleButton
+ expect(onClick).toBeDefined()
+ expect(onClick.length).toBe(1)
+ expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
+ expect(onClick[0].parameters.url).toBe("/url")
+ expect(onClick[0].parameters.peek).toBeTruthy()
+ })
+
+ it("ignores components that have already been migrated", async () => {
+ // Add legacy table block as first child
+ screen.props._children = [
+ {
+ _instanceName: "Table Block",
+ _styles: {},
+ _component: "@budibase/standard-components/tableblock",
+ _id: "foo",
+ linkRows: true,
+ linkURL: "/rows/:id",
+ linkPeek: true,
+ linkColumn: "name",
+ onClick: "foo",
+ },
+ ]
+ const initialDefinition = JSON.stringify(screen.props._children?.[0])
+ await config.createScreen(screen)
+
+ // Run migration
+ screen = await dbCore.doWithDB(app.appId, async (db: any) => {
+ await runMigration(db)
+ return await db.get(screen._id)
+ })
+
+ // Verify new "onClick" setting
+ const newDefinition = JSON.stringify(screen.props._children?.[0])
+ expect(initialDefinition).toEqual(newDefinition)
+ })
+})
diff --git a/packages/server/src/migrations/index.ts b/packages/server/src/migrations/index.ts
index 6ef6a3fda6..a66d793142 100644
--- a/packages/server/src/migrations/index.ts
+++ b/packages/server/src/migrations/index.ts
@@ -12,6 +12,7 @@ import env from "../environment"
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
import * as syncQuotas from "./functions/syncQuotas"
import * as appUrls from "./functions/appUrls"
+import * as tableSettings from "./functions/tableSettings"
import * as backfill from "./functions/backfill"
/**
* Populate the migration function and additional configuration from
@@ -73,6 +74,14 @@ export const buildMigrations = () => {
})
break
}
+ case MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS: {
+ serverMigrations.push({
+ ...definition,
+ appOpts: { dev: true },
+ fn: tableSettings.run,
+ })
+ break
+ }
}
}
diff --git a/packages/types/src/documents/app/component.ts b/packages/types/src/documents/app/component.ts
new file mode 100644
index 0000000000..654b2c87cb
--- /dev/null
+++ b/packages/types/src/documents/app/component.ts
@@ -0,0 +1,9 @@
+import { Document } from "../document"
+
+export interface Component extends Document {
+ _instanceName: string
+ _styles: { [key: string]: any }
+ _component: string
+ _children?: Component[]
+ [key: string]: any
+}
diff --git a/packages/types/src/documents/app/index.ts b/packages/types/src/documents/app/index.ts
index 8544617c30..b81c9e36ac 100644
--- a/packages/types/src/documents/app/index.ts
+++ b/packages/types/src/documents/app/index.ts
@@ -13,3 +13,4 @@ export * from "./user"
export * from "./backup"
export * from "./webhook"
export * from "./links"
+export * from "./component"
diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts
index a3778d140f..5f79620ae4 100644
--- a/packages/types/src/documents/app/screen.ts
+++ b/packages/types/src/documents/app/screen.ts
@@ -1,10 +1,7 @@
import { Document } from "../document"
+import { Component } from "./component"
-export interface ScreenProps extends Document {
- _instanceName: string
- _styles: { [key: string]: any }
- _component: string
- _children: ScreenProps[]
+export interface ScreenProps extends Component {
size?: string
gap?: string
direction?: string
diff --git a/packages/types/src/sdk/migrations.ts b/packages/types/src/sdk/migrations.ts
index 4f5315d003..4667ed0c8f 100644
--- a/packages/types/src/sdk/migrations.ts
+++ b/packages/types/src/sdk/migrations.ts
@@ -44,6 +44,7 @@ export enum MigrationName {
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
+ TABLE_SETTINGS_LINKS_TO_ACTIONS = "table_settings_links_to_actions",
// increment this number to re-activate this migration
SYNC_QUOTAS = "sync_quotas_1",
}
diff --git a/yarn.lock b/yarn.lock
index a0c22d92bf..60cb598a60 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1011,45 +1011,45 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
-"@typescript-eslint/parser@4.28.0":
- version "4.28.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
- integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==
+"@typescript-eslint/parser@5.45.0":
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e"
+ integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==
dependencies:
- "@typescript-eslint/scope-manager" "4.28.0"
- "@typescript-eslint/types" "4.28.0"
- "@typescript-eslint/typescript-estree" "4.28.0"
- debug "^4.3.1"
+ "@typescript-eslint/scope-manager" "5.45.0"
+ "@typescript-eslint/types" "5.45.0"
+ "@typescript-eslint/typescript-estree" "5.45.0"
+ debug "^4.3.4"
-"@typescript-eslint/scope-manager@4.28.0":
- version "4.28.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce"
- integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==
+"@typescript-eslint/scope-manager@5.45.0":
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96"
+ integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==
dependencies:
- "@typescript-eslint/types" "4.28.0"
- "@typescript-eslint/visitor-keys" "4.28.0"
-
-"@typescript-eslint/types@4.28.0":
- version "4.28.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
- integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
+ "@typescript-eslint/types" "5.45.0"
+ "@typescript-eslint/visitor-keys" "5.45.0"
"@typescript-eslint/types@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
-"@typescript-eslint/typescript-estree@4.28.0":
- version "4.28.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
- integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==
+"@typescript-eslint/types@5.45.0":
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5"
+ integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==
+
+"@typescript-eslint/typescript-estree@5.45.0":
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d"
+ integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==
dependencies:
- "@typescript-eslint/types" "4.28.0"
- "@typescript-eslint/visitor-keys" "4.28.0"
- debug "^4.3.1"
- globby "^11.0.3"
- is-glob "^4.0.1"
- semver "^7.3.5"
+ "@typescript-eslint/types" "5.45.0"
+ "@typescript-eslint/visitor-keys" "5.45.0"
+ debug "^4.3.4"
+ globby "^11.1.0"
+ is-glob "^4.0.3"
+ semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@^4.33.0":
@@ -1065,14 +1065,6 @@
semver "^7.3.5"
tsutils "^3.21.0"
-"@typescript-eslint/visitor-keys@4.28.0":
- version "4.28.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
- integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==
- dependencies:
- "@typescript-eslint/types" "4.28.0"
- eslint-visitor-keys "^2.0.0"
-
"@typescript-eslint/visitor-keys@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd"
@@ -1081,6 +1073,14 @@
"@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0"
+"@typescript-eslint/visitor-keys@5.45.0":
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
+ integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==
+ dependencies:
+ "@typescript-eslint/types" "5.45.0"
+ eslint-visitor-keys "^3.3.0"
+
JSONStream@^1.0.4, JSONStream@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -2015,7 +2015,7 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"
-debug@^4.0.0, debug@^4.3.1, debug@^4.3.3:
+debug@^4.0.0, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2446,6 +2446,11 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+eslint-visitor-keys@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
+ integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
+
eslint@^7.28.0:
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
@@ -3081,7 +3086,7 @@ globals@^13.6.0, globals@^13.9.0:
dependencies:
type-fest "^0.20.2"
-globby@^11.0.3:
+globby@^11.0.3, globby@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@@ -3606,7 +3611,7 @@ is-glob@^3.1.0:
dependencies:
is-extglob "^2.1.0"
-is-glob@^4.0.0, is-glob@^4.0.1:
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -5843,6 +5848,13 @@ semver@^7.3.4:
dependencies:
lru-cache "^6.0.0"
+semver@^7.3.7:
+ version "7.3.8"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
+ integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+ dependencies:
+ lru-cache "^6.0.0"
+
semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -6559,10 +6571,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
-typescript@4.5.5:
- version "4.5.5"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
- integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
+typescript@4.7.3:
+ version "4.7.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
+ integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7:
version "3.9.10"