commit
98ca05d97f
|
@ -42,13 +42,14 @@
|
|||
<GridCreateViewButton />
|
||||
{/if}
|
||||
<GridManageAccessButton />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<GridRelationshipButton />
|
||||
{/if}
|
||||
<GridImportButton disabled={isUsersTable} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{:else}
|
||||
<GridImportButton />
|
||||
{/if}
|
||||
<GridExportButton />
|
||||
<GridFilterButton />
|
||||
<GridAddColumnModal />
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</script>
|
||||
|
||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||
Manage access
|
||||
Access
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</script>
|
||||
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Create view
|
||||
Add view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
api = {
|
||||
focus: () => open(),
|
||||
blur: () => close(),
|
||||
isActive: () => isOpen,
|
||||
onKeyDown,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
const cellAPI = {
|
||||
focus: () => api?.focus(),
|
||||
blur: () => api?.blur(),
|
||||
isActive: () => api?.isActive?.() ?? false,
|
||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
||||
isReadonly: () => readonly,
|
||||
getType: () => column.schema.type,
|
||||
|
@ -67,6 +68,7 @@
|
|||
{rowIdx}
|
||||
{focused}
|
||||
{selectedUser}
|
||||
{readonly}
|
||||
error={$error}
|
||||
on:click={() => focusedCellId.set(cellId)}
|
||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let rowIdx
|
||||
export let defaultHeight = false
|
||||
export let center = false
|
||||
export let readonly = false
|
||||
|
||||
$: style = getStyle(width, selectedUser)
|
||||
|
||||
|
@ -27,6 +28,7 @@
|
|||
class:focused
|
||||
class:error
|
||||
class:center
|
||||
class:readonly
|
||||
class:default-height={defaultHeight}
|
||||
class:selected-other={selectedUser != null}
|
||||
on:focus
|
||||
|
@ -121,7 +123,8 @@
|
|||
.cell:hover {
|
||||
cursor: default;
|
||||
}
|
||||
.cell.highlighted:not(.focused) {
|
||||
.cell.highlighted:not(.focused),
|
||||
.cell.focused.readonly {
|
||||
--cell-background: var(--cell-background-hover);
|
||||
}
|
||||
.cell.selected:not(.focused) {
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
<script>
|
||||
import { GutterWidth } from "../lib/constants"
|
||||
import { getContext } from "svelte"
|
||||
import { Checkbox, Icon } from "@budibase/bbui"
|
||||
import GridCell from "./GridCell.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let row
|
||||
export let rowFocused = false
|
||||
export let rowHovered = false
|
||||
export let rowSelected = false
|
||||
export let disableExpand = false
|
||||
export let disableNumber = false
|
||||
export let defaultHeight = false
|
||||
export let disabled = false
|
||||
|
||||
const { config, dispatch, selectedRows } = getContext("grid")
|
||||
const svelteDispatch = createEventDispatcher()
|
||||
|
||||
const select = () => {
|
||||
svelteDispatch("select")
|
||||
const id = row?._id
|
||||
if (id) {
|
||||
selectedRows.update(state => {
|
||||
let newState = {
|
||||
...state,
|
||||
[id]: !state[id],
|
||||
}
|
||||
if (!newState[id]) {
|
||||
delete newState[id]
|
||||
}
|
||||
return newState
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
svelteDispatch("expand")
|
||||
if (row) {
|
||||
dispatch("edit-row", row)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<GridCell
|
||||
width={GutterWidth}
|
||||
highlighted={rowFocused || rowHovered}
|
||||
selected={rowSelected}
|
||||
{defaultHeight}
|
||||
>
|
||||
<div class="gutter">
|
||||
{#if $$slots.default}
|
||||
<slot />
|
||||
{:else}
|
||||
<div
|
||||
on:click={select}
|
||||
class="checkbox"
|
||||
class:visible={$config.allowDeleteRows &&
|
||||
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} {disabled} />
|
||||
</div>
|
||||
{#if !disableNumber}
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.allowDeleteRows ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $config.allowExpandRows}
|
||||
<div
|
||||
class="expand"
|
||||
class:visible={!disableExpand && (rowFocused || rowHovered)}
|
||||
>
|
||||
<Icon name="Maximize" hoverable size="S" on:click={expand} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</GridCell>
|
||||
|
||||
<style>
|
||||
.gutter {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: var(--cell-padding);
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--cell-spacing);
|
||||
}
|
||||
.checkbox,
|
||||
.number {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.checkbox :global(.spectrum-Checkbox) {
|
||||
min-height: 0;
|
||||
height: 20px;
|
||||
}
|
||||
.checkbox :global(.spectrum-Checkbox-box) {
|
||||
margin: 3px 0 0 0;
|
||||
}
|
||||
.number {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.checkbox.visible,
|
||||
.number.visible {
|
||||
display: flex;
|
||||
}
|
||||
.expand {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.expand.visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
|
@ -20,6 +20,7 @@
|
|||
ui,
|
||||
columns,
|
||||
} = getContext("grid")
|
||||
|
||||
const bannedDisplayColumnTypes = [
|
||||
"link",
|
||||
"array",
|
||||
|
@ -100,7 +101,7 @@
|
|||
style="flex: 0 0 {column.width}px;"
|
||||
bind:this={anchor}
|
||||
class:disabled={$isReordering || $isResizing}
|
||||
class:sorted={sortedBy}
|
||||
class:sticky={idx === "sticky"}
|
||||
>
|
||||
<GridCell
|
||||
on:mousedown={onMouseDown}
|
||||
|
@ -194,6 +195,10 @@
|
|||
.header-cell {
|
||||
display: flex;
|
||||
}
|
||||
.header-cell:not(.sticky):hover,
|
||||
.header-cell:not(.sticky) :global(.cell:hover) {
|
||||
cursor: grab;
|
||||
}
|
||||
.header-cell.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -202,9 +207,6 @@
|
|||
gap: calc(2 * var(--cell-spacing));
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.header-cell.sorted :global(.cell) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -31,7 +31,9 @@
|
|||
isOpen = true
|
||||
await tick()
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(0, 0)
|
||||
if (value?.length > 100) {
|
||||
textarea.setSelectionRange(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
|
@ -43,6 +45,7 @@
|
|||
api = {
|
||||
focus: () => open(),
|
||||
blur: () => close(),
|
||||
isActive: () => isOpen,
|
||||
onKeyDown,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
api = {
|
||||
focus: open,
|
||||
blur: close,
|
||||
isActive: () => isOpen,
|
||||
onKeyDown,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -235,6 +235,7 @@
|
|||
api = {
|
||||
focus: open,
|
||||
blur: close,
|
||||
isActive: () => isOpen,
|
||||
onKeyDown,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
api = {
|
||||
focus: () => input?.focus(),
|
||||
blur: () => input?.blur(),
|
||||
isActive: () => active,
|
||||
onKeyDown,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
on:click={() => dispatch("add-column")}
|
||||
disabled={!$config.allowAddColumns}
|
||||
>
|
||||
Create column
|
||||
Add column
|
||||
</ActionButton>
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
icon="TableRowAddBottom"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => dispatch("add-row")}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
disabled={!loaded ||
|
||||
!$config.allowAddRows ||
|
||||
(!$columns.length && !$stickyColumn)}
|
||||
>
|
||||
Create row
|
||||
Add row
|
||||
</ActionButton>
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover } from "@budibase/bbui"
|
||||
import { DefaultColumnWidth } from "../lib/constants"
|
||||
|
||||
const { stickyColumn, columns } = getContext("grid")
|
||||
const smallSize = 120
|
||||
const mediumSize = DefaultColumnWidth
|
||||
const largeSize = DefaultColumnWidth * 1.5
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
|
||||
$: allSmall = allCols.every(col => col.width === smallSize)
|
||||
$: allMedium = allCols.every(col => col.width === mediumSize)
|
||||
$: allLarge = allCols.every(col => col.width === largeSize)
|
||||
$: custom = !allSmall && !allMedium && !allLarge
|
||||
$: sizeOptions = [
|
||||
{
|
||||
label: "Small",
|
||||
size: smallSize,
|
||||
selected: allSmall,
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
size: mediumSize,
|
||||
selected: allMedium,
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
size: largeSize,
|
||||
selected: allLarge,
|
||||
},
|
||||
]
|
||||
|
||||
const changeColumnWidth = async width => {
|
||||
columns.update(state => {
|
||||
state.forEach(column => {
|
||||
column.width = width
|
||||
})
|
||||
return state
|
||||
})
|
||||
if ($stickyColumn) {
|
||||
stickyColumn.update(state => ({
|
||||
...state,
|
||||
width,
|
||||
}))
|
||||
}
|
||||
await columns.actions.saveChanges()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<ActionButton
|
||||
icon="MoveLeftRight"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
disabled={!allCols.length}
|
||||
>
|
||||
Width
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<div class="content">
|
||||
{#each sizeOptions as option}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => changeColumnWidth(option.size)}
|
||||
selected={option.selected}
|
||||
>
|
||||
{option.label}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
{#if custom}
|
||||
<ActionButton selected={custom} quiet>Custom</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,8 @@
|
|||
<script>
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ActionButton,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import { Modal, ModalContent, Button, notifications } from "@budibase/bbui"
|
||||
import { getContext, onMount } from "svelte"
|
||||
|
||||
const { selectedRows, rows, config } = getContext("grid")
|
||||
const { selectedRows, rows, config, subscribe } = getContext("grid")
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -28,18 +23,21 @@
|
|||
await rows.actions.deleteRows(rowsToDelete)
|
||||
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
|
||||
</script>
|
||||
|
||||
{#if selectedRowCount}
|
||||
<div class="delete-button" data-ignore-click-outside="true">
|
||||
<ActionButton
|
||||
<Button
|
||||
icon="Delete"
|
||||
size="S"
|
||||
size="M"
|
||||
on:click={modal.show}
|
||||
disabled={!$config.allowEditRows}
|
||||
cta
|
||||
>
|
||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||
</ActionButton>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -57,16 +55,12 @@
|
|||
</Modal>
|
||||
|
||||
<style>
|
||||
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) {
|
||||
color: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
|
||||
.delete-button :global(.spectrum-Button:not(:disabled)) {
|
||||
background-color: var(--spectrum-global-color-red-400);
|
||||
border-color: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/
|
||||
/* color: var(--spectrum-global-color-gray-600);*/
|
||||
/*}*/
|
||||
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/
|
||||
/* border-color: var(--spectrum-global-color-gray-400);*/
|
||||
/*}*/
|
||||
.delete-button :global(.spectrum-Button:not(:disabled):hover) {
|
||||
background-color: var(--spectrum-global-color-red-500);
|
||||
border-color: var(--spectrum-global-color-red-500);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
selected={open || anyHidden}
|
||||
disabled={!$columns.length}
|
||||
>
|
||||
Hide columns
|
||||
Columns
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,13 +36,13 @@
|
|||
|
||||
<div bind:this={anchor}>
|
||||
<ActionButton
|
||||
icon="LineHeight"
|
||||
icon="MoveUpDown"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
>
|
||||
Row height
|
||||
Height
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||
|
||||
const { sort, visibleColumns, stickyColumn } = getContext("grid")
|
||||
const { sort, columns, stickyColumn } = getContext("grid")
|
||||
const orderOptions = [
|
||||
{ label: "A-Z", value: "ascending" },
|
||||
{ label: "Z-A", value: "descending" },
|
||||
|
@ -11,15 +11,24 @@
|
|||
let open = false
|
||||
let anchor
|
||||
|
||||
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
|
||||
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
|
||||
$: columnOptions = getColumnOptions($stickyColumn, $columns)
|
||||
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
|
||||
|
||||
const getColumnOptions = (stickyColumn, columns) => {
|
||||
let options = []
|
||||
if (stickyColumn) {
|
||||
options.push(stickyColumn.name)
|
||||
options.push({
|
||||
label: stickyColumn.label || stickyColumn.name,
|
||||
value: stickyColumn.name,
|
||||
})
|
||||
}
|
||||
return [...options, ...columns.map(col => col.name)]
|
||||
return [
|
||||
...options,
|
||||
...columns.map(col => ({
|
||||
label: col.label || col.name,
|
||||
value: col.name,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const updateSortColumn = e => {
|
||||
|
@ -37,13 +46,13 @@
|
|||
}
|
||||
|
||||
// Ensure we never have a sort column selected that is not visible
|
||||
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
|
||||
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
|
||||
if (!sortColumn) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
sortColumn !== stickyColumn?.name &&
|
||||
!visibleColumns.some(col => col.name === sortColumn)
|
||||
!columns.some(col => col.name === sortColumn)
|
||||
) {
|
||||
if (stickyColumn) {
|
||||
sort.update(state => ({
|
||||
|
@ -53,7 +62,7 @@
|
|||
} else {
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
column: visibleColumns[0]?.name,
|
||||
column: columns[0]?.name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +75,7 @@
|
|||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open || $sort.column}
|
||||
selected={open}
|
||||
disabled={!columnOptions.length}
|
||||
>
|
||||
Sort
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
||||
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||
import NewRow from "./NewRow.svelte"
|
||||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
|
@ -110,6 +112,7 @@
|
|||
<AddRowButton />
|
||||
<AddColumnButton />
|
||||
<slot name="controls" />
|
||||
<ColumnWidthButton />
|
||||
<RowHeightButton />
|
||||
<HideColumnsButton />
|
||||
<SortButton />
|
||||
|
@ -127,10 +130,11 @@
|
|||
<HeaderRow />
|
||||
<GridBody />
|
||||
</div>
|
||||
<BetaButton />
|
||||
<NewRow />
|
||||
<div class="overlays">
|
||||
<ResizeOverlay />
|
||||
<ReorderOverlay />
|
||||
<BetaButton />
|
||||
<ScrollOverlay />
|
||||
<MenuOverlay />
|
||||
</div>
|
||||
|
|
|
@ -2,11 +2,25 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import GridRow from "./GridRow.svelte"
|
||||
import { BlankRowID } from "../lib/constants"
|
||||
|
||||
const { bounds, renderedRows, rowVerticalInversionIndex } = getContext("grid")
|
||||
const {
|
||||
bounds,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
rowVerticalInversionIndex,
|
||||
config,
|
||||
hoveredRowId,
|
||||
dispatch,
|
||||
} = getContext("grid")
|
||||
|
||||
let body
|
||||
|
||||
$: renderColumnsWidth = $renderedColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
// Observe and record the height of the body
|
||||
const observer = new ResizeObserver(() => {
|
||||
|
@ -24,6 +38,16 @@
|
|||
{#each $renderedRows as row, idx}
|
||||
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
||||
{/each}
|
||||
{#if $config.allowAddRows && $renderedColumns.length}
|
||||
<div
|
||||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
style="width:{renderColumnsWidth}px"
|
||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
/>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
|
||||
|
@ -35,4 +59,15 @@
|
|||
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;
|
||||
}
|
||||
.blank.highlighted {
|
||||
background: var(--cell-background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,10 +29,7 @@
|
|||
// Handles a wheel even and updates the scroll offsets
|
||||
const handleWheel = e => {
|
||||
e.preventDefault()
|
||||
const modifier = e.ctrlKey || e.metaKey
|
||||
let x = modifier ? e.deltaY : e.deltaX
|
||||
let y = modifier ? e.deltaX : e.deltaY
|
||||
debouncedHandleWheel(x, y, e.clientY)
|
||||
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
||||
}
|
||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||
const { top, left } = $scroll
|
||||
|
|
|
@ -2,8 +2,17 @@
|
|||
import { getContext } from "svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
const { renderedColumns } = getContext("grid")
|
||||
const { renderedColumns, dispatch, scroll, hiddenColumnsWidth, width } =
|
||||
getContext("grid")
|
||||
|
||||
$: columnsWidth = $renderedColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
|
||||
$: left = Math.min($width - 40, end)
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
|
@ -14,6 +23,13 @@
|
|||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
<div
|
||||
class="add"
|
||||
style="left:{left}px"
|
||||
on:click={() => dispatch("add-column")}
|
||||
>
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -27,4 +43,20 @@
|
|||
.row {
|
||||
display: flex;
|
||||
}
|
||||
.add {
|
||||
height: var(--default-row-height);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-left: var(--cell-border);
|
||||
border-right: var(--cell-border);
|
||||
border-bottom: var(--cell-border);
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.add:hover {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
<script>
|
||||
import { getContext, onDestroy, onMount, tick } from "svelte"
|
||||
import { Icon, Button } from "@budibase/bbui"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { GutterWidth } from "../lib/constants"
|
||||
import { NewRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
|
||||
const {
|
||||
hoveredRowId,
|
||||
focusedCellId,
|
||||
stickyColumn,
|
||||
scroll,
|
||||
dispatch,
|
||||
rows,
|
||||
focusedCellAPI,
|
||||
tableId,
|
||||
subscribe,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
rowHeight,
|
||||
hasNextPage,
|
||||
maxScrollTop,
|
||||
rowVerticalInversionIndex,
|
||||
columnHorizontalInversionIndex,
|
||||
} = getContext("grid")
|
||||
|
||||
let isAdding = false
|
||||
let newRow = {}
|
||||
let offset = 0
|
||||
|
||||
$: firstColumn = $stickyColumn || $renderedColumns[0]
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $tableId, (isAdding = false)
|
||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||
|
||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
||||
if (offset === 0) {
|
||||
return false
|
||||
}
|
||||
return rows.length >= inversionIndex
|
||||
}
|
||||
|
||||
const addRow = async () => {
|
||||
// Blur the active cell and tick to let final value updates propagate
|
||||
$focusedCellAPI?.blur()
|
||||
await tick()
|
||||
|
||||
// Create row
|
||||
const newRowIndex = offset ? undefined : 0
|
||||
const savedRow = await rows.actions.addRow(newRow, newRowIndex)
|
||||
if (savedRow) {
|
||||
// Reset state
|
||||
clear()
|
||||
|
||||
// Select the first cell if possible
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
isAdding = false
|
||||
$focusedCellId = null
|
||||
$hoveredRowId = null
|
||||
document.removeEventListener("keydown", handleKeyPress)
|
||||
}
|
||||
|
||||
const startAdding = async () => {
|
||||
if (isAdding) {
|
||||
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 {
|
||||
scroll.update(state => ({
|
||||
...state,
|
||||
top: $maxScrollTop,
|
||||
}))
|
||||
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
|
||||
if ($renderedRows.length !== 0) {
|
||||
offset -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// Update state and select initial cell
|
||||
newRow = {}
|
||||
isAdding = true
|
||||
$hoveredRowId = NewRowID
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${NewRowID}-${firstColumn.name}`
|
||||
}
|
||||
|
||||
// Attach key listener
|
||||
document.addEventListener("keydown", handleKeyPress)
|
||||
}
|
||||
|
||||
const updateValue = (rowId, columnName, val) => {
|
||||
newRow[columnName] = val
|
||||
}
|
||||
|
||||
const addViaModal = () => {
|
||||
clear()
|
||||
dispatch("add-row")
|
||||
}
|
||||
|
||||
const handleKeyPress = e => {
|
||||
if (!isAdding) {
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
// Only close the new row component if we aren't actively inside a cell
|
||||
if (!$focusedCellAPI?.isActive()) {
|
||||
e.preventDefault()
|
||||
clear()
|
||||
}
|
||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
addRow()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => subscribe("add-row-inline", startAdding))
|
||||
onDestroy(() => {
|
||||
document.removeEventListener("keydown", handleKeyPress)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if isAdding}
|
||||
<div
|
||||
class="container"
|
||||
class:floating={offset > 0}
|
||||
style="--offset:{offset}px; --sticky-width:{width}px;"
|
||||
>
|
||||
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
|
||||
<div class="underlay" transition:fade={{ duration: 130 }} />
|
||||
<div class="sticky-column" transition:fade={{ duration: 130 }}>
|
||||
<GutterCell on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
</GutterCell>
|
||||
{#if $stickyColumn}
|
||||
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
rowFocused
|
||||
column={$stickyColumn}
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={$stickyColumn.width}
|
||||
{updateValue}
|
||||
rowIdx={0}
|
||||
{invertY}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="normal-columns" transition:fade={{ duration: 130 }}>
|
||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
||||
<div class="row">
|
||||
{#each $renderedColumns as column, columnIdx}
|
||||
{@const cellId = `new-${column.name}`}
|
||||
{#key cellId}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
{updateValue}
|
||||
rowFocused
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={column.width}
|
||||
rowIdx={0}
|
||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
||||
{invertY}
|
||||
/>
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
<div class="buttons" transition:fade={{ duration: 130 }}>
|
||||
<Button size="M" cta on:click={addRow}>Save</Button>
|
||||
<Button size="M" secondary newStyles on:click={clear}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: absolute;
|
||||
top: var(--default-row-height);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
.container :global(.cell) {
|
||||
--cell-background: var(--spectrum-global-color-gray-75) !important;
|
||||
}
|
||||
.container.floating :global(.cell) {
|
||||
height: calc(var(--row-height) + 1px);
|
||||
border-top: var(--cell-border);
|
||||
}
|
||||
|
||||
/* Underlay sits behind everything */
|
||||
.underlay {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--cell-background);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.underlay.sticky {
|
||||
z-index: 2;
|
||||
width: var(--sticky-width);
|
||||
}
|
||||
|
||||
/* Floating buttons which sit on top of the underlay but below the sticky column */
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
pointer-events: all;
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: calc(var(--row-height) + var(--offset) + 24px);
|
||||
left: var(--gutter-width);
|
||||
}
|
||||
|
||||
/* Sticky column styles */
|
||||
.sticky-column {
|
||||
display: flex;
|
||||
z-index: 4;
|
||||
position: relative;
|
||||
align-self: flex-start;
|
||||
flex: 0 0 var(--sticky-width);
|
||||
}
|
||||
.sticky-column :global(.cell:not(:last-child)) {
|
||||
border-right: none;
|
||||
}
|
||||
.sticky-column,
|
||||
.normal-columns {
|
||||
margin-top: var(--offset);
|
||||
}
|
||||
|
||||
/* Normal column styles */
|
||||
.row {
|
||||
width: 0;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
|
@ -1,247 +0,0 @@
|
|||
<script>
|
||||
import GridCell from "../cells/GridCell.svelte"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { Icon, Button } from "@budibase/bbui"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
import { GutterWidth } from "../lib/constants"
|
||||
|
||||
const {
|
||||
hoveredRowId,
|
||||
focusedCellId,
|
||||
stickyColumn,
|
||||
scroll,
|
||||
config,
|
||||
dispatch,
|
||||
visibleColumns,
|
||||
rows,
|
||||
showHScrollbar,
|
||||
tableId,
|
||||
subscribe,
|
||||
scrollLeft,
|
||||
} = getContext("grid")
|
||||
|
||||
let isAdding = false
|
||||
let newRow = {}
|
||||
let touched = false
|
||||
|
||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||
$: rowHovered = $hoveredRowId === "new"
|
||||
$: rowFocused = $focusedCellId?.startsWith("new-")
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $tableId, (isAdding = false)
|
||||
|
||||
const addRow = async () => {
|
||||
// Create row
|
||||
const savedRow = await rows.actions.addRow(newRow, 0)
|
||||
if (savedRow) {
|
||||
// Select the first cell if possible
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
}
|
||||
|
||||
// Reset state
|
||||
isAdding = false
|
||||
scroll.set({
|
||||
left: 0,
|
||||
top: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
isAdding = false
|
||||
}
|
||||
|
||||
const startAdding = () => {
|
||||
newRow = {}
|
||||
isAdding = true
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `new-${firstColumn.name}`
|
||||
}
|
||||
}
|
||||
|
||||
const updateValue = (rowId, columnName, val) => {
|
||||
touched = true
|
||||
newRow[columnName] = val
|
||||
}
|
||||
|
||||
const addViaModal = () => {
|
||||
isAdding = false
|
||||
dispatch("add-row")
|
||||
}
|
||||
|
||||
onMount(() => subscribe("add-row-inline", startAdding))
|
||||
</script>
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if isAdding}
|
||||
<div class="container" transition:fly={{ y: 20, duration: 130 }}>
|
||||
<div class="content" class:above-scrollbar={$showHScrollbar}>
|
||||
<div
|
||||
class="new-row"
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
>
|
||||
<div
|
||||
class="sticky-column"
|
||||
style="flex: 0 0 {width}px"
|
||||
class:scrolled={$scrollLeft > 0}
|
||||
>
|
||||
<GridCell width={GutterWidth} {rowHovered} {rowFocused}>
|
||||
<div class="gutter">
|
||||
<div class="number">1</div>
|
||||
{#if $config.allowExpandRows}
|
||||
<Icon
|
||||
name="Maximize"
|
||||
size="S"
|
||||
hoverable
|
||||
on:click={addViaModal}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</GridCell>
|
||||
{#if $stickyColumn}
|
||||
{@const cellId = `new-${$stickyColumn.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
column={$stickyColumn}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
focused={$focusedCellId === cellId}
|
||||
{rowFocused}
|
||||
width={$stickyColumn.width}
|
||||
{updateValue}
|
||||
rowIdx={0}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
||||
<div class="row">
|
||||
{#each $visibleColumns as column}
|
||||
{@const cellId = `new-${column.name}`}
|
||||
{#key cellId}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
focused={$focusedCellId === cellId}
|
||||
{rowFocused}
|
||||
width={column.width}
|
||||
{updateValue}
|
||||
rowIdx={0}
|
||||
/>
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Button size="M" cta on:click={addRow}>Save</Button>
|
||||
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: var(--row-height);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.container:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--cell-background);
|
||||
opacity: 0.8;
|
||||
z-index: -1;
|
||||
}
|
||||
.content {
|
||||
pointer-events: all;
|
||||
background: var(--background);
|
||||
border-bottom: var(--cell-border);
|
||||
}
|
||||
|
||||
.new-row {
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transition: margin-bottom 130ms ease-out;
|
||||
}
|
||||
.new-row :global(.cell) {
|
||||
--cell-background: var(--background) !important;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sticky-column {
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
/* Don't show borders between cells in the sticky column */
|
||||
.sticky-column :global(.cell:not(:last-child)) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Add shadow when scrolled */
|
||||
.sticky-column.scrolled {
|
||||
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
|
||||
}
|
||||
.sticky-column.scrolled:after {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
|
||||
left: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Styles for gutter */
|
||||
.gutter {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: var(--cell-padding);
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--cell-spacing);
|
||||
}
|
||||
|
||||
/* Floating buttons */
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin: 24px 0 0 var(--gutter-width);
|
||||
pointer-events: all;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
</style>
|
|
@ -1,24 +1,26 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Checkbox, Icon } from "@budibase/bbui"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import GridCell from "../cells/GridCell.svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||
import { GutterWidth } from "../lib/constants"
|
||||
import { GutterWidth, BlankRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
|
||||
const {
|
||||
rows,
|
||||
selectedRows,
|
||||
stickyColumn,
|
||||
renderedColumns,
|
||||
renderedRows,
|
||||
focusedCellId,
|
||||
hoveredRowId,
|
||||
config,
|
||||
selectedCellMap,
|
||||
focusedRow,
|
||||
dispatch,
|
||||
scrollLeft,
|
||||
dispatch,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowCount = $rows.length
|
||||
|
@ -37,19 +39,6 @@
|
|||
$selectedRows = allRows
|
||||
}
|
||||
}
|
||||
|
||||
const selectRow = id => {
|
||||
selectedRows.update(state => {
|
||||
let newState = {
|
||||
...state,
|
||||
[id]: !state[id],
|
||||
}
|
||||
if (!newState[id]) {
|
||||
delete newState[id]
|
||||
}
|
||||
return newState
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -58,26 +47,14 @@
|
|||
class:scrolled={$scrollLeft > 0}
|
||||
>
|
||||
<div class="header row">
|
||||
<GridCell width={GutterWidth} defaultHeight center>
|
||||
<div class="gutter">
|
||||
<div class="checkbox visible">
|
||||
{#if $config.allowDeleteRows}
|
||||
<div on:click={selectAll}>
|
||||
<Checkbox
|
||||
value={rowCount && selectedRowCount === rowCount}
|
||||
disabled={!$renderedRows.length}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $config.allowExpandRows}
|
||||
<div class="expand">
|
||||
<Icon name="Maximize" size="S" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</GridCell>
|
||||
|
||||
<GutterCell
|
||||
disableExpand
|
||||
disableNumber
|
||||
on:select={selectAll}
|
||||
defaultHeight
|
||||
rowSelected={selectedRowCount && selectedRowCount === rowCount}
|
||||
disabled={!$renderedRows.length}
|
||||
/>
|
||||
{#if $stickyColumn}
|
||||
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
||||
{/if}
|
||||
|
@ -95,41 +72,7 @@
|
|||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
>
|
||||
<GridCell
|
||||
width={GutterWidth}
|
||||
highlighted={rowFocused || rowHovered}
|
||||
selected={rowSelected}
|
||||
>
|
||||
<div class="gutter">
|
||||
<div
|
||||
on:click={() => selectRow(row._id)}
|
||||
class="checkbox"
|
||||
class:visible={$config.allowDeleteRows &&
|
||||
(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} />
|
||||
</div>
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.allowDeleteRows ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
</div>
|
||||
{#if $config.allowExpandRows}
|
||||
<div class="expand" class:visible={rowFocused || rowHovered}>
|
||||
<Icon
|
||||
name="Maximize"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => {
|
||||
dispatch("edit-row", row)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</GridCell>
|
||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||
{#if $stickyColumn}
|
||||
<DataCell
|
||||
{row}
|
||||
|
@ -146,6 +89,24 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
|
||||
<div
|
||||
class="row new"
|
||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
>
|
||||
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
</GutterCell>
|
||||
{#if $stickyColumn}
|
||||
<GridCell
|
||||
width={$stickyColumn.width}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -156,6 +117,7 @@
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: var(--cell-background);
|
||||
}
|
||||
|
||||
/* Add right border */
|
||||
|
@ -203,43 +165,7 @@
|
|||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* Styles for gutter */
|
||||
.gutter {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: var(--cell-padding);
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--cell-spacing);
|
||||
}
|
||||
.checkbox,
|
||||
.number {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.checkbox :global(.spectrum-Checkbox) {
|
||||
min-height: 0;
|
||||
height: 20px;
|
||||
}
|
||||
.checkbox :global(.spectrum-Checkbox-box) {
|
||||
margin: 3px 0 0 0;
|
||||
}
|
||||
.number {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.checkbox.visible,
|
||||
.number.visible {
|
||||
display: flex;
|
||||
}
|
||||
.expand {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.expand.visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
.row.new :global(*:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
export const Padding = 276
|
||||
export const Padding = 128
|
||||
export const MaxCellRenderHeight = 252
|
||||
export const MaxCellRenderWidthOverflow = 200
|
||||
export const ScrollBarSize = 8
|
||||
export const GutterWidth = 72
|
||||
export const DefaultColumnWidth = 200
|
||||
export const MinColumnWidth = 100
|
||||
export const MinColumnWidth = 80
|
||||
export const SmallRowHeight = 36
|
||||
export const MediumRowHeight = 64
|
||||
export const LargeRowHeight = 92
|
||||
export const DefaultRowHeight = SmallRowHeight
|
||||
export const NewRowID = "new"
|
||||
export const BlankRowID = "blank"
|
||||
export const RowPageSize = 100
|
||||
|
|
|
@ -15,7 +15,7 @@ const TypeIconMap = {
|
|||
number: "123",
|
||||
boolean: "Boolean",
|
||||
attachment: "AppleFiles",
|
||||
link: "Link",
|
||||
link: "DataCorrelated",
|
||||
formula: "Calculator",
|
||||
json: "Brackets",
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
import { NewRowID } from "../lib/constants"
|
||||
|
||||
const {
|
||||
enrichedRows,
|
||||
|
@ -10,15 +11,24 @@
|
|||
stickyColumn,
|
||||
focusedCellAPI,
|
||||
clipboard,
|
||||
dispatch,
|
||||
selectedRows,
|
||||
} = getContext("grid")
|
||||
|
||||
// Global key listener which intercepts all key events
|
||||
const handleKeyDown = e => {
|
||||
// If nothing selected avoid processing further key presses
|
||||
if (!$focusedCellId) {
|
||||
if (e.key === "Tab") {
|
||||
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
|
||||
e.preventDefault()
|
||||
focusFirstCell()
|
||||
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
dispatch("add-row-inline")
|
||||
} else if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (Object.keys($selectedRows).length) {
|
||||
dispatch("request-bulk-delete")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -26,10 +36,19 @@
|
|||
// Always intercept certain key presses
|
||||
const api = $focusedCellAPI
|
||||
if (e.key === "Escape") {
|
||||
api?.blur?.()
|
||||
// By setting a tiny timeout here we can ensure that other listeners
|
||||
// which depend on being able to read cell state on an escape keypress
|
||||
// get a chance to observe the true state before we blur
|
||||
if (api?.isActive()) {
|
||||
setTimeout(api?.blur, 10)
|
||||
} else {
|
||||
$focusedCellId = null
|
||||
}
|
||||
return
|
||||
} else if (e.key === "Tab") {
|
||||
api?.blur?.()
|
||||
changeFocusedColumn(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Pass the key event to the selected cell and let it decide whether to
|
||||
|
@ -54,8 +73,12 @@
|
|||
clipboard.actions.copy()
|
||||
break
|
||||
case "v":
|
||||
clipboard.actions.paste()
|
||||
if (!api?.isReadonly()) {
|
||||
clipboard.actions.paste()
|
||||
}
|
||||
break
|
||||
case "Enter":
|
||||
dispatch("add-row-inline")
|
||||
}
|
||||
} else {
|
||||
switch (e.key) {
|
||||
|
@ -73,11 +96,19 @@
|
|||
break
|
||||
case "Delete":
|
||||
case "Backspace":
|
||||
deleteSelectedCell()
|
||||
if (Object.keys($selectedRows).length) {
|
||||
dispatch("request-bulk-delete")
|
||||
} else {
|
||||
deleteSelectedCell()
|
||||
}
|
||||
break
|
||||
case "Enter":
|
||||
focusCell()
|
||||
break
|
||||
case " ":
|
||||
case "Space":
|
||||
toggleSelectRow()
|
||||
break
|
||||
default:
|
||||
startEnteringValue(e.key, e.which)
|
||||
}
|
||||
|
@ -156,7 +187,7 @@
|
|||
|
||||
// Focuses the cell and starts entering a new value
|
||||
const startEnteringValue = (key, keyCode) => {
|
||||
if ($focusedCellAPI) {
|
||||
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
|
||||
const type = $focusedCellAPI.getType()
|
||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
||||
$focusedCellAPI.setValue(parseInt(key))
|
||||
|
@ -171,6 +202,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
const toggleSelectRow = () => {
|
||||
const id = $focusedRow?._id
|
||||
if (!id || id === NewRowID) {
|
||||
return
|
||||
}
|
||||
selectedRows.update(state => {
|
||||
state[id] = !state[id]
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
return () => {
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
copiedCell,
|
||||
clipboard,
|
||||
dispatch,
|
||||
focusedCellAPI,
|
||||
} = getContext("grid")
|
||||
|
||||
$: style = makeStyle($menu)
|
||||
|
@ -49,7 +50,7 @@
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Paste"
|
||||
disabled={$copiedCell == null}
|
||||
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
|
||||
on:click={clipboard.actions.paste}
|
||||
on:click={menu.actions.close}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { fetchData } from "../../../fetch/fetchData"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||
|
||||
const initialSortState = {
|
||||
column: null,
|
||||
|
@ -16,6 +17,7 @@ export const createStores = () => {
|
|||
const sort = writable(initialSortState)
|
||||
const rowChangeCache = writable({})
|
||||
const inProgressChanges = writable({})
|
||||
const hasNextPage = writable(false)
|
||||
|
||||
// Generate a lookup map to quick find a row by ID
|
||||
const rowLookupMap = derived(
|
||||
|
@ -50,6 +52,7 @@ export const createStores = () => {
|
|||
sort,
|
||||
rowChangeCache,
|
||||
inProgressChanges,
|
||||
hasNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +73,7 @@ export const deriveStores = context => {
|
|||
rowChangeCache,
|
||||
inProgressChanges,
|
||||
previousFocusedRowId,
|
||||
hasNextPage,
|
||||
} = context
|
||||
const instanceLoaded = writable(false)
|
||||
const fetch = writable(null)
|
||||
|
@ -114,7 +118,7 @@ export const deriveStores = context => {
|
|||
filter: [],
|
||||
sortColumn: initialSortState.column,
|
||||
sortOrder: initialSortState.order,
|
||||
limit: 100,
|
||||
limit: RowPageSize,
|
||||
paginate: true,
|
||||
},
|
||||
})
|
||||
|
@ -122,6 +126,7 @@ export const deriveStores = context => {
|
|||
// Subscribe to changes of this fetch model
|
||||
unsubscribe = newFetch.subscribe($fetch => {
|
||||
if ($fetch.loaded && !$fetch.loading) {
|
||||
hasNextPage.set($fetch.hasNextPage)
|
||||
const $instanceLoaded = get(instanceLoaded)
|
||||
const resetRows = $fetch.resetKey !== lastResetKey
|
||||
lastResetKey = $fetch.resetKey
|
||||
|
@ -230,7 +235,7 @@ export const deriveStores = context => {
|
|||
if (bubble) {
|
||||
throw error
|
||||
} else {
|
||||
handleValidationError("new", error)
|
||||
handleValidationError(NewRowID, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tick } from "svelte"
|
||||
import { Padding, GutterWidth, DefaultRowHeight } from "../lib/constants"
|
||||
import { Padding, GutterWidth } from "../lib/constants"
|
||||
|
||||
export const createStores = () => {
|
||||
const scroll = writable({
|
||||
|
@ -29,7 +29,7 @@ export const deriveStores = context => {
|
|||
// Derive vertical limits
|
||||
const contentHeight = derived(
|
||||
[rows, rowHeight],
|
||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding,
|
||||
([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
|
||||
0
|
||||
)
|
||||
const maxScrollTop = derived(
|
||||
|
@ -138,7 +138,7 @@ export const initialise = context => {
|
|||
const $scroll = get(scroll)
|
||||
const $bounds = get(bounds)
|
||||
const $rowHeight = get(rowHeight)
|
||||
const verticalOffset = DefaultRowHeight * 1.5
|
||||
const verticalOffset = 60
|
||||
|
||||
// Ensure vertical position is viewable
|
||||
if ($focusedRow) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { tick } from "svelte"
|
||||
import {
|
||||
DefaultRowHeight,
|
||||
LargeRowHeight,
|
||||
MediumRowHeight,
|
||||
NewRowID,
|
||||
} from "../lib/constants"
|
||||
|
||||
export const createStores = () => {
|
||||
|
@ -49,14 +51,14 @@ export const deriveStores = context => {
|
|||
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
|
||||
const rowId = $focusedCellId?.split("-")[0]
|
||||
|
||||
if (rowId === "new") {
|
||||
// Edge case for new row
|
||||
return { _id: rowId }
|
||||
} else {
|
||||
// All normal rows
|
||||
const index = $rowLookupMap[rowId]
|
||||
return $enrichedRows[index]
|
||||
// Edge case for new rows
|
||||
if (rowId === NewRowID) {
|
||||
return { _id: NewRowID }
|
||||
}
|
||||
|
||||
// All normal rows
|
||||
const index = $rowLookupMap[rowId]
|
||||
return $enrichedRows[index]
|
||||
},
|
||||
null
|
||||
)
|
||||
|
@ -101,7 +103,10 @@ export const initialise = context => {
|
|||
} = context
|
||||
|
||||
// Ensure we clear invalid rows from state if they disappear
|
||||
rows.subscribe(() => {
|
||||
rows.subscribe(async () => {
|
||||
// We tick here to ensure other derived stores have properly updated.
|
||||
// We depend on the row lookup map which is a derived store,
|
||||
await tick()
|
||||
const $focusedCellId = get(focusedCellId)
|
||||
const $selectedRows = get(selectedRows)
|
||||
const $hoveredRowId = get(hoveredRowId)
|
||||
|
@ -140,20 +145,6 @@ export const initialise = context => {
|
|||
lastFocusedRowId = id
|
||||
})
|
||||
|
||||
// Reset selected rows when selected cell changes
|
||||
focusedCellId.subscribe(id => {
|
||||
if (id) {
|
||||
selectedRows.set({})
|
||||
}
|
||||
})
|
||||
|
||||
// Unset selected cell when rows are selected
|
||||
selectedRows.subscribe(rows => {
|
||||
if (Object.keys(rows || {}).length) {
|
||||
focusedCellId.set(null)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove hovered row when a cell is selected
|
||||
focusedCellId.subscribe(cell => {
|
||||
if (cell && get(hoveredRowId)) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
|
|||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
MinColumnWidth,
|
||||
ScrollBarSize,
|
||||
} from "../lib/constants"
|
||||
|
||||
|
@ -45,11 +46,16 @@ export const deriveStores = context => {
|
|||
)
|
||||
|
||||
// Derive visible columns
|
||||
const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
|
||||
const interval = MinColumnWidth
|
||||
return Math.round($scrollLeft / interval) * interval
|
||||
})
|
||||
const renderedColumns = derived(
|
||||
[visibleColumns, scrollLeft, width],
|
||||
([$visibleColumns, $scrollLeft, $width]) => {
|
||||
[visibleColumns, scrollLeftRounded, width],
|
||||
([$visibleColumns, $scrollLeft, $width], set) => {
|
||||
if (!$visibleColumns.length) {
|
||||
return []
|
||||
set([])
|
||||
return
|
||||
}
|
||||
let startColIdx = 0
|
||||
let rightEdge = $visibleColumns[0].width
|
||||
|
@ -69,19 +75,17 @@ export const deriveStores = context => {
|
|||
leftEdge += $visibleColumns[endColIdx].width
|
||||
endColIdx++
|
||||
}
|
||||
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx)
|
||||
|
||||
// Cautiously shrink the number of rendered columns.
|
||||
// This is to avoid rapidly shrinking and growing the visible column count
|
||||
// which results in remounting cells
|
||||
const currentCount = get(renderedColumns).length
|
||||
if (currentCount === nextRenderedColumns.length + 1) {
|
||||
return $visibleColumns.slice(startColIdx, endColIdx + 1)
|
||||
} else {
|
||||
return nextRenderedColumns
|
||||
// Render an additional column on either side to account for
|
||||
// debounce column updates based on scroll position
|
||||
const next = $visibleColumns.slice(
|
||||
Math.max(0, startColIdx - 1),
|
||||
endColIdx + 1
|
||||
)
|
||||
const current = get(renderedColumns)
|
||||
if (JSON.stringify(next) !== JSON.stringify(current)) {
|
||||
set(next)
|
||||
}
|
||||
},
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
const hiddenColumnsWidth = derived(
|
||||
|
|
Loading…
Reference in New Issue