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