Migrate sheet to data section, improve reordering and reszing
This commit is contained in:
parent
e9b801e205
commit
fae24276f9
|
@ -21,7 +21,7 @@
|
|||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { fetchData, Sheet } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
||||
let hideAutocolumns = true
|
||||
|
@ -153,112 +153,122 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<Table
|
||||
title={$tables.selected?.name}
|
||||
schema={enrichedSchema}
|
||||
{type}
|
||||
tableId={id}
|
||||
data={$fetch.rows}
|
||||
bind:hideAutocolumns
|
||||
loading={!$fetch.loaded}
|
||||
on:sort={onSort}
|
||||
allowEditing
|
||||
disableSorting
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
on:updaterows={onUpdateRows}
|
||||
on:selectionUpdated={e => {
|
||||
selectedRows = e.detail
|
||||
}}
|
||||
customPlaceholder
|
||||
>
|
||||
<div class="buttons">
|
||||
<div class="left-buttons">
|
||||
<CreateColumnButton
|
||||
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
on:updaterows={onUpdateRows}
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
disabled={!hasCols}
|
||||
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||
/>
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right-buttons">
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<ExistingRelationshipButton
|
||||
table={$tables.selected}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<ImportButton
|
||||
disabled={$tables.selected?._id === "ta_users"}
|
||||
tableId={$tables.selected?._id}
|
||||
on:importrows={onImportData}
|
||||
/>
|
||||
<ExportButton
|
||||
disabled={!hasRows || !hasCols}
|
||||
view={$tables.selected?._id}
|
||||
filters={appliedFilter}
|
||||
sorting={appliedSort}
|
||||
{selectedRows}
|
||||
/>
|
||||
{#key id}
|
||||
<TableFilterButton
|
||||
{schema}
|
||||
{filters}
|
||||
on:change={onFilter}
|
||||
disabled={!hasCols}
|
||||
tableId={id}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="placeholder">
|
||||
<Layout gap="S">
|
||||
{#if !hasCols}
|
||||
<Heading>Let's create some columns</Heading>
|
||||
<Body>
|
||||
Start building out your table structure<br />
|
||||
by adding some columns
|
||||
</Body>
|
||||
{:else}
|
||||
<Heading>Now let's add a row</Heading>
|
||||
<Body>
|
||||
Add some data to your table<br />
|
||||
by adding some rows
|
||||
</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</Table>
|
||||
{#key id}
|
||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$fetch.pageNumber + 1}
|
||||
hasPrevPage={$fetch.hasPrevPage}
|
||||
hasNextPage={$fetch.hasNextPage}
|
||||
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
|
||||
goToNextPage={$fetch.loading ? null : fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
<Sheet tableId={$tables.selected?._id} {API} />
|
||||
</div>
|
||||
|
||||
<!--<div>-->
|
||||
<!-- <Table-->
|
||||
<!-- title={$tables.selected?.name}-->
|
||||
<!-- schema={enrichedSchema}-->
|
||||
<!-- {type}-->
|
||||
<!-- tableId={id}-->
|
||||
<!-- data={$fetch.rows}-->
|
||||
<!-- bind:hideAutocolumns-->
|
||||
<!-- loading={!$fetch.loaded}-->
|
||||
<!-- on:sort={onSort}-->
|
||||
<!-- allowEditing-->
|
||||
<!-- disableSorting-->
|
||||
<!-- on:updatecolumns={onUpdateColumns}-->
|
||||
<!-- on:updaterows={onUpdateRows}-->
|
||||
<!-- on:selectionUpdated={e => {-->
|
||||
<!-- selectedRows = e.detail-->
|
||||
<!-- }}-->
|
||||
<!-- customPlaceholder-->
|
||||
<!-- >-->
|
||||
<!-- <div class="buttons">-->
|
||||
<!-- <div class="left-buttons">-->
|
||||
<!-- <CreateColumnButton-->
|
||||
<!-- highlighted={$fetch.loaded && (!hasCols || !hasRows)}-->
|
||||
<!-- on:updatecolumns={onUpdateColumns}-->
|
||||
<!-- />-->
|
||||
<!-- {#if !isUsersTable}-->
|
||||
<!-- <CreateRowButton-->
|
||||
<!-- on:updaterows={onUpdateRows}-->
|
||||
<!-- title={"Create row"}-->
|
||||
<!-- modalContentComponent={CreateEditRow}-->
|
||||
<!-- disabled={!hasCols}-->
|
||||
<!-- highlighted={$fetch.loaded && hasCols && !hasRows}-->
|
||||
<!-- />-->
|
||||
<!-- {/if}-->
|
||||
<!-- {#if isInternal}-->
|
||||
<!-- <CreateViewButton disabled={!hasCols || !hasRows} />-->
|
||||
<!-- {/if}-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="right-buttons">-->
|
||||
<!-- <ManageAccessButton resourceId={$tables.selected?._id} />-->
|
||||
<!-- {#if isUsersTable}-->
|
||||
<!-- <EditRolesButton />-->
|
||||
<!-- {/if}-->
|
||||
<!-- {#if !isInternal}-->
|
||||
<!-- <ExistingRelationshipButton-->
|
||||
<!-- table={$tables.selected}-->
|
||||
<!-- on:updatecolumns={onUpdateColumns}-->
|
||||
<!-- />-->
|
||||
<!-- {/if}-->
|
||||
<!-- <HideAutocolumnButton bind:hideAutocolumns />-->
|
||||
<!-- <ImportButton-->
|
||||
<!-- disabled={$tables.selected?._id === "ta_users"}-->
|
||||
<!-- tableId={$tables.selected?._id}-->
|
||||
<!-- on:importrows={onImportData}-->
|
||||
<!-- />-->
|
||||
<!-- <ExportButton-->
|
||||
<!-- disabled={!hasRows || !hasCols}-->
|
||||
<!-- view={$tables.selected?._id}-->
|
||||
<!-- filters={appliedFilter}-->
|
||||
<!-- sorting={appliedSort}-->
|
||||
<!-- {selectedRows}-->
|
||||
<!-- />-->
|
||||
<!-- {#key id}-->
|
||||
<!-- <TableFilterButton-->
|
||||
<!-- {schema}-->
|
||||
<!-- {filters}-->
|
||||
<!-- on:change={onFilter}-->
|
||||
<!-- disabled={!hasCols}-->
|
||||
<!-- tableId={id}-->
|
||||
<!-- />-->
|
||||
<!-- {/key}-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div slot="placeholder">-->
|
||||
<!-- <Layout gap="S">-->
|
||||
<!-- {#if !hasCols}-->
|
||||
<!-- <Heading>Let's create some columns</Heading>-->
|
||||
<!-- <Body>-->
|
||||
<!-- Start building out your table structure<br />-->
|
||||
<!-- by adding some columns-->
|
||||
<!-- </Body>-->
|
||||
<!-- {:else}-->
|
||||
<!-- <Heading>Now let's add a row</Heading>-->
|
||||
<!-- <Body>-->
|
||||
<!-- Add some data to your table<br />-->
|
||||
<!-- by adding some rows-->
|
||||
<!-- </Body>-->
|
||||
<!-- {/if}-->
|
||||
<!-- </Layout>-->
|
||||
<!-- </div>-->
|
||||
<!-- </Table>-->
|
||||
<!-- {#key id}-->
|
||||
<!-- <div in:fade={{ delay: 200, duration: 100 }}>-->
|
||||
<!-- <div class="pagination">-->
|
||||
<!-- <Pagination-->
|
||||
<!-- page={$fetch.pageNumber + 1}-->
|
||||
<!-- hasPrevPage={$fetch.hasPrevPage}-->
|
||||
<!-- hasNextPage={$fetch.hasNextPage}-->
|
||||
<!-- goToPrevPage={$fetch.loading ? null : fetch.prevPage}-->
|
||||
<!-- goToNextPage={$fetch.loading ? null : fetch.nextPage}-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- {/key}-->
|
||||
|
||||
<!--</div>-->
|
||||
<style>
|
||||
div {
|
||||
flex: 1 1 auto;
|
||||
margin: -28px -40px -40px -40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { reorderPlaceholder } = getContext("spreadsheet")
|
||||
|
||||
$: style = getStyle($reorderPlaceholder)
|
||||
|
||||
const getStyle = state => {
|
||||
return (
|
||||
`--x:${state.x}px;` +
|
||||
`--width:${state.width}px;` +
|
||||
`--height:${state.height}px;`
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $reorderPlaceholder.x != null}
|
||||
<div {style} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div {
|
||||
height: min(calc(100% - 36px), var(--height));
|
||||
width: var(--width);
|
||||
left: var(--x);
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
background: var(--spectrum-global-color-blue-400);
|
||||
opacity: 0.2;
|
||||
z-index: 7;
|
||||
}
|
||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let columnIdx
|
||||
|
||||
const { resize } = getContext("spreadsheet")
|
||||
</script>
|
||||
|
||||
<div on:mousedown={e => resize.actions.startResizing(columnIdx, e)} />
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
div:after {
|
||||
opacity: 0;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--spectrum-global-color-gray-600);
|
||||
transition: opacity 130ms ease-out;
|
||||
}
|
||||
div:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
div:hover:after {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +0,0 @@
|
|||
<script>
|
||||
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
|
||||
</script>
|
||||
|
||||
<SpreadsheetCell
|
||||
{...$$props}
|
||||
spacer
|
||||
on:click={() => ($selectedCellId = null)}
|
||||
on:mouseenter={() => ($hoveredRowId = null)}
|
||||
/>
|
|
@ -1,108 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
|
||||
const {
|
||||
selectedRows,
|
||||
rows,
|
||||
selectedCellId,
|
||||
hoveredRowId,
|
||||
tableId,
|
||||
spreadsheetAPI,
|
||||
} = getContext("spreadsheet")
|
||||
const { API, confirmationStore } = getContext("sdk")
|
||||
|
||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||
$: rowCount = $rows.length
|
||||
|
||||
const deleteRows = () => {
|
||||
// Fetch full row objects to be deleted
|
||||
const rowsToDelete = Object.entries($selectedRows)
|
||||
.map(entry => {
|
||||
if (entry[1] === true) {
|
||||
return $rows.find(x => x._id === entry[0])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(x => x != null)
|
||||
|
||||
// Deletion callback when confirmed
|
||||
const performDeletion = async () => {
|
||||
await API.deleteRows({
|
||||
tableId: $tableId,
|
||||
rows: rowsToDelete,
|
||||
})
|
||||
await spreadsheetAPI.refreshData()
|
||||
|
||||
// Refresh state
|
||||
$selectedCellId = null
|
||||
$hoveredRowId = null
|
||||
$selectedRows = {}
|
||||
}
|
||||
|
||||
// Show confirmation
|
||||
confirmationStore.actions.showConfirmation(
|
||||
"Delete rows",
|
||||
`Are you sure you want to delete ${selectedRowCount} row${
|
||||
selectedRowCount === 1 ? "" : "s"
|
||||
}?`,
|
||||
performDeletion
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<ActionButton icon="Filter" size="S">Filter</ActionButton>
|
||||
<ActionButton icon="Group" size="S">Group</ActionButton>
|
||||
<ActionButton icon="SortOrderDown" size="S">Sort</ActionButton>
|
||||
<ActionButton icon="VisibilityOff" size="S">Hide fields</ActionButton>
|
||||
</div>
|
||||
<div class="title">Sales Records</div>
|
||||
<div class="delete">
|
||||
{#if selectedRowCount}
|
||||
<ActionButton icon="Delete" size="S" on:click={deleteRows}>
|
||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||
</ActionButton>
|
||||
{:else}
|
||||
{rowCount} row{rowCount === 1 ? "" : "s"}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--cell-spacing);
|
||||
}
|
||||
.delete {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.delete :global(.spectrum-ActionButton) {
|
||||
color: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
.delete :global(.spectrum-Icon) {
|
||||
fill: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
|
||||
</script>
|
||||
|
||||
<div
|
||||
on:click={() => ($selectedCellId = null)}
|
||||
on:mouseenter={() => ($hoveredRowId = null)}
|
||||
/>
|
||||
|
||||
<style>
|
||||
div {
|
||||
grid-column: 1/-1;
|
||||
height: 180px;
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
export { default as spreadsheet } from "./Spreadsheet.svelte"
|
|
@ -1,2 +1,3 @@
|
|||
export { default as SplitPage } from "./SplitPage.svelte"
|
||||
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
||||
export { default as Sheet } from "./sheet/Sheet.svelte"
|
||||
|
|
|
@ -1,26 +1,16 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { domDebounce } from "../../../../utils/domDebounce.js"
|
||||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const MinColumnWidth = 100
|
||||
const { columns, rand, visibleColumns } = getContext("spreadsheet")
|
||||
const MinColumnWidth = 100
|
||||
|
||||
export const createResizeStore = context => {
|
||||
const { columns, rand } = context
|
||||
const initialState = {
|
||||
initialMouseX: null,
|
||||
initialWidth: null,
|
||||
columnIdx: null,
|
||||
}
|
||||
|
||||
let initialWidth = 0
|
||||
let initialMouseX = null
|
||||
let initialWidth = null
|
||||
let columnIdx = null
|
||||
let width = 0
|
||||
let left = 0
|
||||
let initialMouseX = null
|
||||
let sheet
|
||||
let columnIdx = 0
|
||||
let columnCount = 0
|
||||
let styles
|
||||
|
||||
const resize = writable(initialState)
|
||||
|
||||
const startResizing = (idx, e) => {
|
||||
const $columns = get(columns)
|
||||
|
@ -37,6 +27,7 @@ export const createResizeStore = context => {
|
|||
// Add mouse event listeners to handle resizing
|
||||
document.addEventListener("mousemove", onResizeMouseMove)
|
||||
document.addEventListener("mouseup", stopResizing)
|
||||
document.getElementById(`sheet-${rand}`).classList.add("is-resizing")
|
||||
}
|
||||
|
||||
const onResizeMouseMove = e => {
|
||||
|
@ -68,7 +59,6 @@ export const createResizeStore = context => {
|
|||
//
|
||||
// sheet.style.cssText += newStyle
|
||||
|
||||
|
||||
// let cells = sheet.querySelectorAll(`[data-col="${columnIdx}"]`)
|
||||
// let left
|
||||
// cells.forEach(cell => {
|
||||
|
@ -93,8 +83,6 @@ export const createResizeStore = context => {
|
|||
// offset += colWidth
|
||||
// }
|
||||
|
||||
|
||||
|
||||
width = newWidth
|
||||
|
||||
// Update width of column
|
||||
|
@ -113,15 +101,53 @@ export const createResizeStore = context => {
|
|||
}
|
||||
|
||||
const stopResizing = () => {
|
||||
resize.set(initialState)
|
||||
columnIdx = null
|
||||
document.removeEventListener("mousemove", onResizeMouseMove)
|
||||
document.removeEventListener("mouseup", stopResizing)
|
||||
document.getElementById(`sheet-${rand}`).classList.remove("is-resizing")
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each $columns as col}
|
||||
<div
|
||||
class="resize-slider"
|
||||
class:visible={columnIdx === col.idx}
|
||||
on:mousedown={e => startResizing(col.idx, e)}
|
||||
style="--left:{col.left + col.width}px;"
|
||||
>
|
||||
<div class="resize-indicator" />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.resize-slider {
|
||||
position: absolute;
|
||||
top: var(--controls-height);
|
||||
z-index: 6;
|
||||
height: var(--cell-height);
|
||||
left: var(--left);
|
||||
opacity: 0;
|
||||
padding: 0 16px;
|
||||
transform: translateX(-50%);
|
||||
user-select: none;
|
||||
}
|
||||
.resize-slider:hover,
|
||||
.resize-slider.visible {
|
||||
cursor: col-resize;
|
||||
opacity: 1;
|
||||
height: calc(100% - var(--controls-height));
|
||||
}
|
||||
.resize-indicator {
|
||||
margin-left: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: var(--spectrum-global-color-blue-400);
|
||||
}
|
||||
|
||||
return {
|
||||
...resize,
|
||||
actions: {
|
||||
startResizing,
|
||||
},
|
||||
:global(.sheet.is-resizing *) {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
}
|
||||
:global(.sheet.is-reordering .resize-slider) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
|
@ -1,27 +1,24 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||
import { fetchData } from "../../fetch/fetchData"
|
||||
import { LuceneUtils } from "../../utils"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createReorderStores } from "./stores/reorder"
|
||||
import { createResizeStore } from "./stores/resize"
|
||||
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
||||
import ResizeSlider from "./ResizeSlider.svelte"
|
||||
import SpreadsheetHeader from "./SpreadsheetHeader.svelte"
|
||||
import SpreadsheetBody from "./SpreadsheetBody.svelte"
|
||||
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||
import SpreadsheetRow from "./SpreadsheetRow.svelte"
|
||||
import SpreadsheetHeader from "./SheetHeader.svelte"
|
||||
import SpreadsheetBody from "./SheetBody.svelte"
|
||||
import SpreadsheetCell from "./SheetCell.svelte"
|
||||
import SpreadsheetRow from "./SheetRow.svelte"
|
||||
import ResizeOverlay from "./ResizeOverlay.svelte"
|
||||
|
||||
export let table
|
||||
export let tableId
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
|
||||
const { styleable, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
export let API
|
||||
|
||||
// Sheet constants
|
||||
const cellHeight = 32
|
||||
const cellHeight = 36
|
||||
const limit = 100
|
||||
const defaultWidth = 160
|
||||
const rand = Math.random()
|
||||
|
@ -32,7 +29,6 @@
|
|||
const hoveredRowId = writable(null)
|
||||
const selectedCellId = writable(null)
|
||||
const selectedRows = writable({})
|
||||
const tableId = writable(table?.tableId)
|
||||
const changeCache = writable({})
|
||||
const newRows = writable([])
|
||||
const visibleRows = writable([0, 0])
|
||||
|
@ -50,6 +46,7 @@
|
|||
|
||||
// Build up spreadsheet context and additional stores
|
||||
const context = {
|
||||
API,
|
||||
rand,
|
||||
rows,
|
||||
columns,
|
||||
|
@ -66,11 +63,9 @@
|
|||
scroll,
|
||||
}
|
||||
const { reorder, reorderPlaceholder } = createReorderStores(context)
|
||||
const resize = createResizeStore(context)
|
||||
|
||||
$: tableId.set(table?.tableId)
|
||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||
$: fetch = createFetch(table)
|
||||
$: fetch = createFetch(tableId)
|
||||
$: fetch.update({
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
|
@ -83,10 +78,13 @@
|
|||
$: updateSortedRows($fetch, $newRows)
|
||||
$: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1])
|
||||
|
||||
const createFetch = datasource => {
|
||||
const createFetch = tableId => {
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
datasource: {
|
||||
type: "table",
|
||||
tableId,
|
||||
},
|
||||
options: {
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
|
@ -99,23 +97,34 @@
|
|||
|
||||
// Generates the column array the first time the schema loads
|
||||
const generateColumns = ({ schema, definition }) => {
|
||||
if (!$columns.length && schema) {
|
||||
let fields = Object.keys(schema || {})
|
||||
const primaryDisplay = definition?.primaryDisplay
|
||||
if (primaryDisplay) {
|
||||
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
|
||||
}
|
||||
$columns = fields.map((field, idx) => {
|
||||
return {
|
||||
idx,
|
||||
name: field,
|
||||
width: defaultWidth,
|
||||
left: 40 + idx * defaultWidth,
|
||||
schema: schema[field],
|
||||
primaryDisplay: field === primaryDisplay,
|
||||
}
|
||||
})
|
||||
if (!schema) {
|
||||
$columns = []
|
||||
return
|
||||
}
|
||||
const currentColumns = $columns
|
||||
|
||||
// Get fields in new schema
|
||||
let fields = Object.keys(schema || {})
|
||||
const primaryDisplay = definition?.primaryDisplay
|
||||
if (primaryDisplay) {
|
||||
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
|
||||
}
|
||||
|
||||
// Update columns, removing extraneous columns and adding missing ones
|
||||
let offset = 40
|
||||
$columns = fields.map((field, idx) => {
|
||||
const existing = currentColumns.find(x => x.name === field)
|
||||
const newCol = {
|
||||
idx,
|
||||
name: field,
|
||||
width: existing?.width || defaultWidth,
|
||||
left: offset,
|
||||
schema: schema[field],
|
||||
primaryDisplay: field === primaryDisplay,
|
||||
}
|
||||
offset += newCol.width
|
||||
return newCol
|
||||
})
|
||||
}
|
||||
|
||||
const getIconForField = field => {
|
||||
|
@ -174,7 +183,7 @@
|
|||
|
||||
const updateSortedRows = (unsortedRows, newRows) => {
|
||||
let foo = unsortedRows.rows
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 0; i++) {
|
||||
foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" })))
|
||||
}
|
||||
let sortedRows = foo.slice()
|
||||
|
@ -197,110 +206,98 @@
|
|||
...context,
|
||||
reorder,
|
||||
reorderPlaceholder,
|
||||
resize,
|
||||
spreadsheetAPI,
|
||||
})
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<div
|
||||
class="wrapper"
|
||||
class:resize={$resize.columnIdx != null}
|
||||
style="--cell-height:{cellHeight}px;"
|
||||
id="sheet-{rand}"
|
||||
>
|
||||
<SpreadsheetHeader />
|
||||
<SpreadsheetBody>
|
||||
<div class="row">
|
||||
<!-- Field headers -->
|
||||
<SpreadsheetCell header label on:click={selectAll} width="40" left="0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rowCount && selectedRowCount === rowCount}
|
||||
/>
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<SpreadsheetCell
|
||||
header
|
||||
sticky={fieldIdx === 0}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
|
||||
width={field.width}
|
||||
left={field.left}
|
||||
>
|
||||
<Icon
|
||||
size="S"
|
||||
name={getIconForField(field)}
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
<span>
|
||||
{field.name}
|
||||
</span>
|
||||
<ResizeSlider columnIdx={fieldIdx} />
|
||||
</SpreadsheetCell>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- All real rows -->
|
||||
{#each renderedRows as row, rowIdx (row._id)}
|
||||
<SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} />
|
||||
{/each}
|
||||
|
||||
<!-- New row placeholder -->
|
||||
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
|
||||
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
|
||||
<SpreadsheetHeader />
|
||||
<SpreadsheetBody>
|
||||
<div class="row">
|
||||
<!-- Field headers -->
|
||||
<SpreadsheetCell header label on:click={selectAll} width="40" left="0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rowCount && selectedRowCount === rowCount}
|
||||
/>
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<SpreadsheetCell
|
||||
label
|
||||
on:click={addRow}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
width="40"
|
||||
left="0"
|
||||
header
|
||||
sticky={fieldIdx === 0}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
|
||||
width={field.width}
|
||||
left={field.left}
|
||||
>
|
||||
<Icon hoverable name="Add" size="S" />
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<SpreadsheetCell
|
||||
sticky={fieldIdx === 0}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:click={() => addRow(field)}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
width={field.width}
|
||||
left={field.left}
|
||||
<Icon
|
||||
size="S"
|
||||
name={getIconForField(field)}
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</SpreadsheetBody>
|
||||
<span>
|
||||
{field.name}
|
||||
</span>
|
||||
</SpreadsheetCell>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Placeholder overlay for new column position -->
|
||||
<ReorderPlaceholder />
|
||||
</div>
|
||||
<!-- All real rows -->
|
||||
{#each renderedRows as row, rowIdx (row._id)}
|
||||
<SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} />
|
||||
{/each}
|
||||
|
||||
<!-- New row placeholder -->
|
||||
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
|
||||
<SpreadsheetCell
|
||||
label
|
||||
on:click={addRow}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
width="40"
|
||||
left="0"
|
||||
>
|
||||
<Icon hoverable name="Add" size="S" />
|
||||
</SpreadsheetCell>
|
||||
{#each $columns as field, fieldIdx}
|
||||
<SpreadsheetCell
|
||||
sticky={fieldIdx === 0}
|
||||
rowHovered={$hoveredRowId === "new"}
|
||||
reorderSource={$reorder.columnIdx === fieldIdx}
|
||||
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
|
||||
on:click={() => addRow(field)}
|
||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
||||
width={field.width}
|
||||
left={field.left}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</SpreadsheetBody>
|
||||
<ResizeOverlay />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
.sheet {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* Variables */
|
||||
--cell-background: var(--spectrum-global-color-gray-50);
|
||||
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
||||
--cell-padding: 8px;
|
||||
--cell-background-hover: var(--spectrum-global-color-gray-75);
|
||||
--cell-padding: 10px;
|
||||
--cell-spacing: 4px;
|
||||
--cell-font-size: 14px;
|
||||
--controls-height: 50px;
|
||||
}
|
||||
.wrapper.resize *:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.wrapper::-webkit-scrollbar-track {
|
||||
background: var(--cell-background);
|
||||
.sheet,
|
||||
.sheet :global(*) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
@ -314,4 +311,7 @@
|
|||
position: absolute;
|
||||
transform: translateY(var(--top));
|
||||
}
|
||||
.row :global(> :last-child) {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { domDebounce } from "../../../utils/domDebounce"
|
||||
import { Utils } from "../../utils"
|
||||
|
||||
const {
|
||||
columns,
|
||||
|
@ -55,7 +55,7 @@
|
|||
// }
|
||||
// }
|
||||
|
||||
const handleScroll = domDebounce(
|
||||
const handleScroll = Utils.domDebounce(
|
||||
({ left, top }) => {
|
||||
// Only update local state when big changes occur
|
||||
if (Math.abs(top - scrollTop) > 100) {
|
||||
|
@ -116,7 +116,7 @@
|
|||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="spreadsheet"
|
||||
class="sheet-body"
|
||||
class:horizontally-scrolled={horizontallyScrolled}
|
||||
on:click|self={() => ($selectedCellId = null)}
|
||||
id={`sheet-${rand}-body`}
|
||||
|
@ -131,12 +131,16 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.spreadsheet {
|
||||
.sheet-body {
|
||||
display: block;
|
||||
height: 800px;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
overflow: auto;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
}
|
||||
.sheet-body::-webkit-scrollbar-track {
|
||||
background: var(--cell-background);
|
||||
}
|
||||
.content {
|
||||
min-width: 100%;
|
|
@ -10,10 +10,11 @@
|
|||
export let reorderTarget = false
|
||||
export let left
|
||||
export let width
|
||||
export let column
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="cell"
|
||||
class="cell col-{column}"
|
||||
class:header
|
||||
class:label
|
||||
class:spacer
|
||||
|
@ -37,7 +38,7 @@
|
|||
.cell {
|
||||
height: var(--cell-height);
|
||||
border-style: solid;
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-200);
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
border-left-width: 1px;
|
||||
|
@ -67,16 +68,26 @@
|
|||
.cell:hover {
|
||||
cursor: default;
|
||||
}
|
||||
.cell.row-selected {
|
||||
background-color: rgb(224, 242, 255);
|
||||
.cell.row-selected:after {
|
||||
pointer-events: none;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0.2;
|
||||
background-color: var(--spectrum-global-color-blue-600);
|
||||
}
|
||||
|
||||
/* Header cells */
|
||||
.cell.header {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
background: var(--background);
|
||||
padding: 0 var(--cell-padding);
|
||||
z-index: 3;
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
border-color: var(--spectrum-global-color-gray-200);
|
||||
font-weight: 600;
|
||||
gap: calc(2 * var(--cell-spacing));
|
||||
}
|
||||
.cell.header :global(span) {
|
||||
flex: 1 1 auto;
|
||||
|
@ -90,7 +101,7 @@
|
|||
.cell.sticky {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
border-left-color: transparent;
|
||||
border-left-width: 0;
|
||||
transform: none;
|
||||
left: 40px;
|
||||
}
|
||||
|
@ -109,7 +120,16 @@
|
|||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.cell.reorder-target {
|
||||
border-left-color: var(--spectrum-global-color-blue-400);
|
||||
z-index: 100;
|
||||
}
|
||||
.cell.reorder-target:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
background: var(--spectrum-global-color-blue-400);
|
||||
width: 2px;
|
||||
z-index: 100;
|
||||
height: calc(var(--cell-height) + 1px);
|
||||
}
|
||||
|
||||
/* Label cells */
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||
|
||||
const {
|
||||
selectedRows,
|
||||
rows,
|
||||
selectedCellId,
|
||||
hoveredRowId,
|
||||
tableId,
|
||||
spreadsheetAPI,
|
||||
API,
|
||||
} = getContext("spreadsheet")
|
||||
|
||||
let modal
|
||||
|
||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||
$: rowCount = $rows.length
|
||||
$: rowsToDelete = Object.entries($selectedRows)
|
||||
.map(entry => {
|
||||
if (entry[1] === true) {
|
||||
return $rows.find(x => x._id === entry[0])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(x => x != null)
|
||||
|
||||
// Deletion callback when confirmed
|
||||
const performDeletion = async () => {
|
||||
await API.deleteRows({
|
||||
tableId,
|
||||
rows: rowsToDelete,
|
||||
})
|
||||
await spreadsheetAPI.refreshData()
|
||||
|
||||
// Refresh state
|
||||
$selectedCellId = null
|
||||
$hoveredRowId = null
|
||||
$selectedRows = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<ActionButton icon="Filter" quiet size="M">Filter</ActionButton>
|
||||
<ActionButton icon="Group" quiet size="M">Group</ActionButton>
|
||||
<ActionButton icon="SortOrderDown" quiet size="M">Sort</ActionButton>
|
||||
<ActionButton icon="VisibilityOff" quiet size="M">Hide fields</ActionButton>
|
||||
</div>
|
||||
<div class="title" />
|
||||
<div class="delete">
|
||||
{#if selectedRowCount}
|
||||
<ActionButton icon="Delete" size="S" on:click={modal.show}>
|
||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||
</ActionButton>
|
||||
{:else}
|
||||
{rowCount} row{rowCount === 1 ? "" : "s"}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
title="Add screens"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
onConfirm={performDeletion}
|
||||
size="M"
|
||||
>
|
||||
Are you sure you want to delete {selectedRowCount}
|
||||
row{selectedRowCount === 1 ? "" : "s"}?
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
height: var(--controls-height);
|
||||
padding: 0 12px;
|
||||
background: var(--background);
|
||||
gap: 8px;
|
||||
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: calc(1 * var(--cell-spacing));
|
||||
margin-left: -8px;
|
||||
}
|
||||
.delete {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
font-size: 14px;
|
||||
}
|
||||
.delete :global(.spectrum-ActionButton) {
|
||||
color: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
.delete :global(.spectrum-Icon) {
|
||||
fill: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import SpreadsheetCell from "./SpreadsheetCell.svelte"
|
||||
import SpreadsheetCell from "./SheetCell.svelte"
|
||||
import OptionsCell from "./cells/OptionsCell.svelte"
|
||||
import DateCell from "./cells/DateCell.svelte"
|
||||
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
|
@ -165,10 +165,10 @@
|
|||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||
max-height: 191px;
|
||||
max-height: calc(6 * var(--cell-height) - 1px);
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -180,12 +180,12 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--cell-spacing);
|
||||
background-color: var(--cell-background);
|
||||
background-color: var(--cell-background-hover);
|
||||
}
|
||||
.option:first-child {
|
||||
flex: 0 0 calc(var(--cell-height) - 1px);
|
||||
}
|
||||
.option:hover {
|
||||
background-color: var(--cell-background-hover);
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
|
||||
export const createReorderStores = context => {
|
||||
const { columns, bounds, rows, scroll } = context
|
||||
const { columns, bounds, rows, scroll, rand } = context
|
||||
const reorderInitialState = {
|
||||
columnIdx: null,
|
||||
swapColumnIdx: null,
|
||||
|
@ -61,6 +61,7 @@ export const createReorderStores = context => {
|
|||
|
||||
// Trigger a move event immediately so ensure a candidate column is chosen
|
||||
onReorderMouseMove(e)
|
||||
document.getElementById(`sheet-${rand}`).classList.add("is-reordering")
|
||||
}
|
||||
|
||||
// Callback when moving the mouse when reordering columns
|
||||
|
@ -135,6 +136,7 @@ export const createReorderStores = context => {
|
|||
// Remove event handlers
|
||||
document.removeEventListener("mousemove", onReorderMouseMove)
|
||||
document.removeEventListener("mouseup", stopReordering)
|
||||
document.getElementById(`sheet-${rand}`).classList.remove("is-reordering")
|
||||
}
|
||||
|
||||
return {
|
|
@ -86,3 +86,24 @@ export const throttle = (callback, minDelay = 1000) => {
|
|||
}
|
||||
return invoke
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to debounce DOM activities using requestAnimationFrame
|
||||
* @param callback the function to run
|
||||
* @param extractParams
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const domDebounce = (callback, extractParams = x => x) => {
|
||||
let active = false
|
||||
let lastParams
|
||||
return (...params) => {
|
||||
lastParams = extractParams(...params)
|
||||
if (!active) {
|
||||
active = true
|
||||
requestAnimationFrame(() => {
|
||||
callback(lastParams)
|
||||
active = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue