commit
98ca05d97f
|
@ -42,13 +42,14 @@
|
||||||
<GridCreateViewButton />
|
<GridCreateViewButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridManageAccessButton />
|
<GridManageAccessButton />
|
||||||
{#if isUsersTable}
|
|
||||||
<EditRolesButton />
|
|
||||||
{/if}
|
|
||||||
{#if !isInternal}
|
{#if !isInternal}
|
||||||
<GridRelationshipButton />
|
<GridRelationshipButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridImportButton disabled={isUsersTable} />
|
{#if isUsersTable}
|
||||||
|
<EditRolesButton />
|
||||||
|
{:else}
|
||||||
|
<GridImportButton />
|
||||||
|
{/if}
|
||||||
<GridExportButton />
|
<GridExportButton />
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
<GridAddColumnModal />
|
<GridAddColumnModal />
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||||
Manage access
|
Access
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ManageAccessModal
|
<ManageAccessModal
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||||
Create view
|
Add view
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateViewModal />
|
<CreateViewModal />
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
blur: () => close(),
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
const cellAPI = {
|
const cellAPI = {
|
||||||
focus: () => api?.focus(),
|
focus: () => api?.focus(),
|
||||||
blur: () => api?.blur(),
|
blur: () => api?.blur(),
|
||||||
|
isActive: () => api?.isActive?.() ?? false,
|
||||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
onKeyDown: (...params) => api?.onKeyDown(...params),
|
||||||
isReadonly: () => readonly,
|
isReadonly: () => readonly,
|
||||||
getType: () => column.schema.type,
|
getType: () => column.schema.type,
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
{rowIdx}
|
{rowIdx}
|
||||||
{focused}
|
{focused}
|
||||||
{selectedUser}
|
{selectedUser}
|
||||||
|
{readonly}
|
||||||
error={$error}
|
error={$error}
|
||||||
on:click={() => focusedCellId.set(cellId)}
|
on:click={() => focusedCellId.set(cellId)}
|
||||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let rowIdx
|
export let rowIdx
|
||||||
export let defaultHeight = false
|
export let defaultHeight = false
|
||||||
export let center = false
|
export let center = false
|
||||||
|
export let readonly = false
|
||||||
|
|
||||||
$: style = getStyle(width, selectedUser)
|
$: style = getStyle(width, selectedUser)
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
class:focused
|
class:focused
|
||||||
class:error
|
class:error
|
||||||
class:center
|
class:center
|
||||||
|
class:readonly
|
||||||
class:default-height={defaultHeight}
|
class:default-height={defaultHeight}
|
||||||
class:selected-other={selectedUser != null}
|
class:selected-other={selectedUser != null}
|
||||||
on:focus
|
on:focus
|
||||||
|
@ -121,7 +123,8 @@
|
||||||
.cell:hover {
|
.cell:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.cell.highlighted:not(.focused) {
|
.cell.highlighted:not(.focused),
|
||||||
|
.cell.focused.readonly {
|
||||||
--cell-background: var(--cell-background-hover);
|
--cell-background: var(--cell-background-hover);
|
||||||
}
|
}
|
||||||
.cell.selected:not(.focused) {
|
.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,
|
ui,
|
||||||
columns,
|
columns,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
const bannedDisplayColumnTypes = [
|
const bannedDisplayColumnTypes = [
|
||||||
"link",
|
"link",
|
||||||
"array",
|
"array",
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
style="flex: 0 0 {column.width}px;"
|
style="flex: 0 0 {column.width}px;"
|
||||||
bind:this={anchor}
|
bind:this={anchor}
|
||||||
class:disabled={$isReordering || $isResizing}
|
class:disabled={$isReordering || $isResizing}
|
||||||
class:sorted={sortedBy}
|
class:sticky={idx === "sticky"}
|
||||||
>
|
>
|
||||||
<GridCell
|
<GridCell
|
||||||
on:mousedown={onMouseDown}
|
on:mousedown={onMouseDown}
|
||||||
|
@ -194,6 +195,10 @@
|
||||||
.header-cell {
|
.header-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.header-cell:not(.sticky):hover,
|
||||||
|
.header-cell:not(.sticky) :global(.cell:hover) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
.header-cell.disabled {
|
.header-cell.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -202,9 +207,6 @@
|
||||||
gap: calc(2 * var(--cell-spacing));
|
gap: calc(2 * var(--cell-spacing));
|
||||||
background: var(--spectrum-global-color-gray-100);
|
background: var(--spectrum-global-color-gray-100);
|
||||||
}
|
}
|
||||||
.header-cell.sorted :global(.cell) {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -31,7 +31,9 @@
|
||||||
isOpen = true
|
isOpen = true
|
||||||
await tick()
|
await tick()
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
textarea.setSelectionRange(0, 0)
|
if (value?.length > 100) {
|
||||||
|
textarea.setSelectionRange(0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
blur: () => close(),
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: open,
|
focus: open,
|
||||||
blur: close,
|
blur: close,
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -235,6 +235,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: open,
|
focus: open,
|
||||||
blur: close,
|
blur: close,
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => input?.focus(),
|
focus: () => input?.focus(),
|
||||||
blur: () => input?.blur(),
|
blur: () => input?.blur(),
|
||||||
|
isActive: () => active,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,5 +12,5 @@
|
||||||
on:click={() => dispatch("add-column")}
|
on:click={() => dispatch("add-column")}
|
||||||
disabled={!$config.allowAddColumns}
|
disabled={!$config.allowAddColumns}
|
||||||
>
|
>
|
||||||
Create column
|
Add column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
icon="TableRowAddBottom"
|
icon="TableRowAddBottom"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => dispatch("add-row")}
|
on:click={() => dispatch("add-row-inline")}
|
||||||
disabled={!loaded ||
|
disabled={!loaded ||
|
||||||
!$config.allowAddRows ||
|
!$config.allowAddRows ||
|
||||||
(!$columns.length && !$stickyColumn)}
|
(!$columns.length && !$stickyColumn)}
|
||||||
>
|
>
|
||||||
Create row
|
Add row
|
||||||
</ActionButton>
|
</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>
|
<script>
|
||||||
import {
|
import { Modal, ModalContent, Button, notifications } from "@budibase/bbui"
|
||||||
Modal,
|
import { getContext, onMount } from "svelte"
|
||||||
ModalContent,
|
|
||||||
ActionButton,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { selectedRows, rows, config } = getContext("grid")
|
const { selectedRows, rows, config, subscribe } = getContext("grid")
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -28,18 +23,21 @@
|
||||||
await rows.actions.deleteRows(rowsToDelete)
|
await rows.actions.deleteRows(rowsToDelete)
|
||||||
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if selectedRowCount}
|
{#if selectedRowCount}
|
||||||
<div class="delete-button" data-ignore-click-outside="true">
|
<div class="delete-button" data-ignore-click-outside="true">
|
||||||
<ActionButton
|
<Button
|
||||||
icon="Delete"
|
icon="Delete"
|
||||||
size="S"
|
size="M"
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
disabled={!$config.allowEditRows}
|
disabled={!$config.allowEditRows}
|
||||||
|
cta
|
||||||
>
|
>
|
||||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||||
</ActionButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -57,16 +55,12 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) {
|
.delete-button :global(.spectrum-Button:not(:disabled)) {
|
||||||
color: var(--spectrum-global-color-red-400);
|
background-color: var(--spectrum-global-color-red-400);
|
||||||
}
|
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
|
|
||||||
border-color: var(--spectrum-global-color-red-400);
|
border-color: var(--spectrum-global-color-red-400);
|
||||||
}
|
}
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/
|
.delete-button :global(.spectrum-Button:not(:disabled):hover) {
|
||||||
/* color: var(--spectrum-global-color-gray-600);*/
|
background-color: var(--spectrum-global-color-red-500);
|
||||||
/*}*/
|
border-color: var(--spectrum-global-color-red-500);
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/
|
}
|
||||||
/* border-color: var(--spectrum-global-color-gray-400);*/
|
|
||||||
/*}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
selected={open || anyHidden}
|
selected={open || anyHidden}
|
||||||
disabled={!$columns.length}
|
disabled={!$columns.length}
|
||||||
>
|
>
|
||||||
Hide columns
|
Columns
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -36,13 +36,13 @@
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="LineHeight"
|
icon="MoveUpDown"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open}
|
selected={open}
|
||||||
>
|
>
|
||||||
Row height
|
Height
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||||
|
|
||||||
const { sort, visibleColumns, stickyColumn } = getContext("grid")
|
const { sort, columns, stickyColumn } = getContext("grid")
|
||||||
const orderOptions = [
|
const orderOptions = [
|
||||||
{ label: "A-Z", value: "ascending" },
|
{ label: "A-Z", value: "ascending" },
|
||||||
{ label: "Z-A", value: "descending" },
|
{ label: "Z-A", value: "descending" },
|
||||||
|
@ -11,15 +11,24 @@
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
|
$: columnOptions = getColumnOptions($stickyColumn, $columns)
|
||||||
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
|
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
|
||||||
|
|
||||||
const getColumnOptions = (stickyColumn, columns) => {
|
const getColumnOptions = (stickyColumn, columns) => {
|
||||||
let options = []
|
let options = []
|
||||||
if (stickyColumn) {
|
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 => {
|
const updateSortColumn = e => {
|
||||||
|
@ -37,13 +46,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we never have a sort column selected that is not visible
|
// Ensure we never have a sort column selected that is not visible
|
||||||
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
|
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
|
||||||
if (!sortColumn) {
|
if (!sortColumn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
sortColumn !== stickyColumn?.name &&
|
sortColumn !== stickyColumn?.name &&
|
||||||
!visibleColumns.some(col => col.name === sortColumn)
|
!columns.some(col => col.name === sortColumn)
|
||||||
) {
|
) {
|
||||||
if (stickyColumn) {
|
if (stickyColumn) {
|
||||||
sort.update(state => ({
|
sort.update(state => ({
|
||||||
|
@ -53,7 +62,7 @@
|
||||||
} else {
|
} else {
|
||||||
sort.update(state => ({
|
sort.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
column: visibleColumns[0]?.name,
|
column: columns[0]?.name,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +75,7 @@
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open || $sort.column}
|
selected={open}
|
||||||
disabled={!columnOptions.length}
|
disabled={!columnOptions.length}
|
||||||
>
|
>
|
||||||
Sort
|
Sort
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
import AddRowButton from "../controls/AddRowButton.svelte"
|
||||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
||||||
|
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||||
|
import NewRow from "./NewRow.svelte"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderHeight,
|
||||||
MaxCellRenderWidthOverflow,
|
MaxCellRenderWidthOverflow,
|
||||||
|
@ -110,6 +112,7 @@
|
||||||
<AddRowButton />
|
<AddRowButton />
|
||||||
<AddColumnButton />
|
<AddColumnButton />
|
||||||
<slot name="controls" />
|
<slot name="controls" />
|
||||||
|
<ColumnWidthButton />
|
||||||
<RowHeightButton />
|
<RowHeightButton />
|
||||||
<HideColumnsButton />
|
<HideColumnsButton />
|
||||||
<SortButton />
|
<SortButton />
|
||||||
|
@ -127,10 +130,11 @@
|
||||||
<HeaderRow />
|
<HeaderRow />
|
||||||
<GridBody />
|
<GridBody />
|
||||||
</div>
|
</div>
|
||||||
|
<BetaButton />
|
||||||
|
<NewRow />
|
||||||
<div class="overlays">
|
<div class="overlays">
|
||||||
<ResizeOverlay />
|
<ResizeOverlay />
|
||||||
<ReorderOverlay />
|
<ReorderOverlay />
|
||||||
<BetaButton />
|
|
||||||
<ScrollOverlay />
|
<ScrollOverlay />
|
||||||
<MenuOverlay />
|
<MenuOverlay />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,25 @@
|
||||||
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 { BlankRowID } from "../lib/constants"
|
||||||
|
|
||||||
const { bounds, renderedRows, rowVerticalInversionIndex } = getContext("grid")
|
const {
|
||||||
|
bounds,
|
||||||
|
renderedRows,
|
||||||
|
renderedColumns,
|
||||||
|
rowVerticalInversionIndex,
|
||||||
|
config,
|
||||||
|
hoveredRowId,
|
||||||
|
dispatch,
|
||||||
|
} = getContext("grid")
|
||||||
|
|
||||||
let body
|
let body
|
||||||
|
|
||||||
|
$: renderColumnsWidth = $renderedColumns.reduce(
|
||||||
|
(total, col) => (total += col.width),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Observe and record the height of the body
|
// Observe and record the height of the body
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
|
@ -24,6 +38,16 @@
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
||||||
{/each}
|
{/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>
|
</GridScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -35,4 +59,15 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1 1 auto;
|
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>
|
</style>
|
||||||
|
|
|
@ -29,10 +29,7 @@
|
||||||
// Handles a wheel even and updates the scroll offsets
|
// Handles a wheel even and updates the scroll offsets
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const modifier = e.ctrlKey || e.metaKey
|
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
||||||
let x = modifier ? e.deltaY : e.deltaX
|
|
||||||
let y = modifier ? e.deltaX : e.deltaY
|
|
||||||
debouncedHandleWheel(x, y, e.clientY)
|
|
||||||
}
|
}
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
const { top, left } = $scroll
|
const { top, left } = $scroll
|
||||||
|
|
|
@ -2,8 +2,17 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import HeaderCell from "../cells/HeaderCell.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>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
@ -14,6 +23,13 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</GridScrollWrapper>
|
</GridScrollWrapper>
|
||||||
|
<div
|
||||||
|
class="add"
|
||||||
|
style="left:{left}px"
|
||||||
|
on:click={() => dispatch("add-column")}
|
||||||
|
>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -27,4 +43,20 @@
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
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>
|
</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>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Checkbox, Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import GridCell from "../cells/GridCell.svelte"
|
import GridCell from "../cells/GridCell.svelte"
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
import DataCell from "../cells/DataCell.svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import HeaderCell from "../cells/HeaderCell.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 {
|
const {
|
||||||
rows,
|
rows,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
|
renderedColumns,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
config,
|
config,
|
||||||
selectedCellMap,
|
selectedCellMap,
|
||||||
focusedRow,
|
focusedRow,
|
||||||
dispatch,
|
|
||||||
scrollLeft,
|
scrollLeft,
|
||||||
|
dispatch,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowCount = $rows.length
|
$: rowCount = $rows.length
|
||||||
|
@ -37,19 +39,6 @@
|
||||||
$selectedRows = allRows
|
$selectedRows = allRows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectRow = id => {
|
|
||||||
selectedRows.update(state => {
|
|
||||||
let newState = {
|
|
||||||
...state,
|
|
||||||
[id]: !state[id],
|
|
||||||
}
|
|
||||||
if (!newState[id]) {
|
|
||||||
delete newState[id]
|
|
||||||
}
|
|
||||||
return newState
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -58,26 +47,14 @@
|
||||||
class:scrolled={$scrollLeft > 0}
|
class:scrolled={$scrollLeft > 0}
|
||||||
>
|
>
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<GridCell width={GutterWidth} defaultHeight center>
|
<GutterCell
|
||||||
<div class="gutter">
|
disableExpand
|
||||||
<div class="checkbox visible">
|
disableNumber
|
||||||
{#if $config.allowDeleteRows}
|
on:select={selectAll}
|
||||||
<div on:click={selectAll}>
|
defaultHeight
|
||||||
<Checkbox
|
rowSelected={selectedRowCount && selectedRowCount === rowCount}
|
||||||
value={rowCount && selectedRowCount === rowCount}
|
disabled={!$renderedRows.length}
|
||||||
disabled={!$renderedRows.length}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if $config.allowExpandRows}
|
|
||||||
<div class="expand">
|
|
||||||
<Icon name="Maximize" size="S" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
|
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -95,41 +72,7 @@
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
on:mouseleave={() => ($hoveredRowId = null)}
|
||||||
>
|
>
|
||||||
<GridCell
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
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>
|
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
<DataCell
|
<DataCell
|
||||||
{row}
|
{row}
|
||||||
|
@ -146,6 +89,24 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/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>
|
</GridScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,6 +117,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background: var(--cell-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add right border */
|
/* Add right border */
|
||||||
|
@ -203,43 +165,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
.row.new :global(*:hover) {
|
||||||
/* Styles for gutter */
|
cursor: pointer;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
export const Padding = 276
|
export const Padding = 128
|
||||||
export const MaxCellRenderHeight = 252
|
export const MaxCellRenderHeight = 252
|
||||||
export const MaxCellRenderWidthOverflow = 200
|
export const MaxCellRenderWidthOverflow = 200
|
||||||
export const ScrollBarSize = 8
|
export const ScrollBarSize = 8
|
||||||
export const GutterWidth = 72
|
export const GutterWidth = 72
|
||||||
export const DefaultColumnWidth = 200
|
export const DefaultColumnWidth = 200
|
||||||
export const MinColumnWidth = 100
|
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 NewRowID = "new"
|
||||||
|
export const BlankRowID = "blank"
|
||||||
|
export const RowPageSize = 100
|
||||||
|
|
|
@ -15,7 +15,7 @@ const TypeIconMap = {
|
||||||
number: "123",
|
number: "123",
|
||||||
boolean: "Boolean",
|
boolean: "Boolean",
|
||||||
attachment: "AppleFiles",
|
attachment: "AppleFiles",
|
||||||
link: "Link",
|
link: "DataCorrelated",
|
||||||
formula: "Calculator",
|
formula: "Calculator",
|
||||||
json: "Brackets",
|
json: "Brackets",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
|
import { NewRowID } from "../lib/constants"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enrichedRows,
|
enrichedRows,
|
||||||
|
@ -10,15 +11,24 @@
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
focusedCellAPI,
|
focusedCellAPI,
|
||||||
clipboard,
|
clipboard,
|
||||||
|
dispatch,
|
||||||
|
selectedRows,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
// Global key listener which intercepts all key events
|
// Global key listener which intercepts all key events
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
// If nothing selected avoid processing further key presses
|
// If nothing selected avoid processing further key presses
|
||||||
if (!$focusedCellId) {
|
if (!$focusedCellId) {
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
focusFirstCell()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,10 +36,19 @@
|
||||||
// Always intercept certain key presses
|
// Always intercept certain key presses
|
||||||
const api = $focusedCellAPI
|
const api = $focusedCellAPI
|
||||||
if (e.key === "Escape") {
|
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") {
|
} else if (e.key === "Tab") {
|
||||||
api?.blur?.()
|
api?.blur?.()
|
||||||
changeFocusedColumn(1)
|
changeFocusedColumn(1)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the key event to the selected cell and let it decide whether to
|
// Pass the key event to the selected cell and let it decide whether to
|
||||||
|
@ -54,8 +73,12 @@
|
||||||
clipboard.actions.copy()
|
clipboard.actions.copy()
|
||||||
break
|
break
|
||||||
case "v":
|
case "v":
|
||||||
clipboard.actions.paste()
|
if (!api?.isReadonly()) {
|
||||||
|
clipboard.actions.paste()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
case "Enter":
|
||||||
|
dispatch("add-row-inline")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
@ -73,11 +96,19 @@
|
||||||
break
|
break
|
||||||
case "Delete":
|
case "Delete":
|
||||||
case "Backspace":
|
case "Backspace":
|
||||||
deleteSelectedCell()
|
if (Object.keys($selectedRows).length) {
|
||||||
|
dispatch("request-bulk-delete")
|
||||||
|
} else {
|
||||||
|
deleteSelectedCell()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "Enter":
|
case "Enter":
|
||||||
focusCell()
|
focusCell()
|
||||||
break
|
break
|
||||||
|
case " ":
|
||||||
|
case "Space":
|
||||||
|
toggleSelectRow()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
startEnteringValue(e.key, e.which)
|
startEnteringValue(e.key, e.which)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +187,7 @@
|
||||||
|
|
||||||
// Focuses the cell and starts entering a new value
|
// Focuses the cell and starts entering a new value
|
||||||
const startEnteringValue = (key, keyCode) => {
|
const startEnteringValue = (key, keyCode) => {
|
||||||
if ($focusedCellAPI) {
|
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
|
||||||
const type = $focusedCellAPI.getType()
|
const type = $focusedCellAPI.getType()
|
||||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
||||||
$focusedCellAPI.setValue(parseInt(key))
|
$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(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", handleKeyDown)
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
copiedCell,
|
copiedCell,
|
||||||
clipboard,
|
clipboard,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
focusedCellAPI,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: style = makeStyle($menu)
|
$: style = makeStyle($menu)
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Paste"
|
icon="Paste"
|
||||||
disabled={$copiedCell == null}
|
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
|
||||||
on:click={clipboard.actions.paste}
|
on:click={clipboard.actions.paste}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { fetchData } from "../../../fetch/fetchData"
|
import { fetchData } from "../../../fetch/fetchData"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||||
|
|
||||||
const initialSortState = {
|
const initialSortState = {
|
||||||
column: null,
|
column: null,
|
||||||
|
@ -16,6 +17,7 @@ export const createStores = () => {
|
||||||
const sort = writable(initialSortState)
|
const sort = writable(initialSortState)
|
||||||
const rowChangeCache = writable({})
|
const rowChangeCache = writable({})
|
||||||
const inProgressChanges = writable({})
|
const inProgressChanges = writable({})
|
||||||
|
const hasNextPage = writable(false)
|
||||||
|
|
||||||
// Generate a lookup map to quick find a row by ID
|
// Generate a lookup map to quick find a row by ID
|
||||||
const rowLookupMap = derived(
|
const rowLookupMap = derived(
|
||||||
|
@ -50,6 +52,7 @@ export const createStores = () => {
|
||||||
sort,
|
sort,
|
||||||
rowChangeCache,
|
rowChangeCache,
|
||||||
inProgressChanges,
|
inProgressChanges,
|
||||||
|
hasNextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +73,7 @@ export const deriveStores = context => {
|
||||||
rowChangeCache,
|
rowChangeCache,
|
||||||
inProgressChanges,
|
inProgressChanges,
|
||||||
previousFocusedRowId,
|
previousFocusedRowId,
|
||||||
|
hasNextPage,
|
||||||
} = context
|
} = context
|
||||||
const instanceLoaded = writable(false)
|
const instanceLoaded = writable(false)
|
||||||
const fetch = writable(null)
|
const fetch = writable(null)
|
||||||
|
@ -114,7 +118,7 @@ export const deriveStores = context => {
|
||||||
filter: [],
|
filter: [],
|
||||||
sortColumn: initialSortState.column,
|
sortColumn: initialSortState.column,
|
||||||
sortOrder: initialSortState.order,
|
sortOrder: initialSortState.order,
|
||||||
limit: 100,
|
limit: RowPageSize,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -122,6 +126,7 @@ export const deriveStores = context => {
|
||||||
// Subscribe to changes of this fetch model
|
// Subscribe to changes of this fetch model
|
||||||
unsubscribe = newFetch.subscribe($fetch => {
|
unsubscribe = newFetch.subscribe($fetch => {
|
||||||
if ($fetch.loaded && !$fetch.loading) {
|
if ($fetch.loaded && !$fetch.loading) {
|
||||||
|
hasNextPage.set($fetch.hasNextPage)
|
||||||
const $instanceLoaded = get(instanceLoaded)
|
const $instanceLoaded = get(instanceLoaded)
|
||||||
const resetRows = $fetch.resetKey !== lastResetKey
|
const resetRows = $fetch.resetKey !== lastResetKey
|
||||||
lastResetKey = $fetch.resetKey
|
lastResetKey = $fetch.resetKey
|
||||||
|
@ -230,7 +235,7 @@ export const deriveStores = context => {
|
||||||
if (bubble) {
|
if (bubble) {
|
||||||
throw error
|
throw error
|
||||||
} else {
|
} else {
|
||||||
handleValidationError("new", error)
|
handleValidationError(NewRowID, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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, DefaultRowHeight } from "../lib/constants"
|
import { Padding, GutterWidth } from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const scroll = writable({
|
const scroll = writable({
|
||||||
|
@ -29,7 +29,7 @@ export const deriveStores = context => {
|
||||||
// Derive vertical limits
|
// Derive vertical limits
|
||||||
const contentHeight = derived(
|
const contentHeight = derived(
|
||||||
[rows, rowHeight],
|
[rows, rowHeight],
|
||||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding,
|
([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const maxScrollTop = derived(
|
const maxScrollTop = derived(
|
||||||
|
@ -138,7 +138,7 @@ export const initialise = context => {
|
||||||
const $scroll = get(scroll)
|
const $scroll = get(scroll)
|
||||||
const $bounds = get(bounds)
|
const $bounds = get(bounds)
|
||||||
const $rowHeight = get(rowHeight)
|
const $rowHeight = get(rowHeight)
|
||||||
const verticalOffset = DefaultRowHeight * 1.5
|
const verticalOffset = 60
|
||||||
|
|
||||||
// Ensure vertical position is viewable
|
// Ensure vertical position is viewable
|
||||||
if ($focusedRow) {
|
if ($focusedRow) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
import { tick } from "svelte"
|
||||||
import {
|
import {
|
||||||
DefaultRowHeight,
|
DefaultRowHeight,
|
||||||
LargeRowHeight,
|
LargeRowHeight,
|
||||||
MediumRowHeight,
|
MediumRowHeight,
|
||||||
|
NewRowID,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
|
@ -49,14 +51,14 @@ export const deriveStores = context => {
|
||||||
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
|
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
|
||||||
const rowId = $focusedCellId?.split("-")[0]
|
const rowId = $focusedCellId?.split("-")[0]
|
||||||
|
|
||||||
if (rowId === "new") {
|
// Edge case for new rows
|
||||||
// Edge case for new row
|
if (rowId === NewRowID) {
|
||||||
return { _id: rowId }
|
return { _id: NewRowID }
|
||||||
} else {
|
|
||||||
// All normal rows
|
|
||||||
const index = $rowLookupMap[rowId]
|
|
||||||
return $enrichedRows[index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All normal rows
|
||||||
|
const index = $rowLookupMap[rowId]
|
||||||
|
return $enrichedRows[index]
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
@ -101,7 +103,10 @@ export const initialise = context => {
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Ensure we clear invalid rows from state if they disappear
|
// 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 $focusedCellId = get(focusedCellId)
|
||||||
const $selectedRows = get(selectedRows)
|
const $selectedRows = get(selectedRows)
|
||||||
const $hoveredRowId = get(hoveredRowId)
|
const $hoveredRowId = get(hoveredRowId)
|
||||||
|
@ -140,20 +145,6 @@ export const initialise = context => {
|
||||||
lastFocusedRowId = id
|
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
|
// Remove hovered row when a cell is selected
|
||||||
focusedCellId.subscribe(cell => {
|
focusedCellId.subscribe(cell => {
|
||||||
if (cell && get(hoveredRowId)) {
|
if (cell && get(hoveredRowId)) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderHeight,
|
||||||
MaxCellRenderWidthOverflow,
|
MaxCellRenderWidthOverflow,
|
||||||
|
MinColumnWidth,
|
||||||
ScrollBarSize,
|
ScrollBarSize,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
|
@ -45,11 +46,16 @@ export const deriveStores = context => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Derive visible columns
|
// Derive visible columns
|
||||||
|
const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
|
||||||
|
const interval = MinColumnWidth
|
||||||
|
return Math.round($scrollLeft / interval) * interval
|
||||||
|
})
|
||||||
const renderedColumns = derived(
|
const renderedColumns = derived(
|
||||||
[visibleColumns, scrollLeft, width],
|
[visibleColumns, scrollLeftRounded, width],
|
||||||
([$visibleColumns, $scrollLeft, $width]) => {
|
([$visibleColumns, $scrollLeft, $width], set) => {
|
||||||
if (!$visibleColumns.length) {
|
if (!$visibleColumns.length) {
|
||||||
return []
|
set([])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let startColIdx = 0
|
let startColIdx = 0
|
||||||
let rightEdge = $visibleColumns[0].width
|
let rightEdge = $visibleColumns[0].width
|
||||||
|
@ -69,19 +75,17 @@ export const deriveStores = context => {
|
||||||
leftEdge += $visibleColumns[endColIdx].width
|
leftEdge += $visibleColumns[endColIdx].width
|
||||||
endColIdx++
|
endColIdx++
|
||||||
}
|
}
|
||||||
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx)
|
// Render an additional column on either side to account for
|
||||||
|
// debounce column updates based on scroll position
|
||||||
// Cautiously shrink the number of rendered columns.
|
const next = $visibleColumns.slice(
|
||||||
// This is to avoid rapidly shrinking and growing the visible column count
|
Math.max(0, startColIdx - 1),
|
||||||
// which results in remounting cells
|
endColIdx + 1
|
||||||
const currentCount = get(renderedColumns).length
|
)
|
||||||
if (currentCount === nextRenderedColumns.length + 1) {
|
const current = get(renderedColumns)
|
||||||
return $visibleColumns.slice(startColIdx, endColIdx + 1)
|
if (JSON.stringify(next) !== JSON.stringify(current)) {
|
||||||
} else {
|
set(next)
|
||||||
return nextRenderedColumns
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const hiddenColumnsWidth = derived(
|
const hiddenColumnsWidth = derived(
|
||||||
|
|
Loading…
Reference in New Issue