Improve sheet integration with data section and add horizontal cell inversion
This commit is contained in:
parent
724bff60f2
commit
69f6834886
|
@ -143,7 +143,7 @@
|
|||
}
|
||||
fields?.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
if (fieldSchema.width) {
|
||||
if (fieldSchema.width && typeof fieldSchema.width === "string") {
|
||||
style += ` ${fieldSchema.width}`
|
||||
} else {
|
||||
style += " minmax(auto, 1fr)"
|
||||
|
|
|
@ -21,7 +21,13 @@
|
|||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Sheet {API} tableId={id} allowAddRows={!isUsersTable}>
|
||||
<Sheet
|
||||
{API}
|
||||
tableId={id}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
on:updatetable={e => tables.updateTable(e.detail)}
|
||||
>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<SheetCreateViewButton />
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||
import { API } from "api"
|
||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
|
@ -22,7 +17,6 @@
|
|||
export let data = []
|
||||
export let tableId
|
||||
export let title
|
||||
export let allowEditing = false
|
||||
export let loading = false
|
||||
export let hideAutocolumns
|
||||
export let rowCount
|
||||
|
@ -32,12 +26,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
let selectedRows = []
|
||||
let editableColumn
|
||||
let editableRow
|
||||
let editRowModal
|
||||
let editColumnModal
|
||||
let customRenderers = []
|
||||
let confirmDelete
|
||||
|
||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
|
@ -92,36 +81,6 @@
|
|||
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
|
||||
)
|
||||
}
|
||||
|
||||
const deleteRows = async targetRows => {
|
||||
try {
|
||||
await API.deleteRows({
|
||||
tableId,
|
||||
rows: targetRows,
|
||||
})
|
||||
|
||||
const deletedRowIds = targetRows.map(row => row._id)
|
||||
data = data.filter(row => deletedRowIds.indexOf(row._id))
|
||||
|
||||
notifications.success(`Successfully deleted ${targetRows.length} rows`)
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting rows")
|
||||
}
|
||||
}
|
||||
|
||||
const editRow = row => {
|
||||
editableRow = row
|
||||
if (row) {
|
||||
editRowModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const editColumn = field => {
|
||||
editableColumn = cloneDeep(schema?.[field])
|
||||
if (editableColumn) {
|
||||
editColumnModal.show()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
|
@ -138,16 +97,6 @@
|
|||
{/if}
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
deleteRows={async rows => {
|
||||
await deleteRows(rows)
|
||||
resetSelectedRows()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
{#key tableId}
|
||||
|
@ -160,13 +109,7 @@
|
|||
{rowCount}
|
||||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
on:editcolumn={e => editColumn(e.detail)}
|
||||
on:editrow={e => editRow(e.detail)}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
>
|
||||
|
@ -176,42 +119,6 @@
|
|||
{/key}
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={editRowModal}>
|
||||
<svelte:component
|
||||
this={editRowComponent}
|
||||
on:updaterows
|
||||
on:deleteRows={() => {
|
||||
confirmDelete.show()
|
||||
}}
|
||||
row={editableRow}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDelete}
|
||||
okText="Delete"
|
||||
onOk={async () => {
|
||||
if (editableRow) {
|
||||
await deleteRows([editableRow])
|
||||
}
|
||||
editableRow = undefined
|
||||
}}
|
||||
onCancel={async () => {
|
||||
editRow(editableRow)
|
||||
}}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you want to delete this row?
|
||||
</ConfirmDialog>
|
||||
|
||||
<Modal bind:this={editColumnModal}>
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns
|
||||
onClosed={editColumnModal.hide}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.table-title {
|
||||
height: 24px;
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
{data}
|
||||
{loading}
|
||||
{type}
|
||||
allowEditing={false}
|
||||
rowCount={10}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
||||
export let highlighted = false
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
|
||||
export const show = () => modal?.show()
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
selected={highlighted}
|
||||
emphasized={highlighted}
|
||||
icon="TableColumnAddRight"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
Create column
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditColumn on:updatecolumns />
|
||||
</Modal>
|
|
@ -1,27 +0,0 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import CreateEditRow from "../modals/CreateEditRow.svelte"
|
||||
|
||||
export let modalContentComponent = CreateEditRow
|
||||
export let title = "Create row"
|
||||
export let disabled = false
|
||||
export let highlighted = false
|
||||
|
||||
let modal
|
||||
|
||||
export const show = () => modal?.show()
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
emphasized={highlighted}
|
||||
selected={highlighted}
|
||||
icon="TableRowAddBottom"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
{title}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<svelte:component this={modalContentComponent} on:updaterows />
|
||||
</Modal>
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Create view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
</Modal>
|
|
@ -1,10 +1,18 @@
|
|||
<script>
|
||||
import CreateViewButton from "../CreateViewButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../../modals/CreateViewModal.svelte"
|
||||
|
||||
const { rows, columns } = getContext("sheet")
|
||||
|
||||
let modal
|
||||
|
||||
$: disabled = !$columns.length || !$rows.length
|
||||
</script>
|
||||
|
||||
<CreateViewButton {disabled} />
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Create view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
</Modal>
|
||||
|
|
|
@ -11,7 +11,5 @@
|
|||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditColumn
|
||||
on:updatecolumns={() => rows.actions.refreshTableDefinition()}
|
||||
/>
|
||||
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
|
||||
</Modal>
|
||||
|
|
|
@ -2,16 +2,13 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import { Modal } from "@budibase/bbui"
|
||||
import CreateEditColumn from "../CreateEditColumn.svelte"
|
||||
import { tables } from "stores/backend"
|
||||
|
||||
const { rows, subscribe } = getContext("sheet")
|
||||
|
||||
let editableColumn
|
||||
let editColumnModal
|
||||
|
||||
const updateColumns = () => {
|
||||
rows.actions.refreshData()
|
||||
}
|
||||
|
||||
const editColumn = column => {
|
||||
editableColumn = column
|
||||
editColumnModal.show()
|
||||
|
@ -23,7 +20,6 @@
|
|||
<Modal bind:this={editColumnModal}>
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns={updateColumns}
|
||||
onClosed={editColumnModal.hide}
|
||||
on:updatecolumns={rows.actions.refreshData}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -135,6 +135,17 @@ export function createTablesStore() {
|
|||
await save(draft)
|
||||
}
|
||||
|
||||
const updateTable = table => {
|
||||
const index = get(store).list.findIndex(x => x._id === table._id)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
store.update(state => {
|
||||
state.list[index] = table
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
|
@ -145,6 +156,7 @@ export function createTablesStore() {
|
|||
delete: deleteTable,
|
||||
saveField,
|
||||
deleteField,
|
||||
updateTable,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
export let onChange
|
||||
export let readonly = false
|
||||
export let api
|
||||
export let invert = false
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
|
||||
const { API } = getContext("sheet")
|
||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
||||
|
@ -89,7 +90,7 @@
|
|||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="dropzone" class:invert>
|
||||
<div class="dropzone" class:invertX class:invertY>
|
||||
<Dropzone
|
||||
{value}
|
||||
compact
|
||||
|
@ -146,7 +147,11 @@
|
|||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||
padding: var(--cell-padding);
|
||||
}
|
||||
.dropzone.invert {
|
||||
.dropzone.invertX {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.dropzone.invertY {
|
||||
transform: translateY(-100%);
|
||||
top: 0;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
export let row
|
||||
export let cellId
|
||||
export let updateRow = rows.actions.updateRow
|
||||
export let invert = false
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
|
@ -74,6 +75,7 @@
|
|||
onChange={cellAPI.updateValue}
|
||||
{focused}
|
||||
{readonly}
|
||||
{invert}
|
||||
{invertY}
|
||||
{invertX}
|
||||
/>
|
||||
</SheetCell>
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
export let onChange
|
||||
export let readonly = false
|
||||
export let api
|
||||
export let invert = false
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
|
||||
let textarea
|
||||
let isOpen = false
|
||||
|
@ -49,7 +50,8 @@
|
|||
|
||||
{#if isOpen}
|
||||
<textarea
|
||||
class:invert
|
||||
class:invertX
|
||||
class:invertY
|
||||
bind:this={textarea}
|
||||
value={value || ""}
|
||||
on:change={handleChange}
|
||||
|
@ -91,12 +93,17 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100% + 100px);
|
||||
height: calc(5 * var(--row-height) + 1px);
|
||||
width: calc(100% + var(--max-cell-render-width-overflow));
|
||||
height: var(--max-cell-render-height);
|
||||
z-index: 1;
|
||||
border-radius: 2px;
|
||||
resize: none;
|
||||
}
|
||||
textarea.invert {
|
||||
textarea.invertX {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
textarea.invertY {
|
||||
transform: translateY(-100%);
|
||||
top: calc(100% + 1px);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
export let multi = false
|
||||
export let readonly = false
|
||||
export let api
|
||||
export let invert = false
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
|
||||
let isOpen = false
|
||||
let focusedOptionIdx = null
|
||||
|
@ -97,7 +98,12 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if isOpen}
|
||||
<div class="options" class:invert on:wheel={e => e.stopPropagation()}>
|
||||
<div
|
||||
class="options"
|
||||
class:invertX
|
||||
class:invertY
|
||||
on:wheel={e => e.stopPropagation()}
|
||||
>
|
||||
{#each options as option, idx}
|
||||
{@const color = getOptionColor(option)}
|
||||
<div
|
||||
|
@ -187,7 +193,11 @@
|
|||
overflow-y: auto;
|
||||
border: var(--cell-border);
|
||||
}
|
||||
.options.invert {
|
||||
.options.invertX {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.options.invertY {
|
||||
transform: translateY(-100%);
|
||||
top: 0;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
export let focused
|
||||
export let schema
|
||||
export let onChange
|
||||
export let invert = false
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
|
||||
const { API } = getContext("sheet")
|
||||
|
||||
|
@ -215,7 +216,7 @@
|
|||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="dropdown" class:invert on:wheel|stopPropagation>
|
||||
<div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation>
|
||||
<div class="search">
|
||||
<Input autofocus quiet type="text" bind:value={searchString} />
|
||||
</div>
|
||||
|
@ -288,7 +289,7 @@
|
|||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
max-width: calc(100% + 240px);
|
||||
max-width: calc(100% + var(--max-cell-render-width-overflow));
|
||||
max-height: var(--max-cell-render-height);
|
||||
background: var(--cell-background);
|
||||
border: var(--cell-border);
|
||||
|
@ -298,10 +299,14 @@
|
|||
align-items: stretch;
|
||||
background-color: var(--cell-background-hover);
|
||||
}
|
||||
.dropdown.invert {
|
||||
.dropdown.invertY {
|
||||
transform: translateY(-100%);
|
||||
top: 0;
|
||||
}
|
||||
.dropdown.invertX {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow-y: auto;
|
||||
|
@ -323,7 +328,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
.result .badge {
|
||||
max-width: 340px;
|
||||
max-width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.search {
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
import UserAvatars from "./UserAvatars.svelte"
|
||||
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
||||
import { clickOutside } from "@budibase/bbui"
|
||||
import SheetControls from "./SheetControls.svelte"
|
||||
import { MaxCellRenderHeight } from "../lib/constants"
|
||||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
} from "../lib/constants"
|
||||
import SortButton from "../controls/SortButton.svelte"
|
||||
import AddColumnButton from "../controls/AddColumnButton.svelte"
|
||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||
|
@ -25,11 +27,11 @@
|
|||
export let API
|
||||
export let tableId
|
||||
export let allowAddRows = true
|
||||
export let allowSelectRows = true
|
||||
export let allowAddColumns = true
|
||||
export let allowEditColumns = true
|
||||
export let allowExpandRows = true
|
||||
export let allowEditRows = true
|
||||
export let allowDeleteRows = true
|
||||
|
||||
// Sheet constants
|
||||
const gutterWidth = 72
|
||||
|
@ -39,11 +41,11 @@
|
|||
const tableIdStore = writable(tableId)
|
||||
const config = writable({
|
||||
allowAddRows,
|
||||
allowSelectRows,
|
||||
allowAddColumns,
|
||||
allowEditColumns,
|
||||
allowExpandRows,
|
||||
allowEditRows,
|
||||
allowDeleteRows,
|
||||
})
|
||||
|
||||
// Build up spreadsheet context
|
||||
|
@ -64,11 +66,11 @@
|
|||
$: tableIdStore.set(tableId)
|
||||
$: config.set({
|
||||
allowAddRows,
|
||||
allowSelectRows,
|
||||
allowAddColumns,
|
||||
allowEditColumns,
|
||||
allowExpandRows,
|
||||
allowEditRows,
|
||||
allowDeleteRows,
|
||||
})
|
||||
|
||||
// Set context for children to consume
|
||||
|
@ -86,13 +88,12 @@
|
|||
id="sheet-{rand}"
|
||||
class:is-resizing={$isResizing}
|
||||
class:is-reordering={$isReordering}
|
||||
style="--row-height:{$rowHeight}px; --gutter-width:{gutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px;"
|
||||
style="--row-height:{$rowHeight}px; --gutter-width:{gutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px;"
|
||||
>
|
||||
<div class="controls">
|
||||
<div class="controls-left">
|
||||
<AddRowButton />
|
||||
<AddColumnButton />
|
||||
<SheetControls />
|
||||
<slot name="controls" />
|
||||
<HideColumnsButton />
|
||||
<SortButton />
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
|
||||
import SheetRow from "./SheetRow.svelte"
|
||||
import { MaxCellRenderHeight } from "../lib/constants"
|
||||
|
||||
const { bounds, renderedRows, visualRowCapacity, rowHeight } =
|
||||
const { bounds, renderedRows, rowVerticalInversionIndex } =
|
||||
getContext("sheet")
|
||||
|
||||
let body
|
||||
|
||||
$: inversionIdx =
|
||||
$visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
|
||||
|
||||
onMount(() => {
|
||||
// Observe and record the height of the body
|
||||
const observer = new ResizeObserver(() => {
|
||||
|
@ -27,7 +23,7 @@
|
|||
<div bind:this={body} class="sheet-body">
|
||||
<SheetScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
|
||||
{#each $renderedRows as row, idx}
|
||||
<SheetRow {row} {idx} invert={idx >= inversionIdx} />
|
||||
<SheetRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
||||
{/each}
|
||||
</SheetScrollWrapper>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<script>
|
||||
import SortButton from "../controls/SortButton.svelte"
|
||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
||||
import AddColumnButton from "../controls/AddColumnButton.svelte"
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let row
|
||||
export let idx
|
||||
export let invert = false
|
||||
export let invertY = false
|
||||
|
||||
const {
|
||||
focusedCellId,
|
||||
|
@ -16,6 +16,7 @@
|
|||
selectedCellMap,
|
||||
focusedRow,
|
||||
hiddenColumnsWidth,
|
||||
columnHorizontalInversionIndex,
|
||||
} = getContext("sheet")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
|
@ -28,18 +29,18 @@
|
|||
|
||||
<div
|
||||
class="row"
|
||||
style={rowFocused ? foo : null}
|
||||
on:focus
|
||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
>
|
||||
{#each cols as column (column.name)}
|
||||
{#each $renderedColumns as column, idx (column.name)}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
{row}
|
||||
{invert}
|
||||
{invertY}
|
||||
invertX={idx >= $columnHorizontalInversionIndex}
|
||||
{rowFocused}
|
||||
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
||||
selected={rowSelected}
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
on:wheel={wheelInteractive ? handleWheel : null}
|
||||
on:click|self={() => ($focusedCellId = null)}
|
||||
>
|
||||
<div {style}>
|
||||
<div {style} class="inner">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,5 +79,13 @@
|
|||
.outer {
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.inner {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -61,8 +61,8 @@
|
|||
<SheetCell width={gutterWidth}>
|
||||
<div class="gutter">
|
||||
<div class="checkbox visible">
|
||||
{#if $config.allowSelectRows}
|
||||
<div on:click={$config.allowSelectRows && selectAll}>
|
||||
{#if $config.allowDeleteRows}
|
||||
<div on:click={selectAll}>
|
||||
<Checkbox
|
||||
value={rowCount && selectedRowCount === rowCount}
|
||||
disabled={!$renderedRows.length}
|
||||
|
@ -104,14 +104,14 @@
|
|||
<div
|
||||
on:click={() => selectRow(row._id)}
|
||||
class="checkbox"
|
||||
class:visible={$config.allowSelectRows &&
|
||||
class:visible={$config.allowDeleteRows &&
|
||||
(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} />
|
||||
</div>
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.allowSelectRows ||
|
||||
class:visible={!$config.allowDeleteRows ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
export const SheetPadding = 264
|
||||
export const MaxCellRenderHeight = 216
|
||||
export const MaxCellRenderWidthOverflow = 200
|
||||
export const ScrollBarSize = 8
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<Menu>
|
||||
<MenuItem
|
||||
icon="Delete"
|
||||
disabled={!$config.allowEditRows}
|
||||
disabled={!$config.allowDeleteRows}
|
||||
on:click={deleteRow}>Delete row</MenuItem
|
||||
>
|
||||
<MenuItem
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { domDebounce } from "../../../utils/utils"
|
||||
import { ScrollBarSize } from "../lib/constants"
|
||||
|
||||
const {
|
||||
scroll,
|
||||
|
@ -17,9 +18,6 @@
|
|||
height,
|
||||
} = getContext("sheet")
|
||||
|
||||
// Bar config
|
||||
const barOffset = 8
|
||||
|
||||
// State for dragging bars
|
||||
let initialMouse
|
||||
let initialScroll
|
||||
|
@ -28,17 +26,17 @@
|
|||
// Terminology is the same for both axes:
|
||||
// renderX - the space available to render the bar in, edge to edge
|
||||
// availX - the space available to render the bar in, until the edge
|
||||
$: renderHeight = $height - 2 * barOffset
|
||||
$: renderHeight = $height - 2 * ScrollBarSize
|
||||
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
|
||||
$: availHeight = renderHeight - barHeight
|
||||
$: barTop =
|
||||
barOffset + $rowHeight + availHeight * ($scrollTop / $maxScrollTop)
|
||||
ScrollBarSize + $rowHeight + availHeight * ($scrollTop / $maxScrollTop)
|
||||
|
||||
// Calculate H scrollbar size and offset
|
||||
$: renderWidth = $screenWidth - 2 * barOffset
|
||||
$: renderWidth = $screenWidth - 2 * ScrollBarSize
|
||||
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
||||
$: availWidth = renderWidth - barWidth
|
||||
$: barLeft = barOffset + availWidth * ($scrollLeft / $maxScrollLeft)
|
||||
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
||||
|
||||
// V scrollbar drag handlers
|
||||
const startVDragging = e => {
|
||||
|
@ -88,14 +86,14 @@
|
|||
{#if $showVScrollbar}
|
||||
<div
|
||||
class="v-scrollbar"
|
||||
style="top:{barTop}px; height:{barHeight}px;right:{barOffset}px;"
|
||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
||||
on:mousedown={startVDragging}
|
||||
/>
|
||||
{/if}
|
||||
{#if $showHScrollbar}
|
||||
<div
|
||||
class="h-scrollbar"
|
||||
style="left:{barLeft}px; width:{barWidth}px;bottom:{barOffset}px;"
|
||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
||||
on:mousedown={startHDragging}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -112,9 +110,11 @@
|
|||
opacity: 1;
|
||||
}
|
||||
.v-scrollbar {
|
||||
width: 8px;
|
||||
width: var(--size);
|
||||
right: var(--size);
|
||||
}
|
||||
.h-scrollbar {
|
||||
height: 8px;
|
||||
height: var(--size);
|
||||
bottom: var(--size);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export const DefaultColumnWidth = 200
|
||||
|
||||
|
@ -16,7 +17,6 @@ export const createStores = () => {
|
|||
const enriched = {
|
||||
...column,
|
||||
left: offset,
|
||||
order: idx,
|
||||
}
|
||||
if (column.visible) {
|
||||
offset += column.width
|
||||
|
@ -47,7 +47,7 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { table, gutterWidth, columns, stickyColumn } = context
|
||||
const { table, gutterWidth, columns, stickyColumn, API, dispatch } = context
|
||||
|
||||
// Merge new schema fields with existing schema in order to preserve widths
|
||||
table.subscribe($table => {
|
||||
|
@ -77,21 +77,13 @@ export const deriveStores = context => {
|
|||
// Update columns, removing extraneous columns and adding missing ones
|
||||
columns.set(
|
||||
fields
|
||||
.map(field => {
|
||||
// Check if there is an existing column with this name so we can keep
|
||||
// the width setting
|
||||
let existing = currentColumns.find(x => x.name === field)
|
||||
if (!existing && currentStickyColumn?.name === field) {
|
||||
existing = currentStickyColumn
|
||||
}
|
||||
return {
|
||||
name: field,
|
||||
width: existing?.width || schema[field].width || DefaultColumnWidth,
|
||||
schema: schema[field],
|
||||
visible: existing?.visible ?? true,
|
||||
order: schema[field].order,
|
||||
}
|
||||
})
|
||||
.map(field => ({
|
||||
name: field,
|
||||
width: schema[field].width || DefaultColumnWidth,
|
||||
schema: schema[field],
|
||||
visible: schema[field].visible ?? true,
|
||||
order: schema[field].order,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Sort by order first
|
||||
const orderA = a.order
|
||||
|
@ -136,5 +128,69 @@ export const deriveStores = context => {
|
|||
})
|
||||
})
|
||||
|
||||
return null
|
||||
// Updates a columns width
|
||||
const updateColumnWidth = async (columnName, width) => {
|
||||
const $table = get(table)
|
||||
await updateTable({
|
||||
...$table,
|
||||
schema: {
|
||||
...$table.schema,
|
||||
[columnName]: {
|
||||
...$table.schema[columnName],
|
||||
width,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Updates a columns visibility
|
||||
const updateColumnVisibility = async (columnName, visible) => {
|
||||
const $table = get(table)
|
||||
await updateTable({
|
||||
...$table,
|
||||
schema: {
|
||||
...$table.schema,
|
||||
[columnName]: {
|
||||
...$table.schema[columnName],
|
||||
visible,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Updates the orders of columns
|
||||
const updateColumnOrders = async newColumns => {
|
||||
const $table = get(table)
|
||||
const newSchema = cloneDeep($table.schema)
|
||||
Object.keys(newSchema).forEach(column => {
|
||||
const index = newColumns.indexOf(column)
|
||||
if (index !== -1) {
|
||||
newSchema[column].order = index
|
||||
} else {
|
||||
delete newSchema[column].order
|
||||
}
|
||||
})
|
||||
await updateTable({
|
||||
...$table,
|
||||
schema: newSchema,
|
||||
})
|
||||
}
|
||||
|
||||
// Updates the table definition
|
||||
const updateTable = async newTable => {
|
||||
table.set(newTable)
|
||||
dispatch("updatetable", newTable)
|
||||
await API.saveTable(newTable)
|
||||
}
|
||||
|
||||
return {
|
||||
columns: {
|
||||
...columns,
|
||||
actions: {
|
||||
updateColumnWidth,
|
||||
updateColumnVisibility,
|
||||
updateColumnOrders,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,19 +24,28 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { reorder, columns, scroll, bounds, stickyColumn, ui, table, API } =
|
||||
context
|
||||
const {
|
||||
reorder,
|
||||
columns,
|
||||
visibleColumns,
|
||||
scroll,
|
||||
bounds,
|
||||
stickyColumn,
|
||||
ui,
|
||||
table,
|
||||
API,
|
||||
} = context
|
||||
|
||||
// Callback when dragging on a colum header and starting reordering
|
||||
const startReordering = (column, e) => {
|
||||
const $columns = get(columns)
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
const $bounds = get(bounds)
|
||||
const $scroll = get(scroll)
|
||||
const $stickyColumn = get(stickyColumn)
|
||||
ui.actions.blur()
|
||||
|
||||
// Generate new breakpoints for the current columns
|
||||
let breakpoints = $columns.map(col => ({
|
||||
let breakpoints = $visibleColumns.map(col => ({
|
||||
x: col.left + col.width,
|
||||
column: col.name,
|
||||
}))
|
||||
|
@ -93,6 +102,22 @@ export const deriveStores = context => {
|
|||
const stopReordering = async () => {
|
||||
// Swap position of columns
|
||||
let { sourceColumn, targetColumn } = get(reorder)
|
||||
moveColumn(sourceColumn, targetColumn)
|
||||
|
||||
// Reset state
|
||||
reorder.set(reorderInitialState)
|
||||
|
||||
// Remove event handlers
|
||||
document.removeEventListener("mousemove", onReorderMouseMove)
|
||||
document.removeEventListener("mouseup", stopReordering)
|
||||
|
||||
// Save column changes
|
||||
await saveOrderChanges()
|
||||
}
|
||||
|
||||
// Moves a column after another columns.
|
||||
// An undefined target column will move the source to index 0.
|
||||
const moveColumn = (sourceColumn, targetColumn) => {
|
||||
let $columns = get(columns)
|
||||
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
||||
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
||||
|
@ -105,61 +130,31 @@ export const deriveStores = context => {
|
|||
state.splice(targetIdx, 0, removed[0])
|
||||
return state.slice()
|
||||
})
|
||||
|
||||
// Reset state
|
||||
reorder.set(reorderInitialState)
|
||||
|
||||
// Remove event handlers
|
||||
document.removeEventListener("mousemove", onReorderMouseMove)
|
||||
document.removeEventListener("mouseup", stopReordering)
|
||||
|
||||
// Persist changes
|
||||
await saveOrderChanges()
|
||||
}
|
||||
|
||||
// Moves a column one place left (as appears visually)
|
||||
const moveColumnLeft = async column => {
|
||||
const $columns = get(columns)
|
||||
const sourceIdx = $columns.findIndex(x => x.name === column)
|
||||
if (sourceIdx === 0) {
|
||||
return
|
||||
}
|
||||
columns.update(state => {
|
||||
let tmp = state[sourceIdx]
|
||||
state[sourceIdx] = state[sourceIdx - 1]
|
||||
state[sourceIdx - 1] = tmp
|
||||
return state.slice()
|
||||
})
|
||||
|
||||
// Persist changes
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
||||
moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
||||
await saveOrderChanges()
|
||||
}
|
||||
|
||||
// Moves a column one place right (as appears visually)
|
||||
const moveColumnRight = async column => {
|
||||
const $columns = get(columns)
|
||||
const sourceIdx = $columns.findIndex(x => x.name === column)
|
||||
if (sourceIdx === $columns.length - 1) {
|
||||
const $visibleColumns = get(visibleColumns)
|
||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
||||
if (sourceIdx === $visibleColumns.length - 1) {
|
||||
return
|
||||
}
|
||||
columns.update(state => {
|
||||
let tmp = state[sourceIdx]
|
||||
state[sourceIdx] = state[sourceIdx + 1]
|
||||
state[sourceIdx + 1] = tmp
|
||||
return state.slice()
|
||||
})
|
||||
|
||||
// Persist changes
|
||||
moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
||||
await saveOrderChanges()
|
||||
}
|
||||
|
||||
// Saves order changes as part of table metadata
|
||||
const saveOrderChanges = async () => {
|
||||
const $table = cloneDeep(get(table))
|
||||
const $columns = get(columns)
|
||||
$columns.forEach(column => {
|
||||
$table.schema[column.name].order = column.order
|
||||
})
|
||||
const newTable = await API.saveTable($table)
|
||||
table.set(newTable)
|
||||
const newOrder = get(columns).map(column => column.name)
|
||||
await columns.actions.updateColumnOrders(newOrder)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -23,7 +23,7 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { resize, columns, stickyColumn, ui, table, API, rows } = context
|
||||
const { resize, columns, stickyColumn, ui } = context
|
||||
|
||||
// Starts resizing a certain column
|
||||
const startResizing = (column, e) => {
|
||||
|
@ -93,41 +93,25 @@ export const deriveStores = context => {
|
|||
|
||||
// Persist width if it changed
|
||||
if ($resize.width !== $resize.initialWidth) {
|
||||
await saveNewColumnWidth($resize.column, $resize.width)
|
||||
await columns.actions.updateColumnWidth($resize.column, $resize.width)
|
||||
}
|
||||
}
|
||||
|
||||
// Resets a column size back to default
|
||||
const resetSize = async column => {
|
||||
let columnIdx = get(columns).findIndex(col => col.name === column.name)
|
||||
if (columnIdx === -1) {
|
||||
stickyColumn.update(state => ({
|
||||
...state,
|
||||
width: DefaultColumnWidth,
|
||||
}))
|
||||
} else {
|
||||
columns.update(state => {
|
||||
state[columnIdx].width = DefaultColumnWidth
|
||||
return [...state]
|
||||
})
|
||||
}
|
||||
await saveNewColumnWidth(column.name, DefaultColumnWidth)
|
||||
}
|
||||
|
||||
// Saves a new column width as part of table metadata
|
||||
const saveNewColumnWidth = async (columnName, width) => {
|
||||
const $table = get(table)
|
||||
const newDefinition = await API.saveTable({
|
||||
...$table,
|
||||
schema: {
|
||||
...$table.schema,
|
||||
[columnName]: {
|
||||
...$table.schema[columnName],
|
||||
width,
|
||||
},
|
||||
},
|
||||
})
|
||||
table.set(newDefinition)
|
||||
// let columnIdx = get(columns).findIndex(col => col.name === column.name)
|
||||
// if (columnIdx === -1) {
|
||||
// stickyColumn.update(state => ({
|
||||
// ...state,
|
||||
// width: DefaultColumnWidth,
|
||||
// }))
|
||||
// } else {
|
||||
// columns.update(state => {
|
||||
// state[columnIdx].width = DefaultColumnWidth
|
||||
// return [...state]
|
||||
// })
|
||||
// }
|
||||
await columns.actions.updateColumnWidth(column.name, DefaultColumnWidth)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -121,6 +121,8 @@ export const deriveStores = context => {
|
|||
instanceLoaded.set(true)
|
||||
scroll.set({ top: 0, left: 0 })
|
||||
} else if (resetRows) {
|
||||
table.set($fetch.definition)
|
||||
|
||||
// Only reset top scroll position when resetting rows
|
||||
scroll.update(state => ({ ...state, top: 0 }))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tick } from "svelte"
|
||||
import { DefaultColumnWidth } from "./columns"
|
||||
import { SheetPadding } from "../lib/constants"
|
||||
|
||||
export const createStores = () => {
|
||||
const scroll = writable({
|
||||
|
@ -35,7 +35,6 @@ export const deriveStores = context => {
|
|||
width,
|
||||
height,
|
||||
} = context
|
||||
const padding = 264
|
||||
|
||||
// Memoize store primitives
|
||||
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
|
||||
|
@ -43,7 +42,7 @@ export const deriveStores = context => {
|
|||
// Derive vertical limits
|
||||
const contentHeight = derived(
|
||||
[rows, rowHeight],
|
||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding,
|
||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + SheetPadding,
|
||||
0
|
||||
)
|
||||
const maxScrollTop = derived(
|
||||
|
@ -56,7 +55,7 @@ export const deriveStores = context => {
|
|||
const contentWidth = derived(
|
||||
[visibleColumns, stickyColumnWidth],
|
||||
([$visibleColumns, $stickyColumnWidth]) => {
|
||||
let width = gutterWidth + padding + $stickyColumnWidth
|
||||
let width = gutterWidth + SheetPadding + $stickyColumnWidth
|
||||
$visibleColumns.forEach(col => {
|
||||
width += col.width
|
||||
})
|
||||
|
@ -146,7 +145,7 @@ export const deriveStores = context => {
|
|||
const $visibleColumns = get(visibleColumns)
|
||||
const columnName = $focusedCellId?.split("-")[1]
|
||||
const column = $visibleColumns.find(col => col.name === columnName)
|
||||
const horizontalOffset = DefaultColumnWidth
|
||||
const horizontalOffset = 50
|
||||
if (!column) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { derived, get } from "svelte/store"
|
||||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
ScrollBarSize,
|
||||
} from "../lib/constants"
|
||||
|
||||
export const deriveStores = context => {
|
||||
const {
|
||||
|
@ -96,11 +101,41 @@ export const deriveStores = context => {
|
|||
0
|
||||
)
|
||||
|
||||
// Determine the row index at which we should start vertically inverting cell
|
||||
// dropdowns
|
||||
const rowVerticalInversionIndex = derived(
|
||||
[visualRowCapacity, rowHeight],
|
||||
([$visualRowCapacity, $rowHeight]) => {
|
||||
return (
|
||||
$visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Determine the column index at which we should start horizontally inverting
|
||||
// cell dropdowns
|
||||
const columnHorizontalInversionIndex = derived(
|
||||
[renderedColumns, scrollLeft, width],
|
||||
([$renderedColumns, $scrollLeft, $width]) => {
|
||||
const cutoff = $width + $scrollLeft - ScrollBarSize * 3
|
||||
let inversionIdx = $renderedColumns.length
|
||||
for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
||||
const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
|
||||
if (rightEdge + MaxCellRenderWidthOverflow < cutoff) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return inversionIdx
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
scrolledRowCount,
|
||||
visualRowCapacity,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
hiddenColumnsWidth,
|
||||
rowVerticalInversionIndex,
|
||||
columnHorizontalInversionIndex,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ export interface FieldSchema {
|
|||
timeOnly?: boolean
|
||||
lastID?: number
|
||||
useRichText?: boolean | null
|
||||
order?: number
|
||||
width?: number
|
||||
meta?: {
|
||||
toTable: string
|
||||
toKey: string
|
||||
|
|
Loading…
Reference in New Issue