Large refactors to row creation, naming and sheet APIs
This commit is contained in:
parent
da2023974e
commit
81a28eb4da
|
@ -84,7 +84,7 @@
|
|||
"@spectrum-css/vars": "3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-flatpickr": "^3.2.3",
|
||||
"svelte-flatpickr": "^3.3.2",
|
||||
"svelte-portal": "^1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -2576,10 +2576,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
svelte-flatpickr@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8"
|
||||
integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw==
|
||||
svelte-flatpickr@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1"
|
||||
integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w==
|
||||
dependencies:
|
||||
flatpickr "^4.5.2"
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { Dropzone, notifications } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let onChange
|
||||
export let readonly = false
|
||||
export let api
|
||||
|
@ -15,9 +15,9 @@
|
|||
|
||||
let isOpen = false
|
||||
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
$: {
|
||||
if (!selected) {
|
||||
if (!focused) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
import { Checkbox } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let onChange
|
||||
export let readonly = false
|
||||
export let api
|
||||
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
|
||||
const handleChange = e => {
|
||||
onChange(e.detail)
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<script>
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import SheetCell from "./SheetCell.svelte"
|
||||
import { getCellRenderer } from "../lib/renderers"
|
||||
import { derived, writable } from "svelte/store"
|
||||
|
||||
const { rows, selectedCellId, menu, selectedCellAPI, config } =
|
||||
const { rows, focusedCellId, menu, sheetAPI, config, validation } =
|
||||
getContext("sheet")
|
||||
|
||||
export let rowSelected
|
||||
export let rowHovered
|
||||
export let rowFocused
|
||||
export let rowIdx
|
||||
export let selected
|
||||
export let focused
|
||||
export let selectedUser
|
||||
export let reorderSource
|
||||
export let reorderTarget
|
||||
|
@ -19,8 +21,36 @@
|
|||
export let updateRow = rows.actions.updateRow
|
||||
export let invert = false
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
let api
|
||||
let error
|
||||
|
||||
$: {
|
||||
// Wipe error if row is unfocused
|
||||
if (!rowFocused && $error) {
|
||||
validation.actions.setError(cellId, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the error for this cell if the row is focused
|
||||
$: error = getErrorStore(rowFocused, cellId)
|
||||
|
||||
// Determine if the cell is editable
|
||||
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
|
||||
|
||||
// Register this cell API if the row is focused
|
||||
$: {
|
||||
if (rowFocused) {
|
||||
sheetAPI.actions.registerCellAPI(cellId, cellAPI)
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorStore = (selected, cellId) => {
|
||||
if (!selected) {
|
||||
return emptyError
|
||||
}
|
||||
return derived(validation, $validation => $validation[cellId])
|
||||
}
|
||||
|
||||
const cellAPI = {
|
||||
focus: () => api?.focus(),
|
||||
|
@ -29,56 +59,43 @@
|
|||
isReadonly: () => readonly,
|
||||
isRequired: () => !!column.schema.constraints?.presence,
|
||||
validate: value => {
|
||||
// Validate the current value if no new value is provided
|
||||
if (value === undefined) {
|
||||
value = row[column.name]
|
||||
}
|
||||
let newError = null
|
||||
if (cellAPI.isReadonly() && !(value == null || value === "")) {
|
||||
// Ensure cell isn't readonly
|
||||
error = "Auto columns can't be edited"
|
||||
newError = "Auto columns can't be edited"
|
||||
} else if (cellAPI.isRequired() && (value == null || value === "")) {
|
||||
// Sanity check required fields
|
||||
error = "Required field"
|
||||
newError = "Required field"
|
||||
} else {
|
||||
error = null
|
||||
newError = null
|
||||
}
|
||||
return error
|
||||
validation.actions.setError(cellId, newError)
|
||||
return newError
|
||||
},
|
||||
updateValue: value => {
|
||||
try {
|
||||
cellAPI.validate(value)
|
||||
if (!error) {
|
||||
if (!$error) {
|
||||
updateRow(row._id, column.name, value)
|
||||
}
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Determine if the cell is editable
|
||||
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
|
||||
|
||||
// Update selected cell API if selected
|
||||
$: {
|
||||
if (selected) {
|
||||
selectedCellAPI.set(cellAPI)
|
||||
} else if (error) {
|
||||
// error = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key error}
|
||||
<SheetCell
|
||||
{rowSelected}
|
||||
{rowHovered}
|
||||
{rowFocused}
|
||||
{rowIdx}
|
||||
{selected}
|
||||
{focused}
|
||||
{selectedUser}
|
||||
{reorderSource}
|
||||
{reorderTarget}
|
||||
{error}
|
||||
on:click={() => selectedCellId.set(cellId)}
|
||||
error={$error}
|
||||
on:click={() => focusedCellId.set(cellId)}
|
||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||
width={column.width}
|
||||
>
|
||||
|
@ -87,18 +104,9 @@
|
|||
bind:api
|
||||
value={row[column.name]}
|
||||
schema={column.schema}
|
||||
{selected}
|
||||
onChange={cellAPI.updateValue}
|
||||
{focused}
|
||||
{readonly}
|
||||
{invert}
|
||||
placeholder="error"
|
||||
/>
|
||||
</SheetCell>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.placeholder {
|
||||
font-style: italic;
|
||||
padding: var(--cell-padding);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
export let value
|
||||
export let schema
|
||||
export let onChange
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let readonly = false
|
||||
|
||||
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
||||
|
@ -18,7 +18,7 @@
|
|||
: dateOnly
|
||||
? "MMM D YYYY"
|
||||
: "MMM D YYYY, HH:mm"
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
.header-cell :global(.cell) {
|
||||
padding: 0 var(--cell-padding);
|
||||
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);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { onMount, tick } from "svelte"
|
||||
|
||||
export let value
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let onChange
|
||||
export let readonly = false
|
||||
export let api
|
||||
|
@ -11,9 +11,9 @@
|
|||
let textarea
|
||||
let isOpen = false
|
||||
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
$: {
|
||||
if (!selected) {
|
||||
if (!focused) {
|
||||
isOpen = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let value
|
||||
export let schema
|
||||
export let onChange
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let multi = false
|
||||
export let readonly = false
|
||||
export let api
|
||||
|
@ -16,11 +16,11 @@
|
|||
let focusedOptionIdx = null
|
||||
|
||||
$: options = schema?.constraints?.inclusion || []
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
||||
$: {
|
||||
// Close when deselected
|
||||
if (!selected) {
|
||||
if (!focused) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +180,7 @@
|
|||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 4px 4px 10px 2px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
max-height: var(--max-cell-render-height);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
export let value
|
||||
export let api
|
||||
export let readonly
|
||||
export let selected
|
||||
export let focused
|
||||
export let schema
|
||||
export let onChange
|
||||
export let invert = false
|
||||
|
@ -25,12 +25,12 @@
|
|||
let results
|
||||
|
||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
$: results = getResults(searchResults, value)
|
||||
$: lookupMap = buildLookupMap(value, isOpen)
|
||||
$: search(searchString)
|
||||
$: {
|
||||
if (!selected) {
|
||||
if (!focused) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
export let rowSelected = false
|
||||
export let rowHovered = false
|
||||
export let selected = false
|
||||
export let rowFocused = false
|
||||
export let focused = false
|
||||
export let reorderSource = false
|
||||
export let reorderTarget = false
|
||||
export let width = ""
|
||||
|
@ -25,12 +26,13 @@
|
|||
class="cell"
|
||||
class:row-selected={rowSelected}
|
||||
class:row-hovered={rowHovered}
|
||||
class:selected
|
||||
class:row-focused={rowFocused}
|
||||
class:focused
|
||||
class:selected-other={selectedUser != null}
|
||||
class:reorder-source={reorderSource}
|
||||
class:reorder-target={reorderTarget}
|
||||
class:center
|
||||
class:error={error && selected}
|
||||
class:error
|
||||
on:focus
|
||||
on:mousedown
|
||||
on:mouseup
|
||||
|
@ -39,8 +41,14 @@
|
|||
{style}
|
||||
data-row={rowIdx}
|
||||
>
|
||||
{#if error}
|
||||
<div class="label">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if selectedUser && !selected}
|
||||
|
||||
{#if selectedUser && !focused}
|
||||
<div class="label">
|
||||
{selectedUser.label}
|
||||
</div>
|
||||
|
@ -64,9 +72,9 @@
|
|||
position: relative;
|
||||
width: 0;
|
||||
}
|
||||
.cell.selected:after,
|
||||
.cell.focused:after,
|
||||
.cell.error:after,
|
||||
.cell.selected-other:not(.selected):after {
|
||||
.cell.selected-other:not(.focused):after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -78,28 +86,32 @@
|
|||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cell.selected {
|
||||
.cell:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
.cell.focused {
|
||||
z-index: 2;
|
||||
}
|
||||
.cell.selected:after {
|
||||
.cell.focused:after {
|
||||
border-color: var(--spectrum-global-color-blue-400);
|
||||
}
|
||||
.cell.error:after {
|
||||
border-color: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
.cell.selected-other:not(.selected) {
|
||||
.cell.selected-other:not(.focused) {
|
||||
z-index: 1;
|
||||
}
|
||||
.cell.selected-other:not(.selected):after {
|
||||
.cell.selected-other:not(.focused):after {
|
||||
border-color: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
.cell:not(.selected) {
|
||||
.cell:not(.focused) {
|
||||
user-select: none;
|
||||
}
|
||||
.cell:hover {
|
||||
cursor: default;
|
||||
}
|
||||
.cell.row-selected {
|
||||
.cell.row-selected,
|
||||
.cell.row-focused {
|
||||
--cell-background: var(--spectrum-global-color-gray-75);
|
||||
}
|
||||
.cell.row-hovered {
|
||||
|
@ -126,10 +138,11 @@
|
|||
.label {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin: 0 0 -2px 0;
|
||||
padding: 1px 4px 3px 4px;
|
||||
background: var(--user-color);
|
||||
border-radius: 2px 2px 0 0;
|
||||
border-radius: 2px;
|
||||
display: none;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
|
@ -143,7 +156,7 @@
|
|||
.cell[data-row="0"] .label {
|
||||
bottom: auto;
|
||||
top: 100%;
|
||||
border-radius: 0 0 2px 2px;
|
||||
border-radius: 0 2px 2px 2px;
|
||||
padding: 2px 4px 2px 4px;
|
||||
margin: -2px 0 0 0;
|
||||
}
|
||||
|
@ -152,6 +165,8 @@
|
|||
}
|
||||
.error .label {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
.error.focused .label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,23 +2,23 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
export let value
|
||||
export let selected = false
|
||||
export let focused = false
|
||||
export let onChange
|
||||
export let type = "text"
|
||||
export let readonly = false
|
||||
export let api
|
||||
|
||||
let input
|
||||
let focused = false
|
||||
let active = false
|
||||
|
||||
$: editable = selected && !readonly
|
||||
$: editable = focused && !readonly
|
||||
|
||||
const handleChange = e => {
|
||||
onChange(e.target.value)
|
||||
}
|
||||
|
||||
const onKeyDown = e => {
|
||||
if (!focused) {
|
||||
if (!active) {
|
||||
return false
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
|
@ -41,8 +41,8 @@
|
|||
{#if editable}
|
||||
<input
|
||||
bind:this={input}
|
||||
on:focus={() => (focused = true)}
|
||||
on:blur={() => (focused = false)}
|
||||
on:focus={() => (active = true)}
|
||||
on:blur={() => (active = false)}
|
||||
{type}
|
||||
value={value || ""}
|
||||
on:change={handleChange}
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
background: var(--background);
|
||||
border-bottom: var(--cell-border);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: var(--row-height);
|
||||
z-index: 1;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
<script>
|
||||
import SheetCell from "../cells/SheetCell.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { Icon, Button } from "@budibase/bbui"
|
||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
|
||||
const {
|
||||
renderedColumns,
|
||||
hoveredRowId,
|
||||
selectedCellId,
|
||||
stickyColumn,
|
||||
gutterWidth,
|
||||
scroll,
|
||||
config,
|
||||
dispatch,
|
||||
visibleColumns,
|
||||
rows,
|
||||
wheel,
|
||||
showHScrollbar,
|
||||
tableId,
|
||||
} = getContext("sheet")
|
||||
|
||||
let isAdding = false
|
||||
let newRow = {}
|
||||
let touched = false
|
||||
|
||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||
$: rowHovered = $hoveredRowId === "new"
|
||||
$: containsSelectedCell = $selectedCellId?.startsWith("new-")
|
||||
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
||||
$: scrollLeft = $scroll.left
|
||||
$: $tableId, (isAdding = false)
|
||||
|
||||
const addRow = async () => {
|
||||
const savedRow = await rows.actions.addRow(newRow)
|
||||
if (savedRow && firstColumn) {
|
||||
$selectedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
isAdding = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
isAdding = false
|
||||
}
|
||||
|
||||
const startAdding = () => {
|
||||
newRow = {}
|
||||
isAdding = true
|
||||
}
|
||||
|
||||
const updateRow = (rowId, columnName, val) => {
|
||||
touched = true
|
||||
newRow[columnName] = val
|
||||
}
|
||||
|
||||
const addViaModal = () => {
|
||||
isAdding = false
|
||||
dispatch("add-row")
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if firstColumn}
|
||||
<div
|
||||
class="add-button"
|
||||
class:visible={!isAdding}
|
||||
class:above-scrollbar={$showHScrollbar}
|
||||
>
|
||||
<Button size="M" cta icon="Add" on:click={startAdding}>Add row</Button>
|
||||
</div>
|
||||
|
||||
<div class="container" class:visible={isAdding}>
|
||||
<div class="buttons">
|
||||
<Button size="M" cta on:click={addRow}>Save</Button>
|
||||
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
|
||||
</div>
|
||||
<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}
|
||||
>
|
||||
<SheetCell
|
||||
width={gutterWidth}
|
||||
{rowHovered}
|
||||
rowSelected={containsSelectedCell}
|
||||
>
|
||||
<div class="gutter">
|
||||
{#if $config.allowExpandRows}
|
||||
<Icon
|
||||
name="Maximize"
|
||||
size="S"
|
||||
hoverable
|
||||
on:click={addViaModal}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</SheetCell>
|
||||
{#if $stickyColumn}
|
||||
{@const cellId = `new-${$stickyColumn.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
column={$stickyColumn}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
selected={$selectedCellId === cellId}
|
||||
rowSelected={containsSelectedCell}
|
||||
width={$stickyColumn.width}
|
||||
{updateRow}
|
||||
invert
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<SheetScrollWrapper scrollVertically={false}>
|
||||
<div class="row">
|
||||
{#each $renderedColumns as column}
|
||||
{@const cellId = `new-${column.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
selected={$selectedCellId === cellId}
|
||||
rowSelected={containsSelectedCell}
|
||||
width={column.width}
|
||||
{updateRow}
|
||||
invert
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</SheetScrollWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.add-button {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 1;
|
||||
transform: translateY(calc(16px + 100%));
|
||||
transition: transform 130ms ease-out;
|
||||
}
|
||||
.add-button.above-scrollbar {
|
||||
bottom: 32px;
|
||||
}
|
||||
.add-button.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transform: translateY(100%);
|
||||
z-index: 1;
|
||||
transition: transform 130ms ease-out;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
var(--cell-background) 80%
|
||||
);
|
||||
width: 100%;
|
||||
padding-top: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.container.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.content {
|
||||
pointer-events: all;
|
||||
background: var(--background);
|
||||
border-top: var(--cell-border);
|
||||
}
|
||||
.content.above-scrollbar {
|
||||
padding: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.new-row {
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transition: margin-bottom 130ms ease-out;
|
||||
}
|
||||
.new-row.visible {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.new-row :global(.cell) {
|
||||
--cell-background: var(--background) !important;
|
||||
}
|
||||
|
||||
.sticky-column {
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
/* 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.scrolled :global(.cell:last-child:after) {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
left: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
|
||||
}
|
||||
|
||||
/* Styles for gutter */
|
||||
.gutter {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
padding: var(--cell-padding);
|
||||
grid-template-columns: 1fr 16px;
|
||||
gap: var(--cell-spacing);
|
||||
}
|
||||
|
||||
/* Floating buttons */
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px 16px;
|
||||
pointer-events: all;
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
|
@ -4,11 +4,11 @@
|
|||
import { Icon, Button } from "@budibase/bbui"
|
||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||
import DataCell from "../cells/DataCell.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
|
||||
const {
|
||||
renderedColumns,
|
||||
hoveredRowId,
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
stickyColumn,
|
||||
gutterWidth,
|
||||
scroll,
|
||||
|
@ -19,7 +19,7 @@
|
|||
showHScrollbar,
|
||||
tableId,
|
||||
subscribe,
|
||||
selectedCellAPI,
|
||||
sheetAPI,
|
||||
} = getContext("sheet")
|
||||
|
||||
let isAdding = false
|
||||
|
@ -28,33 +28,16 @@
|
|||
|
||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||
$: rowHovered = $hoveredRowId === "new"
|
||||
$: containsSelectedCell = $selectedCellId?.startsWith("new-")
|
||||
$: rowFocused = $focusedCellId?.startsWith("new-")
|
||||
$: width = gutterWidth + ($stickyColumn?.width || 0)
|
||||
$: scrollLeft = $scroll.left
|
||||
$: $tableId, (isAdding = false)
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const addRow = async () => {
|
||||
// Validate new row
|
||||
let allColumns = []
|
||||
if ($stickyColumn) {
|
||||
allColumns.push($stickyColumn)
|
||||
}
|
||||
allColumns = allColumns.concat($visibleColumns)
|
||||
for (let col of allColumns) {
|
||||
$selectedCellId = `new-${col.name}`
|
||||
await tick()
|
||||
const error = $selectedCellAPI.validate()
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create row
|
||||
const savedRow = await rows.actions.addRow(newRow, 0)
|
||||
if (savedRow && firstColumn) {
|
||||
$selectedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
isAdding = false
|
||||
}
|
||||
|
||||
|
@ -70,14 +53,10 @@
|
|||
}
|
||||
|
||||
const startAdding = () => {
|
||||
scroll.set({
|
||||
left: 0,
|
||||
top: 0,
|
||||
})
|
||||
newRow = {}
|
||||
isAdding = true
|
||||
if (firstColumn) {
|
||||
$selectedCellId = `new-${firstColumn.name}`
|
||||
$focusedCellId = `new-${firstColumn.name}`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,8 +74,8 @@
|
|||
</script>
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if firstColumn}
|
||||
<div class="container" class:visible={isAdding}>
|
||||
{#if isAdding}
|
||||
<div class="container" transition:fly={{ y: 20, duration: 130 }}>
|
||||
<div class="content" class:above-scrollbar={$showHScrollbar}>
|
||||
<div
|
||||
class="new-row"
|
||||
|
@ -108,11 +87,7 @@
|
|||
style="flex: 0 0 {width}px"
|
||||
class:scrolled={scrollLeft > 0}
|
||||
>
|
||||
<SheetCell
|
||||
width={gutterWidth}
|
||||
{rowHovered}
|
||||
rowSelected={containsSelectedCell}
|
||||
>
|
||||
<SheetCell width={gutterWidth} {rowHovered} {rowFocused}>
|
||||
<div class="gutter">
|
||||
<div class="number">1</div>
|
||||
{#if $config.allowExpandRows}
|
||||
|
@ -132,8 +107,8 @@
|
|||
column={$stickyColumn}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
selected={$selectedCellId === cellId}
|
||||
rowSelected={containsSelectedCell}
|
||||
focused={$focusedCellId === cellId}
|
||||
{rowFocused}
|
||||
width={$stickyColumn.width}
|
||||
{updateRow}
|
||||
rowIdx={0}
|
||||
|
@ -151,8 +126,8 @@
|
|||
{column}
|
||||
row={newRow}
|
||||
{rowHovered}
|
||||
selected={$selectedCellId === cellId}
|
||||
rowSelected={containsSelectedCell}
|
||||
focused={$focusedCellId === cellId}
|
||||
{rowFocused}
|
||||
width={column.width}
|
||||
{updateRow}
|
||||
rowIdx={0}
|
||||
|
@ -176,22 +151,17 @@
|
|||
position: absolute;
|
||||
top: var(--row-height);
|
||||
left: 0;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 130ms ease-out;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--cell-background) 20%,
|
||||
transparent 100%
|
||||
);
|
||||
width: 100%;
|
||||
padding-bottom: 64px;
|
||||
padding-bottom: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.container.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.content {
|
||||
pointer-events: all;
|
||||
background: var(--background);
|
||||
|
@ -216,6 +186,7 @@
|
|||
.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)) {
|
||||
|
@ -228,13 +199,17 @@
|
|||
}
|
||||
|
||||
/* Add shadow when scrolled */
|
||||
.sticky.scrolled :global(.cell:last-child:after) {
|
||||
.sticky-column.scrolled {
|
||||
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
|
||||
}
|
||||
.sticky-column.scrolled:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
|
||||
left: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Styles for gutter */
|
||||
|
@ -252,7 +227,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin: 16px 0 0 16px;
|
||||
margin: 24px 0 0 16px;
|
||||
pointer-events: all;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
import { createMenuStores } from "../stores/menu"
|
||||
import { createMaxScrollStores } from "../stores/max-scroll"
|
||||
import { createPaginationStores } from "../stores/pagination"
|
||||
import { createSheetAPIStores } from "../stores/sheet-api"
|
||||
import { createValidationStores } from "../stores/validation"
|
||||
import DeleteButton from "../controls/DeleteButton.svelte"
|
||||
import SheetBody from "./SheetBody.svelte"
|
||||
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
||||
|
@ -63,11 +65,13 @@
|
|||
tableId: tableIdStore,
|
||||
}
|
||||
context = { ...context, ...createEventManagers() }
|
||||
context = { ...context, ...createValidationStores(context) }
|
||||
context = { ...context, ...createBoundsStores(context) }
|
||||
context = { ...context, ...createScrollStores(context) }
|
||||
context = { ...context, ...createRowsStore(context) }
|
||||
context = { ...context, ...createColumnsStores(context) }
|
||||
context = { ...context, ...createUIStores(context) }
|
||||
context = { ...context, ...createSheetAPIStores(context) }
|
||||
context = { ...context, ...createResizeStores(context) }
|
||||
context = { ...context, ...createViewportStores(context) }
|
||||
context = { ...context, ...createMaxScrollStores(context) }
|
||||
|
@ -75,7 +79,6 @@
|
|||
context = { ...context, ...createUserStores(context) }
|
||||
context = { ...context, ...createMenuStores(context) }
|
||||
context = { ...context, ...createPaginationStores(context) }
|
||||
context = { ...context, ...context }
|
||||
|
||||
// Reference some stores for local use
|
||||
const { isResizing, isReordering, ui, loaded, rowHeight } = context
|
||||
|
|
|
@ -7,41 +7,47 @@
|
|||
export let invert = false
|
||||
|
||||
const {
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
reorder,
|
||||
selectedRows,
|
||||
visibleColumns,
|
||||
renderedColumns,
|
||||
hoveredRowId,
|
||||
selectedCellMap,
|
||||
selectedCellRow,
|
||||
focusedRow,
|
||||
hiddenColumnsWidth,
|
||||
} = getContext("sheet")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
$: rowHovered = $hoveredRowId === row._id
|
||||
$: containsSelectedCell = $selectedCellRow?._id === row._id
|
||||
$: rowFocused = $focusedRow?._id === row._id
|
||||
$: cols = rowFocused ? $visibleColumns : $renderedColumns
|
||||
$: foo = `margin-left: ${-1 * $hiddenColumnsWidth}px;`
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="row"
|
||||
style={rowFocused ? foo : null}
|
||||
on:focus
|
||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
>
|
||||
{#each $renderedColumns as column (column.name)}
|
||||
{#each cols as column (column.name)}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
<DataCell
|
||||
rowSelected={rowSelected || containsSelectedCell}
|
||||
{rowSelected}
|
||||
{rowHovered}
|
||||
rowIdx={idx}
|
||||
selected={$selectedCellId === cellId}
|
||||
selectedUser={$selectedCellMap[cellId]}
|
||||
reorderSource={$reorder.sourceColumn === column.name}
|
||||
reorderTarget={$reorder.targetColumn === column.name}
|
||||
width={column.width}
|
||||
{rowFocused}
|
||||
{cellId}
|
||||
{column}
|
||||
{row}
|
||||
{invert}
|
||||
rowIdx={idx}
|
||||
focused={$focusedCellId === cellId}
|
||||
selectedUser={$selectedCellMap[cellId]}
|
||||
reorderSource={$reorder.sourceColumn === column.name}
|
||||
reorderTarget={$reorder.targetColumn === column.name}
|
||||
width={column.width}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -50,5 +56,6 @@
|
|||
.row {
|
||||
width: 0;
|
||||
display: flex;
|
||||
height: var(--row-height);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,14 +5,13 @@
|
|||
const {
|
||||
rowHeight,
|
||||
scroll,
|
||||
visibleColumns,
|
||||
renderedColumns,
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
renderedRows,
|
||||
maxScrollTop,
|
||||
maxScrollLeft,
|
||||
bounds,
|
||||
hoveredRowId,
|
||||
hiddenColumnsWidth,
|
||||
} = getContext("sheet")
|
||||
|
||||
export let scrollVertically = true
|
||||
|
@ -20,8 +19,7 @@
|
|||
export let wheelInteractive = true
|
||||
export let foo = false
|
||||
|
||||
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
|
||||
$: style = generateStyle($scroll, $rowHeight, hiddenWidths, foo)
|
||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth, foo)
|
||||
|
||||
const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => {
|
||||
let offsetX, offsetY
|
||||
|
@ -35,20 +33,6 @@
|
|||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
||||
}
|
||||
|
||||
// Calculates with total width of all columns currently not rendered
|
||||
const calculateHiddenWidths = renderedColumns => {
|
||||
const idx = $visibleColumns.findIndex(
|
||||
col => col.name === renderedColumns[0]?.name
|
||||
)
|
||||
let width = 0
|
||||
if (idx > 0) {
|
||||
for (let i = 0; i < idx; i++) {
|
||||
width += $visibleColumns[i].width
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
// Handles a wheel even and updates the scroll offsets
|
||||
const handleWheel = e => {
|
||||
e.preventDefault()
|
||||
|
@ -84,7 +68,7 @@
|
|||
<div
|
||||
class="outer"
|
||||
on:wheel={wheelInteractive ? handleWheel : null}
|
||||
on:click|self={() => ($selectedCellId = null)}
|
||||
on:click|self={() => ($focusedCellId = null)}
|
||||
>
|
||||
<div {style}>
|
||||
<slot />
|
||||
|
|
|
@ -11,13 +11,13 @@
|
|||
selectedRows,
|
||||
stickyColumn,
|
||||
renderedRows,
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
hoveredRowId,
|
||||
scroll,
|
||||
reorder,
|
||||
config,
|
||||
selectedCellMap,
|
||||
selectedCellRow,
|
||||
focusedRow,
|
||||
gutterWidth,
|
||||
dispatch,
|
||||
} = getContext("sheet")
|
||||
|
@ -90,7 +90,7 @@
|
|||
{#each $renderedRows as row, idx}
|
||||
{@const rowSelected = !!$selectedRows[row._id]}
|
||||
{@const rowHovered = $hoveredRowId === row._id}
|
||||
{@const containsSelectedRow = $selectedCellRow?._id === row._id}
|
||||
{@const rowFocused = $focusedRow?._id === row._id}
|
||||
<div
|
||||
class="row"
|
||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
|
@ -98,7 +98,8 @@
|
|||
>
|
||||
<SheetCell
|
||||
width={gutterWidth}
|
||||
rowSelected={rowSelected || containsSelectedRow}
|
||||
{rowSelected}
|
||||
{rowFocused}
|
||||
{rowHovered}
|
||||
>
|
||||
<div class="gutter">
|
||||
|
@ -106,22 +107,19 @@
|
|||
on:click={() => selectRow(row._id)}
|
||||
class="checkbox"
|
||||
class:visible={$config.allowSelectRows &&
|
||||
(rowSelected || rowHovered || containsSelectedRow)}
|
||||
(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} />
|
||||
</div>
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.allowSelectRows ||
|
||||
!(rowSelected || rowHovered || containsSelectedRow)}
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
</div>
|
||||
{#if $config.allowExpandRows}
|
||||
<div
|
||||
class="expand"
|
||||
class:visible={containsSelectedRow || rowHovered}
|
||||
>
|
||||
<div class="expand" class:visible={rowFocused || rowHovered}>
|
||||
<Icon
|
||||
name="Maximize"
|
||||
hoverable
|
||||
|
@ -138,10 +136,11 @@
|
|||
{#if $stickyColumn}
|
||||
{@const cellId = `${row._id}-${$stickyColumn.name}`}
|
||||
<DataCell
|
||||
rowSelected={rowSelected || containsSelectedRow}
|
||||
{rowSelected}
|
||||
{rowHovered}
|
||||
{rowFocused}
|
||||
rowIdx={idx}
|
||||
selected={$selectedCellId === cellId}
|
||||
focused={$focusedCellId === cellId}
|
||||
selectedUser={$selectedCellMap[cellId]}
|
||||
width={$stickyColumn.width}
|
||||
reorderTarget={$reorder.targetColumn === $stickyColumn.name}
|
||||
|
@ -162,12 +161,22 @@
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
border-right: var(--cell-border);
|
||||
z-index: 3;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/*Add shadow when scrolled */
|
||||
.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;
|
||||
/*z-index: 1;*/
|
||||
}
|
||||
.sticky-column.scrolled {
|
||||
box-shadow: 0 0 8px -2px rgba(0, 0, 0, 0.2);
|
||||
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
|
||||
}
|
||||
|
||||
/* Don't show borders between cells in the sticky column */
|
||||
|
|
|
@ -2,7 +2,7 @@ import { get } from "svelte/store"
|
|||
import { io } from "socket.io-client"
|
||||
|
||||
export const createWebsocket = context => {
|
||||
const { rows, tableId, users, userId, selectedCellId } = context
|
||||
const { rows, tableId, users, userId, focusedCellId } = context
|
||||
|
||||
// Determine connection info
|
||||
const tls = location.protocol === "https:"
|
||||
|
@ -55,8 +55,8 @@ export const createWebsocket = context => {
|
|||
tableId.subscribe(connectToDataspace)
|
||||
|
||||
// Notify selected cell changes
|
||||
selectedCellId.subscribe($selectedCellId => {
|
||||
socket.emit("select-cell", $selectedCellId)
|
||||
focusedCellId.subscribe($focusedCellId => {
|
||||
socket.emit("select-cell", $focusedCellId)
|
||||
})
|
||||
|
||||
return () => socket?.disconnect()
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
|
||||
const {
|
||||
rows,
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
visibleColumns,
|
||||
selectedCellRow,
|
||||
focusedRow,
|
||||
stickyColumn,
|
||||
selectedCellAPI,
|
||||
} = getContext("sheet")
|
||||
|
||||
const handleKeyDown = e => {
|
||||
// If nothing selected avoid processing further key presses
|
||||
if (!$selectedCellId) {
|
||||
if (!$focusedCellId) {
|
||||
if (e.key === "Tab") {
|
||||
selectFirstCell()
|
||||
}
|
||||
|
@ -72,15 +72,15 @@
|
|||
if (!firstColumn) {
|
||||
return
|
||||
}
|
||||
selectedCellId.set(`${firstRow._id}-${firstColumn.name}`)
|
||||
focusedCellId.set(`${firstRow._id}-${firstColumn.name}`)
|
||||
}
|
||||
|
||||
const changeSelectedColumn = delta => {
|
||||
if (!$selectedCellId) {
|
||||
if (!$focusedCellId) {
|
||||
return
|
||||
}
|
||||
const cols = $visibleColumns
|
||||
const split = $selectedCellId.split("-")
|
||||
const split = $focusedCellId.split("-")
|
||||
const columnName = split[1]
|
||||
let newColumnName
|
||||
if (columnName === $stickyColumn?.name) {
|
||||
|
@ -95,24 +95,24 @@
|
|||
}
|
||||
}
|
||||
if (newColumnName) {
|
||||
$selectedCellId = `${split[0]}-${newColumnName}`
|
||||
$focusedCellId = `${split[0]}-${newColumnName}`
|
||||
}
|
||||
}
|
||||
|
||||
const changeSelectedRow = delta => {
|
||||
if (!$selectedCellRow) {
|
||||
if (!$focusedRow) {
|
||||
return
|
||||
}
|
||||
const newRow = $rows[$selectedCellRow.__idx + delta]
|
||||
const newRow = $rows[$focusedRow.__idx + delta]
|
||||
if (newRow) {
|
||||
const split = $selectedCellId.split("-")
|
||||
$selectedCellId = `${newRow._id}-${split[1]}`
|
||||
const split = $focusedCellId.split("-")
|
||||
$focusedCellId = `${newRow._id}-${split[1]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce to avoid holding down delete and spamming requests
|
||||
const deleteSelectedCell = debounce(() => {
|
||||
if (!$selectedCellId) {
|
||||
if (!$focusedCellId) {
|
||||
return
|
||||
}
|
||||
$selectedCellAPI.updateValue(null)
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import { getContext } from "svelte"
|
||||
|
||||
const {
|
||||
selectedCellRow,
|
||||
focusedRow,
|
||||
menu,
|
||||
rows,
|
||||
columns,
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
stickyColumn,
|
||||
config,
|
||||
} = getContext("sheet")
|
||||
|
@ -19,20 +19,20 @@
|
|||
}
|
||||
|
||||
const deleteRow = () => {
|
||||
rows.actions.deleteRows([$selectedCellRow])
|
||||
rows.actions.deleteRows([$focusedRow])
|
||||
menu.actions.close()
|
||||
notifications.success("Deleted 1 row")
|
||||
}
|
||||
|
||||
const duplicate = async () => {
|
||||
let clone = { ...$selectedCellRow }
|
||||
let clone = { ...$focusedRow }
|
||||
delete clone._id
|
||||
delete clone._rev
|
||||
delete clone.__idx
|
||||
const newRow = await rows.actions.addRow(clone, $selectedCellRow.__idx + 1)
|
||||
const newRow = await rows.actions.addRow(clone, $focusedRow.__idx + 1)
|
||||
if (newRow) {
|
||||
const column = $stickyColumn?.name || $columns[0].name
|
||||
$selectedCellId = `${newRow._id}-${column}`
|
||||
$focusedCellId = `${newRow._id}-${column}`
|
||||
menu.actions.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
{#if !$isReordering}
|
||||
{#if $stickyColumn}
|
||||
<div
|
||||
class="resize-slider sticky"
|
||||
class="resize-slider"
|
||||
class:visible={column === $stickyColumn.name}
|
||||
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
|
||||
on:dblclick={() => resize.actions.resetSize($stickyColumn)}
|
||||
|
@ -62,9 +62,6 @@
|
|||
cursor: col-resize;
|
||||
opacity: 1;
|
||||
}
|
||||
.resize-slider.sticky {
|
||||
z-index: 3;
|
||||
}
|
||||
.resize-indicator {
|
||||
margin-left: -1px;
|
||||
width: 2px;
|
||||
|
|
|
@ -9,8 +9,8 @@ export const createMaxScrollStores = context => {
|
|||
bounds,
|
||||
rowHeight,
|
||||
scroll,
|
||||
selectedCellRow,
|
||||
selectedCellId,
|
||||
focusedRow,
|
||||
focusedCellId,
|
||||
gutterWidth,
|
||||
} = context
|
||||
const padding = 264
|
||||
|
@ -89,18 +89,18 @@ export const createMaxScrollStores = context => {
|
|||
})
|
||||
|
||||
// Ensure the selected cell is visible
|
||||
selectedCellId.subscribe(async $selectedCellId => {
|
||||
focusedCellId.subscribe(async $focusedCellId => {
|
||||
await tick()
|
||||
const $selectedCellRow = get(selectedCellRow)
|
||||
const $focusedRow = get(focusedRow)
|
||||
const $scroll = get(scroll)
|
||||
const $bounds = get(bounds)
|
||||
const $rowHeight = get(rowHeight)
|
||||
const verticalOffset = $rowHeight * 1.5
|
||||
|
||||
// Ensure vertical position is viewable
|
||||
if ($selectedCellRow) {
|
||||
if ($focusedRow) {
|
||||
// Ensure row is not below bottom of screen
|
||||
const rowYPos = $selectedCellRow.__idx * $rowHeight
|
||||
const rowYPos = $focusedRow.__idx * $rowHeight
|
||||
const bottomCutoff =
|
||||
$scroll.top + $bounds.height - $rowHeight - verticalOffset
|
||||
let delta = rowYPos - bottomCutoff
|
||||
|
@ -126,7 +126,7 @@ export const createMaxScrollStores = context => {
|
|||
// Ensure horizontal position is viewable
|
||||
// Check horizontal position of columns next
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
const columnName = $selectedCellId?.split("-")[1]
|
||||
const columnName = $focusedCellId?.split("-")[1]
|
||||
const column = $visibleColumns.find(col => col.name === columnName)
|
||||
const horizontalOffset = 24
|
||||
if (!column) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const createMenuStores = context => {
|
||||
const { bounds, selectedCellId, stickyColumn, rowHeight } = context
|
||||
const { bounds, focusedCellId, stickyColumn, rowHeight } = context
|
||||
const menu = writable({
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -14,7 +14,7 @@ export const createMenuStores = context => {
|
|||
const $stickyColumn = get(stickyColumn)
|
||||
const $rowHeight = get(rowHeight)
|
||||
e.preventDefault()
|
||||
selectedCellId.set(cellId)
|
||||
focusedCellId.set(cellId)
|
||||
menu.set({
|
||||
left: e.clientX - $bounds.left + 44 + ($stickyColumn?.width || 0),
|
||||
top: e.clientY - $bounds.top + $rowHeight + 4,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { fetchData } from "../../../fetch/fetchData"
|
|||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createRowsStore = context => {
|
||||
const { tableId, API, scroll } = context
|
||||
const { tableId, API, scroll, validation } = context
|
||||
const rows = writable([])
|
||||
const table = writable(null)
|
||||
const filter = writable([])
|
||||
|
@ -120,6 +120,21 @@ export const createRowsStore = context => {
|
|||
return index >= 0 ? get(enrichedRows)[index] : null
|
||||
}
|
||||
|
||||
// Handles validation errors from the rows API and updates local validation
|
||||
// state, storing error messages against relevant cells
|
||||
const handleValidationError = (rowId, error) => {
|
||||
if (error?.json?.validationErrors) {
|
||||
for (let column of Object.keys(error.json.validationErrors)) {
|
||||
validation.actions.setError(
|
||||
`${rowId}-${column}`,
|
||||
`${column} ${error.json.validationErrors[column]}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
notifications.error(`Error saving row: ${error?.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a new row
|
||||
const addRow = async (row, idx) => {
|
||||
try {
|
||||
|
@ -139,7 +154,7 @@ export const createRowsStore = context => {
|
|||
notifications.success("Row created successfully")
|
||||
return newRow
|
||||
} catch (error) {
|
||||
notifications.error(`Error adding row: ${error?.message}`)
|
||||
handleValidationError("new", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,7 +223,7 @@ export const createRowsStore = context => {
|
|||
try {
|
||||
await API.saveRow(newRow)
|
||||
} catch (error) {
|
||||
notifications.error(`Error saving row: ${error?.message}`)
|
||||
handleValidationError(newRow._id, error)
|
||||
|
||||
// Revert change
|
||||
rows.update(state => {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
|
||||
export const createSheetAPIStores = context => {
|
||||
const { focusedCellId } = context
|
||||
const cellAPIs = writable({})
|
||||
|
||||
const registerCellAPI = (cellId, api) => {
|
||||
// Ignore registration if cell is not selected
|
||||
const [rowId, column] = cellId.split("-")
|
||||
if (rowId !== "new" && !get(focusedCellId)?.startsWith(rowId)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store API
|
||||
cellAPIs.update(state => ({
|
||||
...state,
|
||||
[column]: api,
|
||||
}))
|
||||
}
|
||||
|
||||
const getCellAPI = column => {
|
||||
return get(cellAPIs)[column]
|
||||
}
|
||||
|
||||
// Derive the selected cell API
|
||||
const selectedCellAPI = derived(
|
||||
[cellAPIs, focusedCellId],
|
||||
([$apis, $focusedCellId]) => {
|
||||
if (!$focusedCellId) {
|
||||
return null
|
||||
}
|
||||
const [, column] = $focusedCellId.split("-")
|
||||
return $apis[column]
|
||||
},
|
||||
null
|
||||
)
|
||||
|
||||
const focusedRowAPI = derived(cellAPIs, $apis => {
|
||||
return {
|
||||
validate: () => {
|
||||
let errors = null
|
||||
for (let [column, api] of Object.entries($apis || {})) {
|
||||
const error = api.validate()
|
||||
if (error) {
|
||||
errors = {
|
||||
...errors,
|
||||
[column]: error,
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
selectedCellAPI,
|
||||
focusedRowAPI,
|
||||
sheetAPI: {
|
||||
actions: {
|
||||
registerCellAPI,
|
||||
getCellAPI,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -2,17 +2,16 @@ import { writable, get, derived } from "svelte/store"
|
|||
|
||||
export const createUIStores = context => {
|
||||
const { rows, rowLookupMap } = context
|
||||
const selectedCellId = writable(null)
|
||||
const focusedCellId = writable(null)
|
||||
const selectedRows = writable({})
|
||||
const hoveredRowId = writable(null)
|
||||
const selectedCellAPI = writable(null)
|
||||
const rowHeight = writable(36)
|
||||
|
||||
// Derive the row that contains the selected cell
|
||||
const selectedCellRow = derived(
|
||||
[selectedCellId, rowLookupMap, rows],
|
||||
([$selectedCellId, $rowLookupMap, $rows]) => {
|
||||
const rowId = $selectedCellId?.split("-")[0]
|
||||
const focusedRow = derived(
|
||||
[focusedCellId, rowLookupMap, rows],
|
||||
([$focusedCellId, $rowLookupMap, $rows]) => {
|
||||
const rowId = $focusedCellId?.split("-")[0]
|
||||
const index = $rowLookupMap[rowId]
|
||||
return $rows[index]
|
||||
},
|
||||
|
@ -21,15 +20,15 @@ export const createUIStores = context => {
|
|||
|
||||
// Ensure we clear invalid rows from state if they disappear
|
||||
rows.subscribe(() => {
|
||||
const $selectedCellId = get(selectedCellId)
|
||||
const $focusedCellId = get(focusedCellId)
|
||||
const $selectedRows = get(selectedRows)
|
||||
const $hoveredRowId = get(hoveredRowId)
|
||||
const hasRow = rows.actions.hasRow
|
||||
|
||||
// Check selected cell
|
||||
const selectedRowId = $selectedCellId?.split("-")[0]
|
||||
const selectedRowId = $focusedCellId?.split("-")[0]
|
||||
if (selectedRowId && !hasRow(selectedRowId)) {
|
||||
selectedCellId.set(null)
|
||||
focusedCellId.set(null)
|
||||
}
|
||||
|
||||
// Check hovered row
|
||||
|
@ -53,7 +52,7 @@ export const createUIStores = context => {
|
|||
})
|
||||
|
||||
// Reset selected rows when selected cell changes
|
||||
selectedCellId.subscribe(id => {
|
||||
focusedCellId.subscribe(id => {
|
||||
if (id) {
|
||||
selectedRows.set({})
|
||||
}
|
||||
|
@ -62,37 +61,29 @@ export const createUIStores = context => {
|
|||
// Unset selected cell when rows are selected
|
||||
selectedRows.subscribe(rows => {
|
||||
if (Object.keys(rows || {}).length) {
|
||||
selectedCellId.set(null)
|
||||
focusedCellId.set(null)
|
||||
}
|
||||
})
|
||||
|
||||
// Callback when leaving the sheet, deselecting all focussed or selected items
|
||||
const blur = () => {
|
||||
selectedCellId.set(null)
|
||||
focusedCellId.set(null)
|
||||
selectedRows.set({})
|
||||
hoveredRowId.set(null)
|
||||
}
|
||||
|
||||
// Remove selected cell API when no selected cell is present
|
||||
selectedCellId.subscribe(cell => {
|
||||
if (!cell && get(selectedCellAPI)) {
|
||||
selectedCellAPI.set(null)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove hovered row when a cell is selected
|
||||
selectedCellId.subscribe(cell => {
|
||||
focusedCellId.subscribe(cell => {
|
||||
if (cell && get(hoveredRowId)) {
|
||||
hoveredRowId.set(null)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
selectedCellId,
|
||||
focusedCellId,
|
||||
selectedRows,
|
||||
hoveredRowId,
|
||||
selectedCellRow,
|
||||
selectedCellAPI,
|
||||
focusedRow,
|
||||
rowHeight,
|
||||
ui: {
|
||||
actions: {
|
||||
|
|
|
@ -62,8 +62,8 @@ export const createUserStores = () => {
|
|||
([$enrichedUsers, $userId]) => {
|
||||
let map = {}
|
||||
$enrichedUsers.forEach(user => {
|
||||
if (user.selectedCellId && user.id !== $userId) {
|
||||
map[user.selectedCellId] = user
|
||||
if (user.focusedCellId && user.id !== $userId) {
|
||||
map[user.focusedCellId] = user
|
||||
}
|
||||
})
|
||||
return map
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const createValidationStores = () => {
|
||||
const validation = writable({})
|
||||
|
||||
return {
|
||||
validation: {
|
||||
subscribe: validation.subscribe,
|
||||
actions: {
|
||||
setError: (cellId, error) => {
|
||||
if (!cellId) {
|
||||
return
|
||||
}
|
||||
validation.update(state => ({
|
||||
...state,
|
||||
[cellId]: error,
|
||||
}))
|
||||
},
|
||||
getError: cellId => {
|
||||
return get(validation)[cellId]
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -77,10 +77,28 @@ export const createViewportStores = context => {
|
|||
[]
|
||||
)
|
||||
|
||||
const hiddenColumnsWidth = derived(
|
||||
[renderedColumns, visibleColumns],
|
||||
([$renderedColumns, $visibleColumns]) => {
|
||||
const idx = $visibleColumns.findIndex(
|
||||
col => col.name === $renderedColumns[0]?.name
|
||||
)
|
||||
let width = 0
|
||||
if (idx > 0) {
|
||||
for (let i = 0; i < idx; i++) {
|
||||
width += $visibleColumns[i].width
|
||||
}
|
||||
}
|
||||
return width
|
||||
},
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
scrolledRowCount,
|
||||
visualRowCapacity,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
hiddenColumnsWidth,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue