diff --git a/lerna.json b/lerna.json
index 954249c20a..843addc63c 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "2.29.21",
+ "version": "2.29.22",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts
index 4936e4da68..a4b924bf54 100644
--- a/packages/backend-core/src/sql/sql.ts
+++ b/packages/backend-core/src/sql/sql.ts
@@ -42,27 +42,28 @@ const envLimit = environment.SQL_MAX_ROWS
: null
const BASE_LIMIT = envLimit || 5000
-function likeKey(client: string | string[], key: string): string {
- let start: string, end: string
+// Takes a string like foo and returns a quoted string like [foo] for SQL Server
+// and "foo" for Postgres.
+function quote(client: SqlClient, str: string): string {
switch (client) {
- case SqlClient.MY_SQL:
- start = end = "`"
- break
case SqlClient.SQL_LITE:
case SqlClient.ORACLE:
case SqlClient.POSTGRES:
- start = end = '"'
- break
+ return `"${str}"`
case SqlClient.MS_SQL:
- start = "["
- end = "]"
- break
- default:
- throw new Error("Unknown client generating like key")
+ return `[${str}]`
+ case SqlClient.MY_SQL:
+ return `\`${str}\``
}
- const parts = key.split(".")
- key = parts.map(part => `${start}${part}${end}`).join(".")
+}
+
+// Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
+// for SQL Server and `a`.`b`.`c` for MySQL.
+function quotedIdentifier(client: SqlClient, key: string): string {
return key
+ .split(".")
+ .map(part => quote(client, part))
+ .join(".")
}
function parse(input: any) {
@@ -113,34 +114,81 @@ function generateSelectStatement(
knex: Knex
): (string | Knex.Raw)[] | "*" {
const { resource, meta } = json
+ const client = knex.client.config.client as SqlClient
if (!resource || !resource.fields || resource.fields.length === 0) {
return "*"
}
- const schema = meta?.table?.schema
+ const schema = meta.table.schema
return resource.fields.map(field => {
- const fieldNames = field.split(/\./g)
- const tableName = fieldNames[0]
- const columnName = fieldNames[1]
- const columnSchema = schema?.[columnName]
- if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) {
- const externalType = schema[columnName].externalType
- if (externalType?.includes("money")) {
- return knex.raw(
- `"${tableName}"."${columnName}"::money::numeric as "${field}"`
- )
- }
+ const parts = field.split(/\./g)
+ let table: string | undefined = undefined
+ let column: string | undefined = undefined
+
+ // Just a column name, e.g.: "column"
+ if (parts.length === 1) {
+ column = parts[0]
}
+
+ // A table name and a column name, e.g.: "table.column"
+ if (parts.length === 2) {
+ table = parts[0]
+ column = parts[1]
+ }
+
+ // A link doc, e.g.: "table.doc1.fieldName"
+ if (parts.length > 2) {
+ table = parts[0]
+ column = parts.slice(1).join(".")
+ }
+
+ if (!column) {
+ throw new Error(`Invalid field name: ${field}`)
+ }
+
+ const columnSchema = schema[column]
+
if (
- knex.client.config.client === SqlClient.MS_SQL &&
+ client === SqlClient.POSTGRES &&
+ columnSchema?.externalType?.includes("money")
+ ) {
+ return knex.raw(
+ `${quotedIdentifier(
+ client,
+ [table, column].join(".")
+ )}::money::numeric as ${quote(client, field)}`
+ )
+ }
+
+ if (
+ client === SqlClient.MS_SQL &&
columnSchema?.type === FieldType.DATETIME &&
columnSchema.timeOnly
) {
- // Time gets returned as timestamp from mssql, not matching the expected HH:mm format
+ // Time gets returned as timestamp from mssql, not matching the expected
+ // HH:mm format
return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
}
- return `${field} as ${field}`
+
+ // There's at least two edge cases being handled in the expression below.
+ // 1. The column name could start/end with a space, and in that case we
+ // want to preseve that space.
+ // 2. Almost all column names are specified in the form table.column, except
+ // in the case of relationships, where it's table.doc1.column. In that
+ // case, we want to split it into `table`.`doc1.column` for reasons that
+ // aren't actually clear to me, but `table`.`doc1` breaks things with the
+ // sample data tests.
+ if (table) {
+ return knex.raw(
+ `${quote(client, table)}.${quote(client, column)} as ${quote(
+ client,
+ field
+ )}`
+ )
+ } else {
+ return knex.raw(`${quote(client, field)} as ${quote(client, field)}`)
+ }
})
}
@@ -173,9 +221,9 @@ function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
}
class InternalBuilder {
- private readonly client: string
+ private readonly client: SqlClient
- constructor(client: string) {
+ constructor(client: SqlClient) {
this.client = client
}
@@ -250,9 +298,10 @@ class InternalBuilder {
} else {
const rawFnc = `${fnc}Raw`
// @ts-ignore
- query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
- `%${value.toLowerCase()}%`,
- ])
+ query = query[rawFnc](
+ `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
+ [`%${value.toLowerCase()}%`]
+ )
}
}
@@ -302,7 +351,10 @@ class InternalBuilder {
}
statement +=
(statement ? andOr : "") +
- `COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?`
+ `COALESCE(LOWER(${quotedIdentifier(
+ this.client,
+ key
+ )}), '') LIKE ?`
}
if (statement === "") {
@@ -336,9 +388,10 @@ class InternalBuilder {
} else {
const rawFnc = `${fnc}Raw`
// @ts-ignore
- query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
- `${value.toLowerCase()}%`,
- ])
+ query = query[rawFnc](
+ `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
+ [`${value.toLowerCase()}%`]
+ )
}
})
}
@@ -376,12 +429,15 @@ class InternalBuilder {
const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) {
query = query[fnc](
- `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`,
+ `CASE WHEN ${quotedIdentifier(
+ this.client,
+ key
+ )} = ? THEN 1 ELSE 0 END = 1`,
[value]
)
} else {
query = query[fnc](
- `COALESCE(${likeKey(this.client, key)} = ?, FALSE)`,
+ `COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`,
[value]
)
}
@@ -392,12 +448,15 @@ class InternalBuilder {
const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) {
query = query[fnc](
- `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`,
+ `CASE WHEN ${quotedIdentifier(
+ this.client,
+ key
+ )} = ? THEN 1 ELSE 0 END = 0`,
[value]
)
} else {
query = query[fnc](
- `COALESCE(${likeKey(this.client, key)} != ?, TRUE)`,
+ `COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`,
[value]
)
}
@@ -769,7 +828,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
private readonly limit: number
// pass through client to get flavour of SQL
- constructor(client: string, limit: number = BASE_LIMIT) {
+ constructor(client: SqlClient, limit: number = BASE_LIMIT) {
super(client)
this.limit = limit
}
diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts
index bdc8a3dd69..02acc8af85 100644
--- a/packages/backend-core/src/sql/sqlTable.ts
+++ b/packages/backend-core/src/sql/sqlTable.ts
@@ -195,14 +195,14 @@ function buildDeleteTable(knex: SchemaBuilder, table: Table): SchemaBuilder {
}
class SqlTableQueryBuilder {
- private readonly sqlClient: string
+ private readonly sqlClient: SqlClient
// pass through client to get flavour of SQL
- constructor(client: string) {
+ constructor(client: SqlClient) {
this.sqlClient = client
}
- getSqlClient(): string {
+ getSqlClient(): SqlClient {
return this.sqlClient
}
diff --git a/packages/backend-core/src/sql/utils.ts b/packages/backend-core/src/sql/utils.ts
index e73c6ac445..67b5d2081b 100644
--- a/packages/backend-core/src/sql/utils.ts
+++ b/packages/backend-core/src/sql/utils.ts
@@ -8,6 +8,7 @@ const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g
const ENCODED_SPACE = encodeURIComponent(" ")
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
+const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
export function isExternalTableID(tableId: string) {
return tableId.startsWith(DocumentType.DATASOURCE + SEPARATOR)
@@ -147,6 +148,10 @@ export function isValidFilter(value: any) {
return value != null && value !== ""
}
+export function isValidTime(value: string) {
+ return TIME_REGEX.test(value)
+}
+
export function sqlLog(client: string, query: string, values?: any[]) {
if (!environment.SQL_LOGGING_ENABLE) {
return
diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte
index e293bd408b..6ae1f4ca67 100644
--- a/packages/bbui/src/Icon/Icon.svelte
+++ b/packages/bbui/src/Icon/Icon.svelte
@@ -29,6 +29,7 @@
>
import "@spectrum-css/progressbar/dist/index-vars.css"
- import { tweened } from "svelte/motion"
- import { cubicOut } from "svelte/easing"
export let value = false
- export let easing = cubicOut
export let duration = 1000
export let width = false
export let sideLabel = false
export let hidePercentage = true
export let color // red, green, default = blue
-
export let size = "M"
-
- const progress = tweened(0, {
- duration: duration,
- easing: easing,
- })
-
- $: if (value || value === 0) $progress = value
- {Math.round($progress)}%
+ {Math.round(value)}%
{/if}
@@ -51,7 +40,7 @@
class="spectrum-ProgressBar-fill"
class:color-green={color === "green"}
class:color-red={color === "red"}
- style={value || value === 0 ? `width: ${$progress}%` : ""}
+ style="width: {value}%; --duration: {duration}ms;"
/>
@@ -64,4 +53,7 @@
.color-red {
background: #dd2019;
}
+ .spectrum-ProgressBar-fill {
+ transition: width var(--duration) ease-out;
+ }
diff --git a/packages/bbui/src/bbui.css b/packages/bbui/src/bbui.css
index 9b5d89f61c..d60a3e18ea 100644
--- a/packages/bbui/src/bbui.css
+++ b/packages/bbui/src/bbui.css
@@ -3,13 +3,13 @@
--ink: #000000;
/* Brand colours */
- --bb-coral: #FF4E4E;
- --bb-coral-light: #F97777;
- --bb-indigo: #6E56FF;
- --bb-indigo-light: #9F8FFF;
- --bb-lime: #ECFFB5;
+ --bb-coral: #ff4e4e;
+ --bb-coral-light: #f97777;
+ --bb-indigo: #6e56ff;
+ --bb-indigo-light: #9f8fff;
+ --bb-lime: #ecffb5;
--bb-forest-green: #053835;
- --bb-beige: #F6EFEA;
+ --bb-beige: #f6efea;
--grey-1: #fafafa;
--grey-2: #f5f5f5;
@@ -49,10 +49,10 @@
--rounded-medium: 8px;
--rounded-large: 16px;
- --font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
- "Helvetica Neue", Arial, "Noto Sans", sans-serif;
- --font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
- "Helvetica Neue", Arial, "Noto Sans", sans-serif;
+ --font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI,
+ "Inter", "Helvetica Neue", Arial, "Noto Sans", sans-serif;
+ --font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI,
+ "Inter", "Helvetica Neue", Arial, "Noto Sans", sans-serif;
--font-serif: "Georgia", Cambria, Times New Roman, Times, serif;
--font-mono: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
@@ -111,7 +111,7 @@ a {
/* Custom theme additions */
.spectrum--darkest {
--drop-shadow: rgba(0, 0, 0, 0.6);
- --spectrum-global-color-blue-100: rgb(28, 33, 43);
+ --spectrum-global-color-blue-100: rgb(30, 36, 50);
}
.spectrum--dark {
--drop-shadow: rgba(0, 0, 0, 0.3);
diff --git a/packages/builder/src/components/ContextMenu.svelte b/packages/builder/src/components/ContextMenu.svelte
new file mode 100644
index 0000000000..37aacf06ed
--- /dev/null
+++ b/packages/builder/src/components/ContextMenu.svelte
@@ -0,0 +1,62 @@
+
+
+
+{#key $contextMenuStore.position}
+
+{/key}
+
+
+
+ {#each $contextMenuStore.items as item}
+ {#if item.visible}
+ handleItemClick(item.callback)}
+ disabled={item.disabled}
+ >
+ {item.name}
+
+ {/if}
+ {/each}
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
index ccb1369c57..9899c454fc 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
@@ -17,12 +17,12 @@
export let blockIdx
export let lastStep
+ export let modal
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
let triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction
- let actionVal
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
let lockedFeatures = [
ActionStepID.COLLECT,
@@ -91,19 +91,17 @@
return acc
}, {})
- const selectAction = action => {
- actionVal = action
+ const selectAction = async action => {
selectedAction = action.name
- }
- async function addBlockToAutomation() {
try {
const newBlock = automationStore.actions.constructBlock(
"ACTION",
- actionVal.stepId,
- actionVal
+ action.stepId,
+ action
)
await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
+ modal.hide()
} catch (error) {
notifications.error("Error saving automation")
}
@@ -114,10 +112,10 @@
Apps
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
index 48b630cb62..811909845a 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
@@ -206,7 +206,7 @@
{/if}
-
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
index 7d223299c7..2a553f8b48 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
@@ -81,7 +81,7 @@
// Check the schema to see if required fields have been entered
$: isError =
!isTriggerValid(trigger) ||
- !trigger.schema.outputs.required.every(
+ !trigger.schema.outputs.required?.every(
required => $memoTestData?.[required] || required !== "row"
)
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
deleted file mode 100644
index 9d946fe55d..0000000000
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
- {#each $automationStore.automations.sort(aut => aut.name) as automation}
- selectAutomation(automation._id)}
- selectedBy={$userSelectedResourceMap[automation._id]}
- >
-
-
- {/each}
-
-
-
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
new file mode 100644
index 0000000000..31f86f5d78
--- /dev/null
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
@@ -0,0 +1,123 @@
+
+
+ automationStore.actions.select(automation._id)}
+ selectedBy={$userSelectedResourceMap[automation._id]}
+ disabled={automation.disabled}
+>
+
+
+
+
+
+
+ Are you sure you wish to delete the automation
+ {automation.name}?
+ This action cannot be undone.
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
index b5fe6d03fd..c3ef3157bd 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
@@ -3,20 +3,13 @@
import { Modal, notifications, Layout } from "@budibase/bbui"
import NavHeader from "components/common/NavHeader.svelte"
import { onMount } from "svelte"
- import {
- automationStore,
- selectedAutomation,
- userSelectedResourceMap,
- } from "stores/builder"
- import NavItem from "components/common/NavItem.svelte"
- import EditAutomationPopover from "./EditAutomationPopover.svelte"
+ import { automationStore } from "stores/builder"
+ import AutomationNavItem from "./AutomationNavItem.svelte"
export let modal
export let webhookModal
let searchString
- $: selectedAutomationId = $selectedAutomation?._id
-
$: filteredAutomations = $automationStore.automations
.filter(automation => {
return (
@@ -49,10 +42,6 @@
notifications.error("Error getting automations list")
}
})
-
- function selectAutomation(id) {
- automationStore.actions.select(id)
- }
@@ -71,17 +60,7 @@
{triggerGroup?.name}
{#each triggerGroup.entries as automation}
- selectAutomation(automation._id)}
- selectedBy={$userSelectedResourceMap[automation._id]}
- disabled={automation.disabled}
- >
-
-
+
{/each}
{/each}
diff --git a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte
deleted file mode 100644
index 9465374ae2..0000000000
--- a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
- Duplicate
- Edit
-
- {automation.disabled ? "Activate" : "Pause"}
-
- Delete
-
-
-
- Are you sure you wish to delete the automation
- {automation.name}?
- This action cannot be undone.
-
-
-
-
diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
index 32e38803b3..0d4361954f 100644
--- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
@@ -79,18 +79,18 @@
table = $tables.list.find(table => table._id === tableId)
+ schemaFields = Object.entries(table?.schema ?? {})
+ .filter(entry => {
+ const [, field] = entry
+ return field.type !== "formula" && !field.autocolumn
+ })
+ .sort(([nameA], [nameB]) => {
+ return nameA < nameB ? -1 : 1
+ })
+
if (table) {
editableRow["tableId"] = tableId
- schemaFields = Object.entries(table?.schema ?? {})
- .filter(entry => {
- const [, field] = entry
- return field.type !== "formula" && !field.autocolumn
- })
- .sort(([nameA], [nameB]) => {
- return nameA < nameB ? -1 : 1
- })
-
// Parse out any data not in the schema.
for (const column in editableFields) {
if (!Object.hasOwn(table?.schema, column)) {
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte
new file mode 100644
index 0000000000..1ba32838ab
--- /dev/null
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DatasourceNavItem.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ {#if datasource._id !== BUDIBASE_INTERNAL_DB_ID}
+
+ {/if}
+
+
+
+
+
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/popovers/EditDatasourcePopover.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DeleteConfirmationModal.svelte
similarity index 50%
rename from packages/builder/src/components/backend/DatasourceNavigator/popovers/EditDatasourcePopover.svelte
rename to packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DeleteConfirmationModal.svelte
index 79efd276f8..13380c2700 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/popovers/EditDatasourcePopover.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavItem/DeleteConfirmationModal.svelte
@@ -1,15 +1,16 @@
-
-
-
-
- {#if datasource.type !== BUDIBASE_DATASOURCE_TYPE}
- Edit
- {/if}
- Delete
-
-
{datasource.name}?
This action cannot be undone.
-
-
-
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
index 23081c92c4..e0745c15a1 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
@@ -1,7 +1,6 @@
+
+ goto(`./query/${query._id}`)}
+ selectedBy={$userSelectedResourceMap[query._id]}
+>
+
+
+
+
+ Are you sure you wish to delete this query? This action cannot be undone.
+
+
+
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/popovers/EditQueryPopover.svelte b/packages/builder/src/components/backend/DatasourceNavigator/popovers/EditQueryPopover.svelte
deleted file mode 100644
index d77c5cc8c0..0000000000
--- a/packages/builder/src/components/backend/DatasourceNavigator/popovers/EditQueryPopover.svelte
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
- Delete
- Duplicate
-
-
-
- Are you sure you wish to delete this query? This action cannot be undone.
-
-
-
diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte
similarity index 54%
rename from packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte
rename to packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte
index f2c726c8bf..21a763be08 100644
--- a/packages/builder/src/components/backend/TableNavigator/popovers/EditTablePopover.svelte
+++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte
@@ -1,35 +1,15 @@
-
-
-
-
- {#if !externalTable}
- Edit
- {/if}
- Delete
-
-
-
-
-
-
-
diff --git a/packages/builder/src/components/start/AppContextMenuModals.svelte b/packages/builder/src/components/start/AppContextMenuModals.svelte
new file mode 100644
index 0000000000..f4921c4312
--- /dev/null
+++ b/packages/builder/src/components/start/AppContextMenuModals.svelte
@@ -0,0 +1,56 @@
+
+
+ {
+ await licensing.init()
+ }}
+/>
+
+
+
+
+
+
+ {
+ await licensing.init()
+ }}
+ />
+
diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte
index 6dd4030f3a..2f0b30ded9 100644
--- a/packages/builder/src/components/start/AppRow.svelte
+++ b/packages/builder/src/components/start/AppRow.svelte
@@ -5,14 +5,17 @@
import { goto } from "@roxi/routify"
import { UserAvatars } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core"
- import AppRowContext from "./AppRowContext.svelte"
+ import AppContextMenuModals from "./AppContextMenuModals.svelte"
+ import getAppContextMenuItems from "./getAppContextMenuItems.js"
import FavouriteAppButton from "pages/builder/portal/apps/FavouriteAppButton.svelte"
+ import { contextMenuStore } from "stores/builder"
export let app
export let lockedAction
- let actionsOpen = false
+ let appContextMenuModals
+ $: contextMenuOpen = `${app.appId}-index` === $contextMenuStore.id
$: editing = app.sessions?.length
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
$: unclickable = !isBuilder && !app.deployed
@@ -40,16 +43,35 @@
window.open(`/app${app.url}`, "_blank")
}
}
+
+ const openContextMenu = e => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ const items = getAppContextMenuItems({
+ app,
+ onDuplicate: appContextMenuModals?.showDuplicateModal,
+ onExportDev: appContextMenuModals?.showExportDevModal,
+ onExportProd: appContextMenuModals?.showExportProdModal,
+ onDelete: appContextMenuModals?.showDeleteModal,
+ })
+
+ contextMenuStore.open(`${app.appId}-index`, items, {
+ x: e.clientX,
+ y: e.clientY,
+ })
+ }
@@ -89,14 +111,11 @@
-
{
- actionsOpen = true
- }}
- on:close={() => {
- actionsOpen = false
- }}
+
{:else}
@@ -109,6 +128,7 @@
+
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
index 0219dc304d..997fac6f10 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ComponentTree.svelte
@@ -1,7 +1,6 @@
@@ -93,6 +106,7 @@
{#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, openNodes)}
openContextMenu(e, component, opened)}
on:click|stopPropagation={() => {
componentStore.select(component._id)
}}
@@ -107,7 +121,8 @@
on:dragover={dragover(component, index)}
on:iconClick={() => handleIconClick(component._id)}
on:drop={onDrop}
- hovering={$hoverStore.componentId === component._id}
+ hovering={$hoverStore.componentId === component._id ||
+ component._id === $contextMenuStore.id}
on:mouseenter={() => hover(component._id)}
on:mouseleave={() => hover(null)}
text={getComponentText(component)}
@@ -120,7 +135,12 @@
highlighted={isChildOfSelectedComponent(component)}
selectedBy={$userSelectedResourceMap[component._id]}
>
-
+ openContextMenu(e, component, opened)}
+ />
{#if opened}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ScreenslotDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ScreenslotDropdownMenu.svelte
deleted file mode 100644
index ddb1630644..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/ScreenslotDropdownMenu.svelte
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-{#if showMenu}
-
-
-
-
- storeComponentForCopy(false)}
- >
- Copy
-
- pasteComponent("inside")}
- disabled={noPaste}
- >
- Paste
-
-
-{/if}
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getComponentContextMenuItems.js b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getComponentContextMenuItems.js
new file mode 100644
index 0000000000..f2dfb73a68
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getComponentContextMenuItems.js
@@ -0,0 +1,123 @@
+import { get } from "svelte/store"
+import { componentStore } from "stores/builder"
+
+const getContextMenuItems = (component, componentCollapsed) => {
+ const definition = componentStore.getDefinition(component?._component)
+ const noPaste = !get(componentStore).componentToPaste
+ const isBlock = definition?.block === true
+ const canEject = !(definition?.ejectable === false)
+ const hasChildren = component?._children?.length
+
+ const keyboardEvent = (key, ctrlKey = false) => {
+ document.dispatchEvent(
+ new CustomEvent("component-menu", {
+ detail: {
+ key,
+ ctrlKey,
+ id: component?._id,
+ },
+ })
+ )
+ }
+
+ return [
+ {
+ icon: "Delete",
+ name: "Delete",
+ keyBind: "!BackAndroid",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("Delete"),
+ },
+ {
+ icon: "ChevronUp",
+ name: "Move up",
+ keyBind: "Ctrl+!ArrowUp",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("ArrowUp", true),
+ },
+ {
+ icon: "ChevronDown",
+ name: "Move down",
+ keyBind: "Ctrl+!ArrowDown",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("ArrowDown", true),
+ },
+ {
+ icon: "Duplicate",
+ name: "Duplicate",
+ keyBind: "Ctrl+D",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("d", true),
+ },
+ {
+ icon: "Cut",
+ name: "Cut",
+ keyBind: "Ctrl+X",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("x", true),
+ },
+ {
+ icon: "Copy",
+ name: "Copy",
+ keyBind: "Ctrl+C",
+ visible: true,
+ disabled: false,
+ callback: () => keyboardEvent("c", true),
+ },
+ {
+ icon: "LayersSendToBack",
+ name: "Paste",
+ keyBind: "Ctrl+V",
+ visible: true,
+ disabled: noPaste,
+ callback: () => keyboardEvent("v", true),
+ },
+ {
+ icon: "Export",
+ name: "Eject block",
+ keyBind: "Ctrl+E",
+ visible: isBlock && canEject,
+ disabled: false,
+ callback: () => keyboardEvent("e", true),
+ },
+ {
+ icon: "TreeExpand",
+ name: "Expand",
+ keyBind: "!ArrowRight",
+ visible: hasChildren,
+ disabled: !componentCollapsed,
+ callback: () => keyboardEvent("ArrowRight", false),
+ },
+ {
+ icon: "TreeExpandAll",
+ name: "Expand All",
+ keyBind: "Ctrl+!ArrowRight",
+ visible: hasChildren,
+ disabled: !componentCollapsed,
+ callback: () => keyboardEvent("ArrowRight", true),
+ },
+ {
+ icon: "TreeCollapse",
+ name: "Collapse",
+ keyBind: "!ArrowLeft",
+ visible: hasChildren,
+ disabled: componentCollapsed,
+ callback: () => keyboardEvent("ArrowLeft", false),
+ },
+ {
+ icon: "TreeCollapseAll",
+ name: "Collapse All",
+ keyBind: "Ctrl+!ArrowLeft",
+ visible: hasChildren,
+ disabled: componentCollapsed,
+ callback: () => keyboardEvent("ArrowLeft", true),
+ },
+ ]
+}
+
+export default getContextMenuItems
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getScreenContextMenuItems.js b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getScreenContextMenuItems.js
new file mode 100644
index 0000000000..25f2e908e6
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/getScreenContextMenuItems.js
@@ -0,0 +1,40 @@
+import { get } from "svelte/store"
+import { componentStore } from "stores/builder"
+import { notifications } from "@budibase/bbui"
+
+const getContextMenuItems = (component, showCopy) => {
+ const noPaste = !get(componentStore).componentToPaste
+
+ const storeComponentForCopy = (cut = false) => {
+ componentStore.copy(component, cut)
+ }
+
+ const pasteComponent = mode => {
+ try {
+ componentStore.paste(component, mode)
+ } catch (error) {
+ notifications.error("Error saving component")
+ }
+ }
+
+ return [
+ {
+ icon: "Copy",
+ name: "Copy",
+ keyBind: "Ctrl+C",
+ visible: showCopy,
+ disabled: false,
+ callback: () => storeComponentForCopy(false),
+ },
+ {
+ icon: "LayersSendToBack",
+ name: "Paste",
+ keyBind: "Ctrl+V",
+ visible: true,
+ disabled: noPaste,
+ callback: () => pasteComponent("inside"),
+ },
+ ]
+}
+
+export default getContextMenuItems
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
index 4a6716ebc5..fce8c12800 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte
@@ -7,14 +7,15 @@
componentStore,
userSelectedResourceMap,
hoverStore,
+ contextMenuStore,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import ComponentTree from "./ComponentTree.svelte"
import { dndStore, DropPosition } from "./dndStore.js"
- import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
+ import getScreenContextMenuItems from "./getScreenContextMenuItems"
let scrolling = false
@@ -43,6 +44,32 @@
}
const hover = hoverStore.hover
+
+ // showCopy is used to hide the copy button when the user right-clicks the empty
+ // background of their component tree. Pasting in the empty space makes sense,
+ // but copying it doesn't
+ const openScreenContextMenu = (e, showCopy) => {
+ const screenComponent = $selectedScreen?.props
+ const definition = componentStore.getDefinition(screenComponent?._component)
+ // "editable" has been repurposed for inline text editing.
+ // It remains here for legacy compatibility.
+ // Future components should define "static": true for indicate they should
+ // not show a context menu.
+ if (definition?.editable !== false && definition?.static !== true) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ const items = getScreenContextMenuItems(screenComponent, showCopy)
+ contextMenuStore.open(
+ `${showCopy ? "background-" : ""}screenComponent._id`,
+ items,
+ {
+ x: e.clientX,
+ y: e.clientY,
+ }
+ )
+ }
+ }
@@ -56,8 +83,11 @@
-
-
+ openScreenContextMenu(e, false)}
+ >
+ openScreenContextMenu(e, true)}>
{
componentStore.select(`${$screenStore.selectedScreenId}-screen`)
}}
- hovering={$hoverStore.componentId === screenComponentId}
+ hovering={$hoverStore.componentId === screenComponentId ||
+ $selectedScreen?.props._id === $contextMenuStore.id}
on:mouseenter={() => hover(screenComponentId)}
on:mouseleave={() => hover(null)}
id="component-screen"
selectedBy={$userSelectedResourceMap[screenComponentId]}
>
-
+ openScreenContextMenu(e, $selectedScreen?.props)}
+ />
+
+
- import { screenStore, componentStore, navigationStore } from "stores/builder"
- import ConfirmDialog from "components/common/ConfirmDialog.svelte"
+ import { Modal, Helpers, notifications, Icon } from "@budibase/bbui"
import {
- ActionMenu,
- MenuItem,
- Icon,
- Modal,
- Helpers,
- notifications,
- } from "@budibase/bbui"
+ navigationStore,
+ screenStore,
+ userSelectedResourceMap,
+ contextMenuStore,
+ componentStore,
+ } from "stores/builder"
+ import NavItem from "components/common/NavItem.svelte"
+ import RoleIndicator from "./RoleIndicator.svelte"
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl"
import { makeComponentUnique } from "helpers/components"
import { capitalise } from "helpers"
+ import ConfirmDialog from "components/common/ConfirmDialog.svelte"
- export let screenId
+ export let screen
let confirmDeleteDialog
let screenDetailsModal
- $: screen = $screenStore.screens.find(screen => screen._id === screenId)
- $: noPaste = !$componentStore.componentToPaste
-
- const pasteComponent = mode => {
- try {
- componentStore.paste(screen.props, mode, screen)
- } catch (error) {
- notifications.error("Error saving component")
- }
- }
-
- const duplicateScreen = () => {
- screenDetailsModal.show()
- }
-
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
// Create a dupe and ensure it is unique
let duplicateScreen = Helpers.cloneDeep(screen)
@@ -69,22 +55,75 @@
notifications.error("Error deleting screen")
}
}
+
+ $: noPaste = !$componentStore.componentToPaste
+
+ const pasteComponent = mode => {
+ try {
+ componentStore.paste(screen.props, mode, screen)
+ } catch (error) {
+ notifications.error("Error saving component")
+ }
+ }
+
+ const openContextMenu = (e, screen) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ const items = [
+ {
+ icon: "ShowOneLayer",
+ name: "Paste inside",
+ keyBind: null,
+ visible: true,
+ disabled: noPaste,
+ callback: () => pasteComponent("inside"),
+ },
+ {
+ icon: "Duplicate",
+ name: "Duplicate",
+ keyBind: null,
+ visible: true,
+ disabled: false,
+ callback: screenDetailsModal.show,
+ },
+ {
+ icon: "Delete",
+ name: "Delete",
+ keyBind: null,
+ visible: true,
+ disabled: false,
+ callback: confirmDeleteDialog.show,
+ },
+ ]
+
+ contextMenuStore.open(screen._id, items, { x: e.clientX, y: e.clientY })
+ }
-
-
-
+
openContextMenu(e, screen)}
+ scrollable
+ icon={screen.routing.homeScreen ? "Home" : null}
+ indentLevel={0}
+ selected={$screenStore.selectedScreenId === screen._id}
+ hovering={screen._id === $contextMenuStore.id}
+ text={screen.routing.route}
+ on:click={() => screenStore.select(screen._id)}
+ rightAlignIcon
+ showTooltip
+ selectedBy={$userSelectedResourceMap[screen._id]}
+>
+ openContextMenu(e, screen)}
+ size="S"
+ hoverable
+ name="MoreSmallList"
+ />
+
+
- pasteComponent("inside")}
- disabled={noPaste}
- >
- Paste inside
-
- Duplicate
- Delete
-
+
.icon {
- display: grid;
- place-items: center;
+ margin-left: 4px;
+ margin-right: 4px;
}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte
index 27df661281..5c9da100e4 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte
@@ -1,13 +1,7 @@
@@ -116,10 +137,15 @@
size="S"
/>
-
{#if noScreens}
@@ -155,6 +181,7 @@
/>
{/if}
+
diff --git a/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte b/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte
index e9cd170c0b..0f72accf9f 100644
--- a/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte
@@ -1,11 +1,9 @@
focusedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)}
+ on:mousedown={startSelection}
+ on:mouseenter={updateSelectionCallback}
+ on:mouseup={stopSelectionCallback}
+ on:click={handleClick}
width={column.width}
>
{#if error}
@@ -155,6 +156,7 @@
.cell.focused.readonly {
--cell-background: var(--cell-background-hover);
}
+ .cell.selected.focused,
.cell.selected:not(.focused) {
--cell-background: var(--spectrum-global-color-blue-100);
}
diff --git a/packages/frontend-core/src/components/grid/cells/GutterCell.svelte b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte
index 60b41a2b87..0cb9502322 100644
--- a/packages/frontend-core/src/components/grid/cells/GutterCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte
@@ -16,14 +16,22 @@
const { config, dispatch, selectedRows } = getContext("grid")
const svelteDispatch = createEventDispatcher()
- $: selectionEnabled = $config.canSelectRows || $config.canDeleteRows
-
const select = e => {
e.stopPropagation()
svelteDispatch("select")
const id = row?._id
if (id) {
- selectedRows.actions.toggleRow(id)
+ // Bulk select with shift
+ if (e.shiftKey) {
+ // Prevent default if already selected, to prevent checkbox clearing
+ if (rowSelected) {
+ e.preventDefault()
+ } else {
+ selectedRows.actions.bulkSelectRows(id)
+ }
+ } else {
+ selectedRows.actions.toggleRow(id)
+ }
}
}
@@ -54,16 +62,14 @@
{#if !disableNumber}
{row.__idx + 1}
diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
index c999bf6006..3b6aa5d424 100644
--- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
@@ -18,7 +18,7 @@
isReordering,
isResizing,
sort,
- visibleColumns,
+ scrollableColumns,
dispatch,
subscribe,
config,
@@ -51,7 +51,7 @@
$: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && idx > 0
- $: canMoveRight = orderable && idx < $visibleColumns.length - 1
+ $: canMoveRight = orderable && idx < $scrollableColumns.length - 1
$: sortingLabels = getSortingLabels(column.schema?.type)
$: searchable = isColumnSearchable(column)
$: resetSearchValue(column.name)
@@ -270,7 +270,7 @@
on:touchcancel={onMouseUp}
on:contextmenu={onContextMenu}
width={column.width}
- left={column.left}
+ left={column.__left}
defaultHeight
center
>
diff --git a/packages/frontend-core/src/components/grid/controls/BulkDeleteHandler.svelte b/packages/frontend-core/src/components/grid/controls/BulkDeleteHandler.svelte
index cb90f12293..027ac96aa2 100644
--- a/packages/frontend-core/src/components/grid/controls/BulkDeleteHandler.svelte
+++ b/packages/frontend-core/src/components/grid/controls/BulkDeleteHandler.svelte
@@ -1,35 +1,120 @@
-
+
- Are you sure you want to delete {selectedRowCount}
- row{selectedRowCount === 1 ? "" : "s"}?
+ Are you sure you want to delete {promptQuantity} rows?
+ {#if processing}
+
+ {/if}
+
+
+
+
+
+ Are you sure you want to delete {promptQuantity} cells?
+ {#if processing}
+
+ {/if}
diff --git a/packages/frontend-core/src/components/grid/controls/BulkDuplicationHandler.svelte b/packages/frontend-core/src/components/grid/controls/BulkDuplicationHandler.svelte
new file mode 100644
index 0000000000..a300843185
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/BulkDuplicationHandler.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+ Are you sure you want to duplicate {promptQuantity} rows?
+ {#if processing}
+
+ {/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte b/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte
new file mode 100644
index 0000000000..142f6419ad
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+ Are you sure you want to paste? This will update multiple values.
+ {#if processing}
+
+ {/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte
index f16a1183a4..f3a7678cf2 100644
--- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte
+++ b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte
@@ -7,14 +7,12 @@
export let allowViewReadonlyColumns = false
- const { columns, datasource, stickyColumn, dispatch } = getContext("grid")
+ const { columns, datasource, dispatch } = getContext("grid")
let open = false
let anchor
- $: allColumns = $stickyColumn ? [$stickyColumn, ...$columns] : $columns
-
- $: restrictedColumns = allColumns.filter(col => !col.visible || col.readonly)
+ $: restrictedColumns = $columns.filter(col => !col.visible || col.readonly)
$: anyRestricted = restrictedColumns.length
$: text = anyRestricted ? `Columns (${anyRestricted} restricted)` : "Columns"
@@ -43,12 +41,9 @@
HIDDEN: "hidden",
}
- $: displayColumns = allColumns.map(c => {
+ $: displayColumns = $columns.map(c => {
const isRequired = helpers.schema.isRequired(c.schema.constraints)
- const isDisplayColumn = $stickyColumn === c
-
const requiredTooltip = isRequired && "Required columns must be writable"
-
const editEnabled =
!isRequired ||
columnToPermissionOptions(c) !== PERMISSION_OPTIONS.WRITABLE
@@ -74,9 +69,9 @@
options.push({
icon: "VisibilityOff",
value: PERMISSION_OPTIONS.HIDDEN,
- disabled: isDisplayColumn || isRequired,
+ disabled: c.primaryDisplay || isRequired,
tooltip:
- (isDisplayColumn && "Display column cannot be hidden") ||
+ (c.primaryDisplay && "Display column cannot be hidden") ||
requiredTooltip ||
"Hidden",
})
diff --git a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte b/packages/frontend-core/src/components/grid/controls/SizeButton.svelte
index c2797ce537..320aa47345 100644
--- a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte
+++ b/packages/frontend-core/src/components/grid/controls/SizeButton.svelte
@@ -8,14 +8,8 @@
SmallRowHeight,
} from "../lib/constants"
- const {
- stickyColumn,
- columns,
- rowHeight,
- definition,
- fixedRowHeight,
- datasource,
- } = getContext("grid")
+ const { columns, rowHeight, definition, fixedRowHeight, datasource } =
+ getContext("grid")
// Some constants for column width options
const smallColSize = 120
@@ -42,10 +36,9 @@
let anchor
// Column width sizes
- $: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
- $: allSmall = allCols.every(col => col.width === smallColSize)
- $: allMedium = allCols.every(col => col.width === mediumColSize)
- $: allLarge = allCols.every(col => col.width === largeColSize)
+ $: allSmall = $columns.every(col => col.width === smallColSize)
+ $: allMedium = $columns.every(col => col.width === mediumColSize)
+ $: allLarge = $columns.every(col => col.width === largeColSize)
$: custom = !allSmall && !allMedium && !allLarge
$: columnSizeOptions = [
{
@@ -80,7 +73,7 @@
size="M"
on:click={() => (open = !open)}
selected={open}
- disabled={!allCols.length}
+ disabled={!$columns.length}
>
Size
diff --git a/packages/frontend-core/src/components/grid/controls/SortButton.svelte b/packages/frontend-core/src/components/grid/controls/SortButton.svelte
index 339ed32293..96e5481d7a 100644
--- a/packages/frontend-core/src/components/grid/controls/SortButton.svelte
+++ b/packages/frontend-core/src/components/grid/controls/SortButton.svelte
@@ -3,34 +3,20 @@
import { ActionButton, Popover, Select } from "@budibase/bbui"
import { canBeSortColumn } from "@budibase/shared-core"
- const { sort, columns, stickyColumn } = getContext("grid")
+ const { sort, columns } = getContext("grid")
let open = false
let anchor
- $: columnOptions = getColumnOptions($stickyColumn, $columns)
+ $: columnOptions = $columns
+ .map(col => ({
+ label: col.label || col.name,
+ value: col.name,
+ type: col.schema?.type,
+ }))
+ .filter(col => canBeSortColumn(col.type))
$: orderOptions = getOrderOptions($sort.column, columnOptions)
- const getColumnOptions = (stickyColumn, columns) => {
- let options = []
- if (stickyColumn) {
- options.push({
- label: stickyColumn.label || stickyColumn.name,
- value: stickyColumn.name,
- type: stickyColumn.schema?.type,
- })
- }
- options = [
- ...options,
- ...columns.map(col => ({
- label: col.label || col.name,
- value: col.name,
- type: col.schema?.type,
- })),
- ]
- return options.filter(col => canBeSortColumn(col.type))
- }
-
const getOrderOptions = (column, columnOptions) => {
const type = columnOptions.find(col => col.value === column)?.type
return [
diff --git a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
index ead2c67787..159f0dbd45 100644
--- a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
+++ b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte
@@ -13,8 +13,8 @@
rows,
focusedRow,
selectedRows,
- visibleColumns,
- scroll,
+ scrollableColumns,
+ scrollLeft,
isDragging,
buttonColumnWidth,
showVScrollbar,
@@ -24,12 +24,13 @@
let container
$: buttons = $props.buttons?.slice(0, 3) || []
- $: columnsWidth = $visibleColumns.reduce(
+ $: columnsWidth = $scrollableColumns.reduce(
(total, col) => (total += col.width),
0
)
- $: end = columnsWidth - 1 - $scroll.left
- $: left = Math.min($width - $buttonColumnWidth, end)
+ $: columnEnd = columnsWidth - $scrollLeft - 1
+ $: gridEnd = $width - $buttonColumnWidth - 1
+ $: left = Math.min(columnEnd, gridEnd)
const handleClick = async (button, row) => {
await button.onClick?.(rows.actions.cleanRow(row))
@@ -40,7 +41,7 @@
onMount(() => {
const observer = new ResizeObserver(entries => {
const width = entries?.[0]?.contentRect?.width ?? 0
- buttonColumnWidth.set(width)
+ buttonColumnWidth.set(Math.floor(width) - 1)
})
observer.observe(container)
})
@@ -51,6 +52,7 @@
class="button-column"
style="left:{left}px"
class:hidden={$buttonColumnWidth === 0}
+ class:right-border={left !== gridEnd}
>
($hoveredRowId = null)}>
@@ -150,4 +152,7 @@
.button-column :global(.cell) {
border-left: var(--cell-border);
}
+ .button-column:not(.right-border) :global(.cell) {
+ border-right-color: transparent;
+ }
diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte
index 8ea9e2264d..878a9805c0 100644
--- a/packages/frontend-core/src/components/grid/layout/Grid.svelte
+++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte
@@ -7,6 +7,8 @@
import { createAPIClient } from "../../../api"
import { attachStores } from "../stores"
import BulkDeleteHandler from "../controls/BulkDeleteHandler.svelte"
+ import BulkDuplicationHandler from "../controls/BulkDuplicationHandler.svelte"
+ import ClipboardHandler from "../controls/ClipboardHandler.svelte"
import GridBody from "./GridBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
@@ -42,7 +44,6 @@
export let canDeleteRows = true
export let canEditColumns = true
export let canSaveSchema = true
- export let canSelectRows = false
export let stripeRows = false
export let quiet = false
export let collaboration = true
@@ -99,7 +100,6 @@
canDeleteRows,
canEditColumns,
canSaveSchema,
- canSelectRows,
stripeRows,
quiet,
collaboration,
@@ -209,9 +209,11 @@
{/if}
- {#if $config.canDeleteRows}
-
+ {#if $config.canAddRows}
+
{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/GridBody.svelte b/packages/frontend-core/src/components/grid/layout/GridBody.svelte
index cf93f3004e..e56db8d088 100644
--- a/packages/frontend-core/src/components/grid/layout/GridBody.svelte
+++ b/packages/frontend-core/src/components/grid/layout/GridBody.svelte
@@ -9,7 +9,7 @@
const {
bounds,
renderedRows,
- visibleColumns,
+ scrollableColumns,
hoveredRowId,
dispatch,
isDragging,
@@ -19,7 +19,7 @@
let body
- $: columnsWidth = $visibleColumns.reduce(
+ $: columnsWidth = $scrollableColumns.reduce(
(total, col) => (total += col.width),
0
)
diff --git a/packages/frontend-core/src/components/grid/layout/GridRow.svelte b/packages/frontend-core/src/components/grid/layout/GridRow.svelte
index c3d6f6eb86..2f63bf0eb6 100644
--- a/packages/frontend-core/src/components/grid/layout/GridRow.svelte
+++ b/packages/frontend-core/src/components/grid/layout/GridRow.svelte
@@ -10,19 +10,23 @@
focusedCellId,
reorder,
selectedRows,
- visibleColumns,
+ scrollableColumns,
hoveredRowId,
- selectedCellMap,
focusedRow,
contentLines,
isDragging,
dispatch,
rows,
columnRenderMap,
+ userCellMap,
+ isSelectingCells,
+ selectedCellMap,
+ selectedCellCount,
} = getContext("grid")
$: rowSelected = !!$selectedRows[row._id]
- $: rowHovered = $hoveredRowId === row._id
+ $: rowHovered =
+ $hoveredRowId === row._id && (!$selectedCellCount || !$isSelectingCells)
$: rowFocused = $focusedRow?._id === row._id
$: reorderSource = $reorder.sourceColumn
@@ -36,22 +40,24 @@
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
>
- {#each $visibleColumns as column}
+ {#each $scrollableColumns as column}
{@const cellId = getCellID(row._id, column.name)}
{/each}
diff --git a/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte b/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte
index 763b01dd84..d3cce30092 100644
--- a/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte
+++ b/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte
@@ -5,7 +5,7 @@
const {
rowHeight,
scroll,
- focusedCellId,
+ ui,
renderedRows,
maxScrollTop,
maxScrollLeft,
@@ -13,6 +13,8 @@
hoveredRowId,
menu,
focusedCellAPI,
+ scrollTop,
+ scrollLeft,
} = getContext("grid")
export let scrollVertically = false
@@ -24,11 +26,11 @@
let initialTouchX
let initialTouchY
- $: style = generateStyle($scroll, $rowHeight)
+ $: style = generateStyle($scrollLeft, $scrollTop, $rowHeight)
- const generateStyle = (scroll, rowHeight) => {
- const offsetX = scrollHorizontally ? -1 * scroll.left : 0
- const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
+ const generateStyle = (scrollLeft, scrollTop, rowHeight) => {
+ const offsetX = scrollHorizontally ? -1 * scrollLeft : 0
+ const offsetY = scrollVertically ? -1 * (scrollTop % rowHeight) : 0
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
}
@@ -108,7 +110,7 @@
on:wheel={attachHandlers ? handleWheel : null}
on:touchstart={attachHandlers ? handleTouchStart : null}
on:touchmove={attachHandlers ? handleTouchMove : null}
- on:click|self={() => ($focusedCellId = null)}
+ on:click|self={ui.actions.blur}
>
diff --git a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte
index b8655b98b3..21015b7b20 100644
--- a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte
+++ b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte
@@ -5,14 +5,14 @@
import HeaderCell from "../cells/HeaderCell.svelte"
import { TempTooltip, TooltipType } from "@budibase/bbui"
- const { visibleColumns, config, hasNonAutoColumn, datasource, loading } =
+ const { scrollableColumns, config, hasNonAutoColumn, datasource, loading } =
getContext("grid")