Merge branch 'master' of github.com:Budibase/budibase into table-improvements-2
This commit is contained in:
commit
b58519d562
|
@ -1,14 +1,5 @@
|
||||||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
export {
|
||||||
"_id",
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
"_rev",
|
CONSTANT_EXTERNAL_ROW_COLS,
|
||||||
"type",
|
isInternalColumnName,
|
||||||
"createdAt",
|
} from "@budibase/shared-core"
|
||||||
"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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
SWITCHABLE_TYPES,
|
SWITCHABLE_TYPES,
|
||||||
ValidColumnNameRegex,
|
ValidColumnNameRegex,
|
||||||
helpers,
|
helpers,
|
||||||
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
|
CONSTANT_EXTERNAL_ROW_COLS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
@ -52,7 +54,6 @@
|
||||||
const DATE_TYPE = FieldType.DATETIME
|
const DATE_TYPE = FieldType.DATETIME
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
|
||||||
const { dispatch: gridDispatch, rows } = getContext("grid")
|
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
@ -487,20 +488,27 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const newError = {}
|
const newError = {}
|
||||||
|
const prohibited = externalTable
|
||||||
|
? CONSTANT_EXTERNAL_ROW_COLS
|
||||||
|
: CONSTANT_INTERNAL_ROW_COLS
|
||||||
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
||||||
newError.name = `Column name cannot start with an underscore.`
|
newError.name = `Column name cannot start with an underscore.`
|
||||||
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
||||||
newError.name = `Illegal character; must be alpha-numeric.`
|
newError.name = `Illegal character; must be alpha-numeric.`
|
||||||
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
|
} else if (
|
||||||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
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)) {
|
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
|
||||||
newError.name = `Column name already in use.`
|
newError.name = `Column name already in use.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
|
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) {
|
if (fieldInfo.fieldName && fieldInfo.tableId) {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import GridCell from "../cells/GridCell.svelte"
|
import GridCell from "../cells/GridCell.svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
|
import { BlankRowID } from "../lib/constants"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
renderedRows,
|
renderedRows,
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
isDragging,
|
isDragging,
|
||||||
buttonColumnWidth,
|
buttonColumnWidth,
|
||||||
showVScrollbar,
|
showVScrollbar,
|
||||||
|
dispatch,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
let container
|
let container
|
||||||
|
@ -91,6 +93,17 @@
|
||||||
</GridCell>
|
</GridCell>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/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>
|
</GridScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -131,8 +144,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
.blank :global(.cell:hover) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add left cell border */
|
/* Add left cell border to all cells */
|
||||||
.button-column :global(.cell) {
|
.button-column :global(.cell) {
|
||||||
border-left: var(--cell-border);
|
border-left: var(--cell-border);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
MaxCellRenderOverflow,
|
MaxCellRenderOverflow,
|
||||||
GutterWidth,
|
GutterWidth,
|
||||||
DefaultRowHeight,
|
DefaultRowHeight,
|
||||||
Padding,
|
VPadding,
|
||||||
SmallRowHeight,
|
SmallRowHeight,
|
||||||
ControlsHeight,
|
ControlsHeight,
|
||||||
ScrollBarSize,
|
ScrollBarSize,
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
// Derive min height and make available in context
|
// Derive min height and make available in context
|
||||||
const minHeight = derived(rowHeight, $height => {
|
const minHeight = derived(rowHeight, $height => {
|
||||||
const heightForControls = showControls ? ControlsHeight : 0
|
const heightForControls = showControls ? ControlsHeight : 0
|
||||||
return Padding + SmallRowHeight + $height + heightForControls
|
return VPadding + SmallRowHeight + $height + heightForControls
|
||||||
})
|
})
|
||||||
context = { ...context, minHeight }
|
context = { ...context, minHeight }
|
||||||
|
|
||||||
|
@ -356,8 +356,13 @@
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrides */
|
/* Overrides for quiet */
|
||||||
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)) {
|
.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;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
.grid.quiet :global(.sticky-column:before) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import GridRow from "./GridRow.svelte"
|
import GridRow from "./GridRow.svelte"
|
||||||
|
import GridCell from "../cells/GridCell.svelte"
|
||||||
import { BlankRowID } from "../lib/constants"
|
import { BlankRowID } from "../lib/constants"
|
||||||
import ButtonColumn from "./ButtonColumn.svelte"
|
import ButtonColumn from "./ButtonColumn.svelte"
|
||||||
|
|
||||||
|
@ -46,7 +47,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div bind:this={body} class="grid-body">
|
<div bind:this={body} class="grid-body">
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
|
@ -54,13 +54,16 @@
|
||||||
{/each}
|
{/each}
|
||||||
{#if $config.canAddRows}
|
{#if $config.canAddRows}
|
||||||
<div
|
<div
|
||||||
class="blank"
|
class="row blank"
|
||||||
class:highlighted={$hoveredRowId === BlankRowID}
|
|
||||||
style="width:{columnsWidth}px"
|
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
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}
|
{/if}
|
||||||
</GridScrollWrapper>
|
</GridScrollWrapper>
|
||||||
{#if $props.buttons?.length}
|
{#if $props.buttons?.length}
|
||||||
|
@ -76,15 +79,13 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.blank {
|
.row {
|
||||||
height: var(--row-height);
|
display: flex;
|
||||||
background: var(--cell-background);
|
flex-direction: row;
|
||||||
border-bottom: var(--cell-border);
|
justify-content: flex-start;
|
||||||
border-right: var(--cell-border);
|
align-items: stretch;
|
||||||
position: absolute;
|
|
||||||
}
|
}
|
||||||
.blank.highlighted {
|
.blank :global(.cell:hover) {
|
||||||
background: var(--cell-background-hover);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
inlineFilters,
|
inlineFilters,
|
||||||
columnRenderMap,
|
columnRenderMap,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
|
scrollTop,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
@ -44,6 +45,21 @@
|
||||||
$: $datasource, (visible = false)
|
$: $datasource, (visible = false)
|
||||||
$: selectedRowCount = Object.values($selectedRows).length
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
$: hasNoRows = !$rows.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 () => {
|
const addRow = async () => {
|
||||||
// Blur the active cell and tick to let final value updates propagate
|
// Blur the active cell and tick to let final value updates propagate
|
||||||
|
@ -89,12 +105,6 @@
|
||||||
return
|
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
|
// If we don't have a next page then we're at the bottom and can scroll to
|
||||||
// the max available offset
|
// the max available offset
|
||||||
else {
|
else {
|
||||||
|
@ -102,10 +112,6 @@
|
||||||
...state,
|
...state,
|
||||||
top: $maxScrollTop,
|
top: $maxScrollTop,
|
||||||
}))
|
}))
|
||||||
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
|
|
||||||
if ($renderedRows.length !== 0) {
|
|
||||||
offset -= 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state and select initial cell
|
// Update state and select initial cell
|
||||||
|
@ -175,39 +181,41 @@
|
||||||
<!-- Only show new row functionality if we have any columns -->
|
<!-- Only show new row functionality if we have any columns -->
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="new-row"
|
||||||
class:floating={offset > 0}
|
class:floating={offset > 0}
|
||||||
style="--offset:{offset}px; --sticky-width:{width}px;"
|
style="--offset:{offset}px; --sticky-width:{width}px;"
|
||||||
>
|
>
|
||||||
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||||
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
<div class="row">
|
||||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
||||||
{#if isAdding}
|
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||||
<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}
|
|
||||||
{#if isAdding}
|
{#if isAdding}
|
||||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||||
{/if}
|
{/if}
|
||||||
</DataCell>
|
</GutterCell>
|
||||||
{/if}
|
{#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>
|
||||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
|
@ -274,7 +282,7 @@
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.new-row {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--default-row-height);
|
top: var(--default-row-height);
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -284,10 +292,10 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.container :global(.cell) {
|
.new-row :global(.cell) {
|
||||||
--cell-background: var(--spectrum-global-color-gray-75) !important;
|
--cell-background: var(--spectrum-global-color-gray-75) !important;
|
||||||
}
|
}
|
||||||
.container.floating :global(.cell) {
|
.new-row.floating :global(.cell) {
|
||||||
height: calc(var(--row-height) + 1px);
|
height: calc(var(--row-height) + 1px);
|
||||||
border-top: var(--cell-border);
|
border-top: var(--cell-border);
|
||||||
}
|
}
|
||||||
|
@ -316,8 +324,10 @@
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--row-height) + var(--offset) + 24px);
|
top: calc(
|
||||||
left: 18px;
|
var(--row-height) + var(--offset) + var(--default-row-height) / 2
|
||||||
|
);
|
||||||
|
left: calc(var(--default-row-height) / 2);
|
||||||
}
|
}
|
||||||
.button-with-keys {
|
.button-with-keys {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -69,66 +69,62 @@
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="content">
|
<GridScrollWrapper scrollVertically attachHandlers>
|
||||||
<GridScrollWrapper scrollVertically attachHandlers>
|
{#each $renderedRows as row, idx}
|
||||||
{#each $renderedRows as row, idx}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowHovered =
|
||||||
{@const rowHovered =
|
$hoveredRowId === row._id &&
|
||||||
$hoveredRowId === row._id &&
|
(!$selectedCellCount || !$isSelectingCells)}
|
||||||
(!$selectedCellCount || !$isSelectingCells)}
|
{@const rowFocused = $focusedRow?._id === row._id}
|
||||||
{@const rowFocused = $focusedRow?._id === row._id}
|
{@const cellId = getCellID(row._id, $displayColumn?.name)}
|
||||||
{@const cellId = getCellID(row._id, $displayColumn?.name)}
|
<div
|
||||||
<div
|
class="row"
|
||||||
class="row"
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
>
|
||||||
>
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
{#if $displayColumn}
|
||||||
{#if $displayColumn}
|
<DataCell
|
||||||
<DataCell
|
{row}
|
||||||
{row}
|
{cellId}
|
||||||
{cellId}
|
{rowFocused}
|
||||||
{rowFocused}
|
{rowSelected}
|
||||||
{rowSelected}
|
cellSelected={$selectedCellMap[cellId]}
|
||||||
cellSelected={$selectedCellMap[cellId]}
|
highlighted={rowHovered || rowFocused}
|
||||||
highlighted={rowHovered || rowFocused}
|
rowIdx={row.__idx}
|
||||||
rowIdx={row.__idx}
|
topRow={idx === 0}
|
||||||
topRow={idx === 0}
|
focused={$focusedCellId === cellId}
|
||||||
focused={$focusedCellId === cellId}
|
selectedUser={$userCellMap[cellId]}
|
||||||
selectedUser={$userCellMap[cellId]}
|
width={$displayColumn.width}
|
||||||
width={$displayColumn.width}
|
column={$displayColumn}
|
||||||
column={$displayColumn}
|
contentLines={$contentLines}
|
||||||
contentLines={$contentLines}
|
isSelectingCells={$isSelectingCells}
|
||||||
isSelectingCells={$isSelectingCells}
|
/>
|
||||||
/>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
{#if $config.canAddRows}
|
||||||
{#if $config.canAddRows}
|
<div
|
||||||
<div
|
class="row blank"
|
||||||
class="row new"
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||||
on:mouseenter={$isDragging
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
? null
|
on:click={() => dispatch("add-row-inline")}
|
||||||
: () => ($hoveredRowId = BlankRowID)}
|
>
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
||||||
on:click={() => dispatch("add-row-inline")}
|
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||||
>
|
</GutterCell>
|
||||||
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
{#if $displayColumn}
|
||||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
<GridCell
|
||||||
</GutterCell>
|
width={$displayColumn.width}
|
||||||
{#if $displayColumn}
|
highlighted={$hoveredRowId === BlankRowID}
|
||||||
<GridCell
|
>
|
||||||
width={$displayColumn.width}
|
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
||||||
highlighted={$hoveredRowId === BlankRowID}
|
</GridCell>
|
||||||
>
|
{/if}
|
||||||
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
</div>
|
||||||
</GridCell>
|
{/if}
|
||||||
{/if}
|
</GridScrollWrapper>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -181,11 +177,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.content {
|
.blank :global(.cell:hover) {
|
||||||
position: relative;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.row.new :global(*:hover) {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</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 SmallRowHeight = 36
|
||||||
export const MediumRowHeight = 64
|
export const MediumRowHeight = 64
|
||||||
export const LargeRowHeight = 92
|
export const LargeRowHeight = 92
|
||||||
export const DefaultRowHeight = SmallRowHeight
|
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 NewRowID = "new"
|
||||||
export const BlankRowID = "blank"
|
export const BlankRowID = "blank"
|
||||||
export const RowPageSize = 100
|
export const RowPageSize = 100
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { tick } from "svelte"
|
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"
|
import { parseCellID } from "../lib/utils"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
|
@ -36,26 +42,15 @@ export const deriveStores = context => {
|
||||||
return ($displayColumn?.width || 0) + GutterWidth
|
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
|
// Derive horizontal limits
|
||||||
const contentWidth = derived(
|
const contentWidth = derived(
|
||||||
[visibleColumns, buttonColumnWidth],
|
[visibleColumns, buttonColumnWidth],
|
||||||
([$visibleColumns, $buttonColumnWidth]) => {
|
([$visibleColumns, $buttonColumnWidth]) => {
|
||||||
const space = Math.max(Padding, $buttonColumnWidth - 1)
|
let width = GutterWidth + $buttonColumnWidth
|
||||||
let width = GutterWidth + space
|
|
||||||
$visibleColumns.forEach(col => {
|
$visibleColumns.forEach(col => {
|
||||||
width += col.width
|
width += col.width
|
||||||
})
|
})
|
||||||
return width
|
return width + HPadding
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const screenWidth = derived(
|
const screenWidth = derived(
|
||||||
|
@ -70,14 +65,6 @@ export const deriveStores = context => {
|
||||||
return Math.max($contentWidth - $screenWidth, 0)
|
return Math.max($contentWidth - $screenWidth, 0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Derive whether to show scrollbars or not
|
|
||||||
const showVScrollbar = derived(
|
|
||||||
[contentHeight, height],
|
|
||||||
([$contentHeight, $height]) => {
|
|
||||||
return $contentHeight > $height
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const showHScrollbar = derived(
|
const showHScrollbar = derived(
|
||||||
[contentWidth, screenWidth],
|
[contentWidth, screenWidth],
|
||||||
([$contentWidth, $screenWidth]) => {
|
([$contentWidth, $screenWidth]) => {
|
||||||
|
@ -85,6 +72,28 @@ 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.max($contentHeight - $height, 0)
|
||||||
|
)
|
||||||
|
const showVScrollbar = derived(
|
||||||
|
[contentHeight, height],
|
||||||
|
([$contentHeight, $height]) => {
|
||||||
|
return $contentHeight > $height
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stickyWidth,
|
stickyWidth,
|
||||||
contentHeight,
|
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 () => {
|
it("should add a new column for an internal DB table", async () => {
|
||||||
const saveTableRequest: SaveTableRequest = {
|
const saveTableRequest: SaveTableRequest = {
|
||||||
...basicTable(),
|
...basicTable(),
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula"
|
import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
|
import { findDuplicateInternalColumns } from "@budibase/shared-core"
|
||||||
import { getTable } from "../getters"
|
import { getTable } from "../getters"
|
||||||
import { checkAutoColumns } from "./utils"
|
import { checkAutoColumns } from "./utils"
|
||||||
import * as viewsSdk from "../../views"
|
import * as viewsSdk from "../../views"
|
||||||
|
@ -44,6 +45,17 @@ export async function save(
|
||||||
if (hasTypeChanged(table, oldTable)) {
|
if (hasTypeChanged(table, oldTable)) {
|
||||||
throw new Error("A column type has changed.")
|
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
|
// check that subtypes have been maintained
|
||||||
table = checkAutoColumns(table, oldTable)
|
table = checkAutoColumns(table, oldTable)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from "./api"
|
export * from "./api"
|
||||||
export * from "./fields"
|
export * from "./fields"
|
||||||
|
export * from "./rows"
|
||||||
|
|
||||||
export const OperatorOptions = {
|
export const OperatorOptions = {
|
||||||
Equals: {
|
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> = {
|
const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.STRING]: true,
|
[FieldType.STRING]: true,
|
||||||
|
@ -51,3 +52,27 @@ export function canBeDisplayColumn(type: FieldType): boolean {
|
||||||
export function canBeSortColumn(type: FieldType): boolean {
|
export function canBeSortColumn(type: FieldType): boolean {
|
||||||
return !!allowSortColumnByType[type]
|
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