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;" />
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 @@
  • -
    {#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)}