Merge branch 'table-improvements-2' of github.com:Budibase/budibase into conditional-table-colours
This commit is contained in:
commit
fbeb8199f1
|
@ -1,14 +1,5 @@
|
|||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
||||
"_id",
|
||||
"_rev",
|
||||
"type",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"tableId",
|
||||
] as const
|
||||
|
||||
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
|
||||
|
||||
export function isInternalColumnName(name: string): boolean {
|
||||
return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
|
||||
}
|
||||
export {
|
||||
CONSTANT_INTERNAL_ROW_COLS,
|
||||
CONSTANT_EXTERNAL_ROW_COLS,
|
||||
isInternalColumnName,
|
||||
} from "@budibase/shared-core"
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
SWITCHABLE_TYPES,
|
||||
ValidColumnNameRegex,
|
||||
helpers,
|
||||
CONSTANT_INTERNAL_ROW_COLS,
|
||||
CONSTANT_EXTERNAL_ROW_COLS,
|
||||
} from "@budibase/shared-core"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -52,7 +54,6 @@
|
|||
const DATE_TYPE = FieldType.DATETIME
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||
|
||||
export let field
|
||||
|
@ -487,20 +488,27 @@
|
|||
})
|
||||
}
|
||||
const newError = {}
|
||||
const prohibited = externalTable
|
||||
? CONSTANT_EXTERNAL_ROW_COLS
|
||||
: CONSTANT_INTERNAL_ROW_COLS
|
||||
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
||||
newError.name = `Column name cannot start with an underscore.`
|
||||
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
||||
newError.name = `Illegal character; must be alpha-numeric.`
|
||||
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
||||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
||||
} else if (
|
||||
prohibited.some(
|
||||
name => fieldInfo?.name?.toLowerCase() === name.toLowerCase()
|
||||
)
|
||||
) {
|
||||
newError.name = `${prohibited.join(
|
||||
", "
|
||||
)} are not allowed as column names`
|
||||
)} are not allowed as column names - case insensitive.`
|
||||
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
|
||||
newError.name = `Column name already in use.`
|
||||
}
|
||||
|
||||
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
|
||||
newError.subtype = `Auto Column requires a type`
|
||||
newError.subtype = `Auto Column requires a type.`
|
||||
}
|
||||
|
||||
if (fieldInfo.fieldName && fieldInfo.tableId) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import GridCell from "../cells/GridCell.svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import { BlankRowID } from "../lib/constants"
|
||||
|
||||
const {
|
||||
renderedRows,
|
||||
|
@ -13,10 +14,11 @@
|
|||
focusedRow,
|
||||
selectedRows,
|
||||
scrollableColumns,
|
||||
scroll,
|
||||
scrollLeft,
|
||||
isDragging,
|
||||
buttonColumnWidth,
|
||||
showVScrollbar,
|
||||
dispatch,
|
||||
} = getContext("grid")
|
||||
|
||||
let container
|
||||
|
@ -26,8 +28,8 @@
|
|||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
$: columnEnd = columnsWidth - $scroll.left - 1
|
||||
$: gridEnd = $width - $buttonColumnWidth
|
||||
$: columnEnd = columnsWidth - $scrollLeft - 1
|
||||
$: gridEnd = $width - $buttonColumnWidth - 1
|
||||
$: left = Math.min(columnEnd, gridEnd)
|
||||
|
||||
const handleClick = async (button, row) => {
|
||||
|
@ -39,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)
|
||||
})
|
||||
|
@ -91,6 +93,17 @@
|
|||
</GridCell>
|
||||
</div>
|
||||
{/each}
|
||||
<div
|
||||
class="row blank"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
>
|
||||
<GridCell
|
||||
width={$buttonColumnWidth}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
/>
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -131,8 +144,11 @@
|
|||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.blank :global(.cell:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Add left cell border */
|
||||
/* Add left cell border to all cells */
|
||||
.button-column :global(.cell) {
|
||||
border-left: var(--cell-border);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
MaxCellRenderOverflow,
|
||||
GutterWidth,
|
||||
DefaultRowHeight,
|
||||
Padding,
|
||||
VPadding,
|
||||
SmallRowHeight,
|
||||
ControlsHeight,
|
||||
ScrollBarSize,
|
||||
|
@ -119,7 +119,7 @@
|
|||
// Derive min height and make available in context
|
||||
const minHeight = derived(rowHeight, $height => {
|
||||
const heightForControls = showControls ? ControlsHeight : 0
|
||||
return Padding + SmallRowHeight + $height + heightForControls
|
||||
return VPadding + SmallRowHeight + $height + heightForControls
|
||||
})
|
||||
context = { ...context, minHeight }
|
||||
|
||||
|
@ -357,8 +357,13 @@
|
|||
transition: none;
|
||||
}
|
||||
|
||||
/* Overrides */
|
||||
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)) {
|
||||
/* Overrides for quiet */
|
||||
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)),
|
||||
.grid.quiet :global(.sticky-column .row > .cell),
|
||||
.grid.quiet :global(.new-row .row > .cell:not(:last-child)) {
|
||||
border-right: none;
|
||||
}
|
||||
.grid.quiet :global(.sticky-column:before) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import GridRow from "./GridRow.svelte"
|
||||
import GridCell from "../cells/GridCell.svelte"
|
||||
import { BlankRowID } from "../lib/constants"
|
||||
import ButtonColumn from "./ButtonColumn.svelte"
|
||||
|
||||
|
@ -46,7 +47,6 @@
|
|||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div bind:this={body} class="grid-body">
|
||||
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||
{#each $renderedRows as row, idx}
|
||||
|
@ -54,13 +54,16 @@
|
|||
{/each}
|
||||
{#if $config.canAddRows}
|
||||
<div
|
||||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
style="width:{columnsWidth}px"
|
||||
class="row blank"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
/>
|
||||
>
|
||||
<GridCell
|
||||
width={columnsWidth}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
{#if $props.buttons?.length}
|
||||
|
@ -76,15 +79,13 @@
|
|||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.blank {
|
||||
height: var(--row-height);
|
||||
background: var(--cell-background);
|
||||
border-bottom: var(--cell-border);
|
||||
border-right: var(--cell-border);
|
||||
position: absolute;
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.blank.highlighted {
|
||||
background: var(--cell-background-hover);
|
||||
.blank :global(.cell:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
hoveredRowId,
|
||||
menu,
|
||||
focusedCellAPI,
|
||||
scrollTop,
|
||||
scrollLeft,
|
||||
} = getContext("grid")
|
||||
|
||||
export let scrollVertically = false
|
||||
|
@ -24,13 +26,11 @@
|
|||
let initialTouchX
|
||||
let initialTouchY
|
||||
|
||||
$: style = generateStyle($scroll, $rowHeight)
|
||||
$: style = generateStyle($scrollLeft, $scrollTop, $rowHeight)
|
||||
|
||||
const generateStyle = (scroll, rowHeight) => {
|
||||
const offsetX = Math.round(scrollHorizontally ? -1 * scroll.left : 0)
|
||||
const offsetY = Math.round(
|
||||
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: translate(${offsetX}px, ${offsetY}px);`
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,14 @@
|
|||
import { Icon } from "@budibase/bbui"
|
||||
import GridPopover from "../overlays/GridPopover.svelte"
|
||||
|
||||
const { scrollableColumns, scroll, width, subscribe, ui, keyboardBlocked } =
|
||||
getContext("grid")
|
||||
const {
|
||||
scrollableColumns,
|
||||
scrollLeft,
|
||||
width,
|
||||
subscribe,
|
||||
ui,
|
||||
keyboardBlocked,
|
||||
} = getContext("grid")
|
||||
|
||||
let anchor
|
||||
let isOpen = false
|
||||
|
@ -13,7 +19,7 @@
|
|||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
$: end = columnsWidth - 1 - $scroll.left
|
||||
$: end = columnsWidth - 1 - $scrollLeft
|
||||
$: left = Math.min($width - 40, end)
|
||||
$: keyboardBlocked.set(isOpen)
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
inlineFilters,
|
||||
columnRenderMap,
|
||||
visibleColumns,
|
||||
scrollTop,
|
||||
} = getContext("grid")
|
||||
|
||||
let visible = false
|
||||
|
@ -44,6 +45,21 @@
|
|||
$: $datasource, (visible = false)
|
||||
$: selectedRowCount = Object.values($selectedRows).length
|
||||
$: hasNoRows = !$rows.length
|
||||
$: renderedRowCount = $renderedRows.length
|
||||
$: offset = getOffset($hasNextPage, renderedRowCount, $rowHeight, $scrollTop)
|
||||
|
||||
const getOffset = (hasNextPage, rowCount, rowHeight, scrollTop) => {
|
||||
// If we have a next page of data then we aren't truly at the bottom, so we
|
||||
// render the add row component at the top
|
||||
if (hasNextPage) {
|
||||
return 0
|
||||
}
|
||||
offset = rowCount * rowHeight - (scrollTop % rowHeight)
|
||||
if (rowCount !== 0) {
|
||||
offset -= 1
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
const addRow = async () => {
|
||||
// Blur the active cell and tick to let final value updates propagate
|
||||
|
@ -89,12 +105,6 @@
|
|||
return
|
||||
}
|
||||
|
||||
// If we have a next page of data then we aren't truly at the bottom, so we
|
||||
// render the add row component at the top
|
||||
if ($hasNextPage) {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// If we don't have a next page then we're at the bottom and can scroll to
|
||||
// the max available offset
|
||||
else {
|
||||
|
@ -102,10 +112,6 @@
|
|||
...state,
|
||||
top: $maxScrollTop,
|
||||
}))
|
||||
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
|
||||
if ($renderedRows.length !== 0) {
|
||||
offset -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// Update state and select initial cell
|
||||
|
@ -175,39 +181,41 @@
|
|||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if visible}
|
||||
<div
|
||||
class="container"
|
||||
class="new-row"
|
||||
class:floating={offset > 0}
|
||||
style="--offset:{offset}px; --sticky-width:{width}px;"
|
||||
>
|
||||
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</GutterCell>
|
||||
{#if $displayColumn}
|
||||
{@const cellId = getCellID(NewRowID, $displayColumn.name)}
|
||||
<DataCell
|
||||
{cellId}
|
||||
rowFocused
|
||||
column={$displayColumn}
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={$displayColumn.width}
|
||||
{updateValue}
|
||||
topRow={offset === 0}
|
||||
>
|
||||
{#if $displayColumn?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
<div class="row">
|
||||
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/if}
|
||||
</GutterCell>
|
||||
{#if $displayColumn}
|
||||
{@const cellId = getCellID(NewRowID, $displayColumn.name)}
|
||||
<DataCell
|
||||
{cellId}
|
||||
rowFocused
|
||||
column={$displayColumn}
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={$displayColumn.width}
|
||||
{updateValue}
|
||||
topRow={offset === 0}
|
||||
>
|
||||
{#if $displayColumn?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||
|
@ -274,7 +282,7 @@
|
|||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.container {
|
||||
.new-row {
|
||||
position: absolute;
|
||||
top: var(--default-row-height);
|
||||
left: 0;
|
||||
|
@ -284,10 +292,10 @@
|
|||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
.container :global(.cell) {
|
||||
.new-row :global(.cell) {
|
||||
--cell-background: var(--spectrum-global-color-gray-75) !important;
|
||||
}
|
||||
.container.floating :global(.cell) {
|
||||
.new-row.floating :global(.cell) {
|
||||
height: calc(var(--row-height) + 1px);
|
||||
border-top: var(--cell-border);
|
||||
}
|
||||
|
@ -316,8 +324,10 @@
|
|||
pointer-events: all;
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: calc(var(--row-height) + var(--offset) + 24px);
|
||||
left: 18px;
|
||||
top: calc(
|
||||
var(--row-height) + var(--offset) + var(--default-row-height) / 2
|
||||
);
|
||||
left: calc(var(--default-row-height) / 2);
|
||||
}
|
||||
.button-with-keys {
|
||||
display: flex;
|
||||
|
|
|
@ -69,66 +69,62 @@
|
|||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="content">
|
||||
<GridScrollWrapper scrollVertically attachHandlers>
|
||||
{#each $renderedRows as row, idx}
|
||||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@const rowHovered =
|
||||
$hoveredRowId === row._id &&
|
||||
(!$selectedCellCount || !$isSelectingCells)}
|
||||
{@const rowFocused = $focusedRow?._id === row._id}
|
||||
{@const cellId = getCellID(row._id, $displayColumn?.name)}
|
||||
<div
|
||||
class="row"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||
>
|
||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||
{#if $displayColumn}
|
||||
<DataCell
|
||||
{row}
|
||||
{cellId}
|
||||
{rowFocused}
|
||||
{rowSelected}
|
||||
cellSelected={$selectedCellMap[cellId]}
|
||||
highlighted={rowHovered || rowFocused}
|
||||
rowIdx={row.__idx}
|
||||
topRow={idx === 0}
|
||||
focused={$focusedCellId === cellId}
|
||||
selectedUser={$userCellMap[cellId]}
|
||||
width={$displayColumn.width}
|
||||
column={$displayColumn}
|
||||
contentLines={$contentLines}
|
||||
isSelectingCells={$isSelectingCells}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $config.canAddRows}
|
||||
<div
|
||||
class="row new"
|
||||
on:mouseenter={$isDragging
|
||||
? null
|
||||
: () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
>
|
||||
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
</GutterCell>
|
||||
{#if $displayColumn}
|
||||
<GridCell
|
||||
width={$displayColumn.width}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
>
|
||||
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
||||
</GridCell>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
<GridScrollWrapper scrollVertically attachHandlers>
|
||||
{#each $renderedRows as row, idx}
|
||||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@const rowHovered =
|
||||
$hoveredRowId === row._id &&
|
||||
(!$selectedCellCount || !$isSelectingCells)}
|
||||
{@const rowFocused = $focusedRow?._id === row._id}
|
||||
{@const cellId = getCellID(row._id, $displayColumn?.name)}
|
||||
<div
|
||||
class="row"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||
>
|
||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||
{#if $displayColumn}
|
||||
<DataCell
|
||||
{row}
|
||||
{cellId}
|
||||
{rowFocused}
|
||||
{rowSelected}
|
||||
cellSelected={$selectedCellMap[cellId]}
|
||||
highlighted={rowHovered || rowFocused}
|
||||
rowIdx={row.__idx}
|
||||
topRow={idx === 0}
|
||||
focused={$focusedCellId === cellId}
|
||||
selectedUser={$userCellMap[cellId]}
|
||||
width={$displayColumn.width}
|
||||
column={$displayColumn}
|
||||
contentLines={$contentLines}
|
||||
isSelectingCells={$isSelectingCells}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $config.canAddRows}
|
||||
<div
|
||||
class="row blank"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
>
|
||||
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
</GutterCell>
|
||||
{#if $displayColumn}
|
||||
<GridCell
|
||||
width={$displayColumn.width}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
>
|
||||
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
||||
</GridCell>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -184,11 +180,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.row.new :global(*:hover) {
|
||||
.blank :global(.cell:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
export const Padding = 100
|
||||
export const ScrollBarSize = 8
|
||||
export const GutterWidth = 72
|
||||
export const DefaultColumnWidth = 200
|
||||
export const MinColumnWidth = 80
|
||||
export const SmallRowHeight = 36
|
||||
export const MediumRowHeight = 64
|
||||
export const LargeRowHeight = 92
|
||||
export const DefaultRowHeight = SmallRowHeight
|
||||
export const VPadding = SmallRowHeight * 2
|
||||
export const HPadding = 40
|
||||
export const ScrollBarSize = 8
|
||||
export const GutterWidth = 72
|
||||
export const DefaultColumnWidth = 200
|
||||
export const MinColumnWidth = 80
|
||||
export const NewRowID = "new"
|
||||
export const BlankRowID = "blank"
|
||||
export const RowPageSize = 100
|
||||
|
|
|
@ -119,12 +119,12 @@ export const createActions = context => {
|
|||
// Actual logic to consider the current position and determine the new order
|
||||
const considerReorderPosition = () => {
|
||||
const $reorder = get(reorder)
|
||||
const $scroll = get(scroll)
|
||||
const $scrollLeft = get(scrollLeft)
|
||||
|
||||
// Compute the closest breakpoint to the current position
|
||||
let breakpoint
|
||||
let minDistance = Number.MAX_SAFE_INTEGER
|
||||
const mouseX = latestX - $reorder.gridLeft + $scroll.left
|
||||
const mouseX = latestX - $reorder.gridLeft + $scrollLeft
|
||||
$reorder.breakpoints.forEach(point => {
|
||||
const distance = Math.abs(point.x - mouseX)
|
||||
if (distance < minDistance) {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tick } from "svelte"
|
||||
import { Padding, GutterWidth, FocusedCellMinOffset } from "../lib/constants"
|
||||
import {
|
||||
GutterWidth,
|
||||
FocusedCellMinOffset,
|
||||
ScrollBarSize,
|
||||
HPadding,
|
||||
VPadding,
|
||||
} from "../lib/constants"
|
||||
import { parseCellID } from "../lib/utils"
|
||||
|
||||
export const createStores = () => {
|
||||
|
@ -10,8 +16,8 @@ export const createStores = () => {
|
|||
})
|
||||
|
||||
// Derive height and width as primitives to avoid wasted computation
|
||||
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
|
||||
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
|
||||
const scrollTop = derived(scroll, $scroll => Math.round($scroll.top))
|
||||
const scrollLeft = derived(scroll, $scroll => Math.round($scroll.left))
|
||||
|
||||
return {
|
||||
scroll,
|
||||
|
@ -36,22 +42,11 @@ export const deriveStores = context => {
|
|||
return ($displayColumn?.width || 0) + GutterWidth
|
||||
})
|
||||
|
||||
// Derive vertical limits
|
||||
const contentHeight = derived(
|
||||
[rows, rowHeight],
|
||||
([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding
|
||||
)
|
||||
const maxScrollTop = derived(
|
||||
[height, contentHeight],
|
||||
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0)
|
||||
)
|
||||
|
||||
// Derive horizontal limits
|
||||
const contentWidth = derived(
|
||||
[visibleColumns, buttonColumnWidth],
|
||||
([$visibleColumns, $buttonColumnWidth]) => {
|
||||
const space = Math.max(Padding, $buttonColumnWidth - 1)
|
||||
let width = GutterWidth + space
|
||||
let width = GutterWidth + Math.max($buttonColumnWidth, HPadding)
|
||||
$visibleColumns.forEach(col => {
|
||||
width += col.width
|
||||
})
|
||||
|
@ -67,15 +62,7 @@ export const deriveStores = context => {
|
|||
const maxScrollLeft = derived(
|
||||
[contentWidth, screenWidth],
|
||||
([$contentWidth, $screenWidth]) => {
|
||||
return Math.max($contentWidth - $screenWidth, 0)
|
||||
}
|
||||
)
|
||||
|
||||
// Derive whether to show scrollbars or not
|
||||
const showVScrollbar = derived(
|
||||
[contentHeight, height],
|
||||
([$contentHeight, $height]) => {
|
||||
return $contentHeight > $height
|
||||
return Math.round(Math.max($contentWidth - $screenWidth, 0))
|
||||
}
|
||||
)
|
||||
const showHScrollbar = derived(
|
||||
|
@ -85,6 +72,29 @@ export const deriveStores = context => {
|
|||
}
|
||||
)
|
||||
|
||||
// Derive vertical limits
|
||||
const contentHeight = derived(
|
||||
[rows, rowHeight, showHScrollbar],
|
||||
([$rows, $rowHeight, $showHScrollbar]) => {
|
||||
let height = ($rows.length + 1) * $rowHeight + VPadding
|
||||
if ($showHScrollbar) {
|
||||
height += ScrollBarSize * 2
|
||||
}
|
||||
return height
|
||||
}
|
||||
)
|
||||
const maxScrollTop = derived(
|
||||
[height, contentHeight],
|
||||
([$height, $contentHeight]) =>
|
||||
Math.round(Math.max($contentHeight - $height, 0))
|
||||
)
|
||||
const showVScrollbar = derived(
|
||||
[contentHeight, height],
|
||||
([$contentHeight, $height]) => {
|
||||
return $contentHeight > $height
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
stickyWidth,
|
||||
contentHeight,
|
||||
|
|
|
@ -276,6 +276,31 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
it("shouldn't allow duplicate column names", async () => {
|
||||
const saveTableRequest: SaveTableRequest = {
|
||||
...basicTable(),
|
||||
}
|
||||
saveTableRequest.schema["Type"] = {
|
||||
type: FieldType.STRING,
|
||||
name: "Type",
|
||||
}
|
||||
// allow the "Type" column - internal columns aren't case sensitive
|
||||
await config.api.table.save(saveTableRequest, {
|
||||
status: 200,
|
||||
})
|
||||
saveTableRequest.schema.foo = { type: FieldType.STRING, name: "foo" }
|
||||
saveTableRequest.schema.FOO = { type: FieldType.STRING, name: "FOO" }
|
||||
|
||||
await config.api.table.save(saveTableRequest, {
|
||||
status: 400,
|
||||
body: {
|
||||
message:
|
||||
'Column(s) "foo" are duplicated - check for other columns with these name (case in-sensitive)',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should add a new column for an internal DB table", async () => {
|
||||
const saveTableRequest: SaveTableRequest = {
|
||||
...basicTable(),
|
||||
|
|
|
@ -17,6 +17,7 @@ import { cloneDeep } from "lodash/fp"
|
|||
import isEqual from "lodash/isEqual"
|
||||
import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { findDuplicateInternalColumns } from "@budibase/shared-core"
|
||||
import { getTable } from "../getters"
|
||||
import { checkAutoColumns } from "./utils"
|
||||
import * as viewsSdk from "../../views"
|
||||
|
@ -44,6 +45,17 @@ export async function save(
|
|||
if (hasTypeChanged(table, oldTable)) {
|
||||
throw new Error("A column type has changed.")
|
||||
}
|
||||
|
||||
// check for case sensitivity - we don't want to allow duplicated columns
|
||||
const duplicateColumn = findDuplicateInternalColumns(table)
|
||||
if (duplicateColumn.length) {
|
||||
throw new Error(
|
||||
`Column(s) "${duplicateColumn.join(
|
||||
", "
|
||||
)}" are duplicated - check for other columns with these name (case in-sensitive)`
|
||||
)
|
||||
}
|
||||
|
||||
// check that subtypes have been maintained
|
||||
table = checkAutoColumns(table, oldTable)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./api"
|
||||
export * from "./fields"
|
||||
export * from "./rows"
|
||||
|
||||
export const OperatorOptions = {
|
||||
Equals: {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
||||
"_id",
|
||||
"_rev",
|
||||
"type",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"tableId",
|
||||
] as const
|
||||
|
||||
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
|
||||
|
||||
export function isInternalColumnName(name: string): boolean {
|
||||
return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { FieldType, Table } from "@budibase/types"
|
||||
import { CONSTANT_INTERNAL_ROW_COLS } from "./constants"
|
||||
|
||||
const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||
[FieldType.STRING]: true,
|
||||
|
@ -51,3 +52,27 @@ export function canBeDisplayColumn(type: FieldType): boolean {
|
|||
export function canBeSortColumn(type: FieldType): boolean {
|
||||
return !!allowSortColumnByType[type]
|
||||
}
|
||||
|
||||
export function findDuplicateInternalColumns(table: Table): string[] {
|
||||
// maintains the case of keys
|
||||
const casedKeys = Object.keys(table.schema)
|
||||
// get the column names
|
||||
const uncasedKeys = casedKeys.map(colName => colName.toLowerCase())
|
||||
// there are duplicates
|
||||
const set = new Set(uncasedKeys)
|
||||
let duplicates: string[] = []
|
||||
if (set.size !== uncasedKeys.length) {
|
||||
for (let key of set.keys()) {
|
||||
const count = uncasedKeys.filter(name => name === key).length
|
||||
if (count > 1) {
|
||||
duplicates.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let internalColumn of CONSTANT_INTERNAL_ROW_COLS) {
|
||||
if (casedKeys.find(key => key === internalColumn)) {
|
||||
duplicates.push(internalColumn)
|
||||
}
|
||||
}
|
||||
return duplicates
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue