diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 6a5cec15d2..2ccd029e25 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -8,6 +8,7 @@ import MultiSelectCell from "./MultiSelectCell.svelte" import NumberCell from "./NumberCell.svelte" import RelationshipCell from "./RelationshipCell.svelte" + import { getColor } from "./utils.js" export let table export let filter @@ -20,8 +21,9 @@ const limit = 100 const defaultWidth = 160 const minWidth = 100 + const rand = Math.random() - let widths + let fieldConfigs = [] let hoveredRow let selectedCell let selectedRows = {} @@ -29,6 +31,22 @@ let changeCache = {} let newRows = [] + // State for resizing columns + let resizeInitialX + let resizeInitialWidth + let resizeFieldIndex + + // State for reordering columns + let isReordering = false + let reorderFieldIndex + let reorderBreakpoints + let reorderPlaceholderX + let reorderPlaceholderInitialX + let reorderPlaceholderWidth + let reorderInitialX + let reorderPlaceholderHeight + let reorderCandidateFieldIdx + $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) $: fetch.update({ @@ -37,12 +55,8 @@ query, limit, }) - $: schema = $fetch.schema - $: primaryDisplay = $fetch.definition?.primaryDisplay - $: fields = getFields(schema, primaryDisplay) - $: fieldCount = fields.length - $: fieldCount, initWidths() - $: gridStyles = getGridStyles(widths) + $: updateFieldConfig($fetch) + $: gridStyles = getGridStyles(fieldConfigs) $: rowCount = $fetch.rows?.length || 0 $: selectedRowCount = Object.values(selectedRows).filter(x => !!x).length $: rows = getSortedRows($fetch.rows, newRows) @@ -61,19 +75,25 @@ }) } - const getFields = (schema, primaryDisplay) => { - let fields = Object.keys(schema || {}) - if (primaryDisplay) { - fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] + const updateFieldConfig = ({ schema, definition }) => { + // Generate first time config if required + if (!fieldConfigs.length && schema) { + let fields = Object.keys(schema || {}) + const primaryDisplay = definition?.primaryDisplay + if (primaryDisplay) { + fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] + } + fieldConfigs = fields.map(field => ({ + name: field, + width: defaultWidth, + schema: schema[field], + primaryDisplay: field === primaryDisplay, + })) } - return fields } - const initWidths = () => { - widths = fields.map(() => defaultWidth) - } - - const getGridStyles = widths => { + const getGridStyles = fieldConfig => { + const widths = fieldConfig?.map(x => x.width) if (!widths?.length) { return "--grid: 1fr;" } @@ -88,7 +108,7 @@ } const getCellForField = field => { - const type = schema?.[field]?.type + const type = field.schema.type if (type === "options") { return OptionsCell } else if (type === "datetime") { @@ -104,7 +124,7 @@ } const getIconForField = field => { - const type = schema?.[field]?.type + const type = field.schema.type if (type === "options") { return "ChevronDown" } else if (type === "datetime") { @@ -133,10 +153,10 @@ if (!row) { return } - if (row[field] === value) { + if (row[field.name] === value) { return } - changeCache[rowId] = { [field]: value } + changeCache[rowId] = { [field.name]: value } await API.saveRow({ ...row, ...changeCache[rowId], @@ -164,11 +184,6 @@ rows: rowsToDelete, }) await fetch.refresh() - // notificationStore.actions.success( - // `${selectedRowCount} row${ - // selectedRowCount === 1 ? "" : "s" - // } deleted successfully` - // ) // Refresh state selectedCell = null @@ -188,7 +203,7 @@ const addRow = async field => { const res = await API.saveRow({ tableId: table.tableId }) - selectedCell = `${res._id}-${field}` + selectedCell = `${res._id}-${field.name}` newRows.push(res._id) await fetch.refresh() } @@ -203,31 +218,101 @@ return sortedRows } - let resizeInitialX - let resizeInitialWidth - let resizeFieldIndex + const startReordering = (fieldIdx, e) => { + isReordering = true + reorderFieldIndex = fieldIdx + + let breakpoints = [] + fieldConfigs.forEach((config, idx) => { + const header = document.getElementById(`sheet-${rand}-header-${idx}`) + const bounds = header.getBoundingClientRect() + breakpoints.push(bounds.x) + if (idx === fieldConfigs.length - 1) { + breakpoints.push(bounds.x + bounds.width) + } + }) + reorderBreakpoints = breakpoints + const self = document.getElementById(`sheet-${rand}-header-${fieldIdx}`) + const selfBounds = self.getBoundingClientRect() + const body = document.getElementById(`sheet-${rand}-body`) + const bodyBounds = body.getBoundingClientRect() + reorderPlaceholderInitialX = selfBounds.x - bodyBounds.x + reorderPlaceholderX = reorderPlaceholderInitialX + reorderPlaceholderWidth = selfBounds.width + reorderInitialX = e.clientX + reorderPlaceholderHeight = (rows.length + 2) * 32 + onReorderMove(e) + document.addEventListener("mousemove", onReorderMove) + document.addEventListener("mouseup", stopReordering) + } + + const onReorderMove = e => { + if (!isReordering) { + return + } + reorderPlaceholderX = + e.clientX - reorderInitialX + reorderPlaceholderInitialX + reorderPlaceholderX = Math.max(0, reorderPlaceholderX) + let candidateFieldIdx + let minDistance = Number.MAX_SAFE_INTEGER + reorderBreakpoints.forEach((point, idx) => { + const distance = Math.abs(point - e.clientX) + if (distance < minDistance) { + minDistance = distance + candidateFieldIdx = idx + } + }) + reorderCandidateFieldIdx = candidateFieldIdx + } + + const stopReordering = () => { + const newConfigs = fieldConfigs.slice() + const removed = newConfigs.splice(reorderFieldIndex, 1) + if (--reorderCandidateFieldIdx < reorderFieldIndex) { + reorderCandidateFieldIdx++ + } + newConfigs.splice(reorderCandidateFieldIdx, 0, removed[0]) + fieldConfigs = newConfigs + + isReordering = false + reorderFieldIndex = null + reorderBreakpoints = null + reorderPlaceholderX = null + reorderPlaceholderInitialX = null + reorderPlaceholderWidth = null + reorderInitialX = null + reorderPlaceholderHeight = null + reorderCandidateFieldIdx = null + + document.removeEventListener("mousemove", onReorderMove) + document.removeEventListener("mouseup", stopReordering) + } const startResizing = (fieldIdx, e) => { + e.stopPropagation() resizeInitialX = e.clientX - resizeInitialWidth = widths[fieldIdx] + resizeInitialWidth = fieldConfigs[fieldIdx].width resizeFieldIndex = fieldIdx - document.addEventListener("mousemove", onResize) + document.addEventListener("mousemove", onResizeMove) document.addEventListener("mouseup", stopResizing) } - const onResize = e => { + const onResizeMove = e => { const dx = e.clientX - resizeInitialX - widths[resizeFieldIndex] = Math.max(minWidth, resizeInitialWidth + dx) + fieldConfigs[resizeFieldIndex].width = Math.max( + minWidth, + resizeInitialWidth + dx + ) } const stopResizing = () => { - document.removeEventListener("mousemove", onResize) + document.removeEventListener("mousemove", onResizeMove) document.removeEventListener("mouseup", stopResizing) }