@budibase/frontend-core 2.5.9 → 2.5.10-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -4
- package/src/api/other.js +0 -9
- package/src/components/grid/cells/AttachmentCell.svelte +154 -0
- package/src/components/grid/cells/BooleanCell.svelte +44 -0
- package/src/components/grid/cells/DataCell.svelte +91 -0
- package/src/components/grid/cells/DateCell.svelte +77 -0
- package/src/components/grid/cells/FormulaCell.svelte +5 -0
- package/src/components/grid/cells/GridCell.svelte +165 -0
- package/src/components/grid/cells/GutterCell.svelte +127 -0
- package/src/components/grid/cells/HeaderCell.svelte +248 -0
- package/src/components/grid/cells/JSONCell.svelte +36 -0
- package/src/components/grid/cells/LongFormCell.svelte +122 -0
- package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
- package/src/components/grid/cells/NumberCell.svelte +14 -0
- package/src/components/grid/cells/OptionsCell.svelte +249 -0
- package/src/components/grid/cells/RelationshipCell.svelte +518 -0
- package/src/components/grid/cells/TextCell.svelte +111 -0
- package/src/components/grid/controls/AddColumnButton.svelte +16 -0
- package/src/components/grid/controls/AddRowButton.svelte +18 -0
- package/src/components/grid/controls/BetaButton.svelte +46 -0
- package/src/components/grid/controls/ColumnWidthButton.svelte +91 -0
- package/src/components/grid/controls/DeleteButton.svelte +66 -0
- package/src/components/grid/controls/HideColumnsButton.svelte +95 -0
- package/src/components/grid/controls/RowHeightButton.svelte +70 -0
- package/src/components/grid/controls/SortButton.svelte +129 -0
- package/src/components/grid/index.js +1 -0
- package/src/components/grid/layout/Avatar.svelte +24 -0
- package/src/components/grid/layout/Grid.svelte +254 -0
- package/src/components/grid/layout/GridBody.svelte +73 -0
- package/src/components/grid/layout/GridRow.svelte +59 -0
- package/src/components/grid/layout/GridScrollWrapper.svelte +81 -0
- package/src/components/grid/layout/HeaderRow.svelte +62 -0
- package/src/components/grid/layout/KeyboardShortcut.svelte +53 -0
- package/src/components/grid/layout/NewRow.svelte +334 -0
- package/src/components/grid/layout/RowCount.svelte +9 -0
- package/src/components/grid/layout/StickyColumn.svelte +176 -0
- package/src/components/grid/layout/UserAvatars.svelte +20 -0
- package/src/components/grid/lib/constants.js +14 -0
- package/src/components/grid/lib/events.js +29 -0
- package/src/components/grid/lib/renderers.js +29 -0
- package/src/components/grid/lib/utils.js +26 -0
- package/src/components/grid/lib/websocket.js +63 -0
- package/src/components/grid/overlays/KeyboardManager.svelte +232 -0
- package/src/components/grid/overlays/MenuOverlay.svelte +96 -0
- package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
- package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
- package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
- package/src/components/grid/stores/bounds.js +16 -0
- package/src/components/grid/stores/clipboard.js +34 -0
- package/src/components/grid/stores/columns.js +206 -0
- package/src/components/grid/stores/index.js +48 -0
- package/src/components/grid/stores/menu.js +49 -0
- package/src/components/grid/stores/pagination.js +24 -0
- package/src/components/grid/stores/reorder.js +155 -0
- package/src/components/grid/stores/resize.js +124 -0
- package/src/components/grid/stores/rows.js +467 -0
- package/src/components/grid/stores/scroll.js +201 -0
- package/src/components/grid/stores/ui.js +159 -0
- package/src/components/grid/stores/users.js +115 -0
- package/src/components/grid/stores/validation.js +45 -0
- package/src/components/grid/stores/viewport.js +145 -0
- package/src/components/index.js +1 -0
- package/src/constants.js +1 -0
- package/src/fetch/DataFetch.js +27 -21
- package/src/fetch/QueryFetch.js +1 -1
- package/src/themes/midnight.css +3 -0
- package/src/themes/nord.css +3 -0
- package/src/utils/utils.js +20 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onDestroy, onMount, tick } from "svelte"
|
|
3
|
+
import { Icon, Button } from "@budibase/bbui"
|
|
4
|
+
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
5
|
+
import DataCell from "../cells/DataCell.svelte"
|
|
6
|
+
import { fade } from "svelte/transition"
|
|
7
|
+
import { GutterWidth } from "../lib/constants"
|
|
8
|
+
import { NewRowID } from "../lib/constants"
|
|
9
|
+
import GutterCell from "../cells/GutterCell.svelte"
|
|
10
|
+
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
hoveredRowId,
|
|
14
|
+
focusedCellId,
|
|
15
|
+
stickyColumn,
|
|
16
|
+
scroll,
|
|
17
|
+
dispatch,
|
|
18
|
+
rows,
|
|
19
|
+
focusedCellAPI,
|
|
20
|
+
tableId,
|
|
21
|
+
subscribe,
|
|
22
|
+
renderedRows,
|
|
23
|
+
renderedColumns,
|
|
24
|
+
rowHeight,
|
|
25
|
+
hasNextPage,
|
|
26
|
+
maxScrollTop,
|
|
27
|
+
rowVerticalInversionIndex,
|
|
28
|
+
columnHorizontalInversionIndex,
|
|
29
|
+
} = getContext("grid")
|
|
30
|
+
|
|
31
|
+
let visible = false
|
|
32
|
+
let isAdding = false
|
|
33
|
+
let newRow = {}
|
|
34
|
+
let offset = 0
|
|
35
|
+
|
|
36
|
+
$: firstColumn = $stickyColumn || $renderedColumns[0]
|
|
37
|
+
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
|
38
|
+
$: $tableId, (visible = false)
|
|
39
|
+
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
|
40
|
+
|
|
41
|
+
const shouldInvertY = (offset, inversionIndex, rows) => {
|
|
42
|
+
if (offset === 0) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
return rows.length >= inversionIndex
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const addRow = async () => {
|
|
49
|
+
// Blur the active cell and tick to let final value updates propagate
|
|
50
|
+
isAdding = true
|
|
51
|
+
$focusedCellId = null
|
|
52
|
+
await tick()
|
|
53
|
+
|
|
54
|
+
// Create row
|
|
55
|
+
const newRowIndex = offset ? undefined : 0
|
|
56
|
+
const savedRow = await rows.actions.addRow(newRow, newRowIndex)
|
|
57
|
+
if (savedRow) {
|
|
58
|
+
// Reset state
|
|
59
|
+
clear()
|
|
60
|
+
|
|
61
|
+
// Select the first cell if possible
|
|
62
|
+
if (firstColumn) {
|
|
63
|
+
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isAdding = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const clear = () => {
|
|
70
|
+
isAdding = false
|
|
71
|
+
visible = false
|
|
72
|
+
$focusedCellId = null
|
|
73
|
+
$hoveredRowId = null
|
|
74
|
+
document.removeEventListener("keydown", handleKeyPress)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const startAdding = async () => {
|
|
78
|
+
if (visible) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If we have a next page of data then we aren't truly at the bottom, so we
|
|
83
|
+
// render the add row component at the top
|
|
84
|
+
if ($hasNextPage) {
|
|
85
|
+
offset = 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If we don't have a next page then we're at the bottom and can scroll to
|
|
89
|
+
// the max available offset
|
|
90
|
+
else {
|
|
91
|
+
scroll.update(state => ({
|
|
92
|
+
...state,
|
|
93
|
+
top: $maxScrollTop,
|
|
94
|
+
}))
|
|
95
|
+
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
|
|
96
|
+
if ($renderedRows.length !== 0) {
|
|
97
|
+
offset -= 1
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Update state and select initial cell
|
|
102
|
+
newRow = {}
|
|
103
|
+
visible = true
|
|
104
|
+
$hoveredRowId = NewRowID
|
|
105
|
+
if (firstColumn) {
|
|
106
|
+
$focusedCellId = `${NewRowID}-${firstColumn.name}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Attach key listener
|
|
110
|
+
document.addEventListener("keydown", handleKeyPress)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const updateValue = (rowId, columnName, val) => {
|
|
114
|
+
newRow[columnName] = val
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const addViaModal = () => {
|
|
118
|
+
clear()
|
|
119
|
+
dispatch("add-row")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleKeyPress = e => {
|
|
123
|
+
if (!visible) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (e.key === "Escape") {
|
|
127
|
+
// Only close the new row component if we aren't actively inside a cell
|
|
128
|
+
if (!$focusedCellAPI?.isActive()) {
|
|
129
|
+
e.preventDefault()
|
|
130
|
+
clear()
|
|
131
|
+
}
|
|
132
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
133
|
+
e.preventDefault()
|
|
134
|
+
addRow()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onMount(() => subscribe("add-row-inline", startAdding))
|
|
139
|
+
onDestroy(() => {
|
|
140
|
+
document.removeEventListener("keydown", handleKeyPress)
|
|
141
|
+
})
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<!-- Only show new row functionality if we have any columns -->
|
|
145
|
+
{#if visible}
|
|
146
|
+
<div
|
|
147
|
+
class="container"
|
|
148
|
+
class:floating={offset > 0}
|
|
149
|
+
style="--offset:{offset}px; --sticky-width:{width}px;"
|
|
150
|
+
>
|
|
151
|
+
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
|
|
152
|
+
<div class="underlay" transition:fade={{ duration: 130 }} />
|
|
153
|
+
<div class="sticky-column" transition:fade={{ duration: 130 }}>
|
|
154
|
+
<GutterCell on:expand={addViaModal} rowHovered>
|
|
155
|
+
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
|
156
|
+
{#if isAdding}
|
|
157
|
+
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
|
158
|
+
{/if}
|
|
159
|
+
</GutterCell>
|
|
160
|
+
{#if $stickyColumn}
|
|
161
|
+
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
|
|
162
|
+
<DataCell
|
|
163
|
+
{cellId}
|
|
164
|
+
rowFocused
|
|
165
|
+
column={$stickyColumn}
|
|
166
|
+
row={newRow}
|
|
167
|
+
focused={$focusedCellId === cellId}
|
|
168
|
+
width={$stickyColumn.width}
|
|
169
|
+
{updateValue}
|
|
170
|
+
rowIdx={0}
|
|
171
|
+
{invertY}
|
|
172
|
+
>
|
|
173
|
+
{#if $stickyColumn?.schema?.autocolumn}
|
|
174
|
+
<div class="readonly-overlay">Can't edit auto column</div>
|
|
175
|
+
{/if}
|
|
176
|
+
{#if isAdding}
|
|
177
|
+
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
|
178
|
+
{/if}
|
|
179
|
+
</DataCell>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
<div class="normal-columns" transition:fade={{ duration: 130 }}>
|
|
183
|
+
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
|
184
|
+
<div class="row">
|
|
185
|
+
{#each $renderedColumns as column, columnIdx}
|
|
186
|
+
{@const cellId = `new-${column.name}`}
|
|
187
|
+
{#key cellId}
|
|
188
|
+
<DataCell
|
|
189
|
+
{cellId}
|
|
190
|
+
{column}
|
|
191
|
+
{updateValue}
|
|
192
|
+
rowFocused
|
|
193
|
+
row={newRow}
|
|
194
|
+
focused={$focusedCellId === cellId}
|
|
195
|
+
width={column.width}
|
|
196
|
+
rowIdx={0}
|
|
197
|
+
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
198
|
+
{invertY}
|
|
199
|
+
>
|
|
200
|
+
{#if column?.schema?.autocolumn}
|
|
201
|
+
<div class="readonly-overlay">Can't edit auto column</div>
|
|
202
|
+
{/if}
|
|
203
|
+
{#if isAdding}
|
|
204
|
+
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
|
205
|
+
{/if}
|
|
206
|
+
</DataCell>
|
|
207
|
+
{/key}
|
|
208
|
+
{/each}
|
|
209
|
+
</div>
|
|
210
|
+
</GridScrollWrapper>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="buttons" transition:fade={{ duration: 130 }}>
|
|
213
|
+
<Button size="M" cta on:click={addRow} disabled={isAdding}>
|
|
214
|
+
<div class="button-with-keys">
|
|
215
|
+
Save
|
|
216
|
+
<KeyboardShortcut overlay keybind="Ctrl+Enter" />
|
|
217
|
+
</div>
|
|
218
|
+
</Button>
|
|
219
|
+
<Button size="M" secondary newStyles on:click={clear}>
|
|
220
|
+
<div class="button-with-keys">
|
|
221
|
+
Cancel
|
|
222
|
+
<KeyboardShortcut overlay keybind="Esc" />
|
|
223
|
+
</div>
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
<style>
|
|
230
|
+
.container {
|
|
231
|
+
position: absolute;
|
|
232
|
+
top: var(--default-row-height);
|
|
233
|
+
left: 0;
|
|
234
|
+
width: 100%;
|
|
235
|
+
height: 100%;
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: row;
|
|
238
|
+
align-items: stretch;
|
|
239
|
+
}
|
|
240
|
+
.container :global(.cell) {
|
|
241
|
+
--cell-background: var(--spectrum-global-color-gray-75) !important;
|
|
242
|
+
}
|
|
243
|
+
.container.floating :global(.cell) {
|
|
244
|
+
height: calc(var(--row-height) + 1px);
|
|
245
|
+
border-top: var(--cell-border);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Underlay sits behind everything */
|
|
249
|
+
.underlay {
|
|
250
|
+
position: absolute;
|
|
251
|
+
content: "";
|
|
252
|
+
left: 0;
|
|
253
|
+
top: 0;
|
|
254
|
+
height: 100%;
|
|
255
|
+
width: 100%;
|
|
256
|
+
background: var(--cell-background);
|
|
257
|
+
opacity: 0.8;
|
|
258
|
+
}
|
|
259
|
+
.underlay.sticky {
|
|
260
|
+
z-index: 2;
|
|
261
|
+
width: var(--sticky-width);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Floating buttons which sit on top of the underlay but below the sticky column */
|
|
265
|
+
.buttons {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: row;
|
|
268
|
+
gap: 8px;
|
|
269
|
+
pointer-events: all;
|
|
270
|
+
z-index: 3;
|
|
271
|
+
position: absolute;
|
|
272
|
+
top: calc(var(--row-height) + var(--offset) + 24px);
|
|
273
|
+
left: var(--gutter-width);
|
|
274
|
+
}
|
|
275
|
+
.button-with-keys {
|
|
276
|
+
display: flex;
|
|
277
|
+
gap: 6px;
|
|
278
|
+
align-items: center;
|
|
279
|
+
}
|
|
280
|
+
.button-with-keys :global(> div) {
|
|
281
|
+
padding-top: 2px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* Sticky column styles */
|
|
285
|
+
.sticky-column {
|
|
286
|
+
display: flex;
|
|
287
|
+
z-index: 4;
|
|
288
|
+
position: relative;
|
|
289
|
+
align-self: flex-start;
|
|
290
|
+
flex: 0 0 var(--sticky-width);
|
|
291
|
+
}
|
|
292
|
+
.sticky-column :global(.cell:not(:last-child)) {
|
|
293
|
+
border-right: none;
|
|
294
|
+
}
|
|
295
|
+
.sticky-column,
|
|
296
|
+
.normal-columns {
|
|
297
|
+
margin-top: var(--offset);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Normal column styles */
|
|
301
|
+
.row {
|
|
302
|
+
width: 0;
|
|
303
|
+
display: flex;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Readonly cell overlay */
|
|
307
|
+
.readonly-overlay {
|
|
308
|
+
position: absolute;
|
|
309
|
+
top: 0;
|
|
310
|
+
left: 0;
|
|
311
|
+
height: var(--row-height);
|
|
312
|
+
width: 100%;
|
|
313
|
+
padding: var(--cell-padding);
|
|
314
|
+
font-style: italic;
|
|
315
|
+
color: var(--spectrum-global-color-gray-600);
|
|
316
|
+
z-index: 1;
|
|
317
|
+
user-select: none;
|
|
318
|
+
overflow: hidden;
|
|
319
|
+
white-space: nowrap;
|
|
320
|
+
text-overflow: ellipsis;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Overlay while row is being added */
|
|
324
|
+
.loading-overlay {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 0;
|
|
327
|
+
left: 0;
|
|
328
|
+
height: var(--row-height);
|
|
329
|
+
width: 100%;
|
|
330
|
+
z-index: 1;
|
|
331
|
+
background: var(--spectrum-global-color-gray-400);
|
|
332
|
+
opacity: 0.25;
|
|
333
|
+
}
|
|
334
|
+
</style>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { Icon } from "@budibase/bbui"
|
|
4
|
+
import GridCell from "../cells/GridCell.svelte"
|
|
5
|
+
import DataCell from "../cells/DataCell.svelte"
|
|
6
|
+
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
7
|
+
import HeaderCell from "../cells/HeaderCell.svelte"
|
|
8
|
+
import { GutterWidth, BlankRowID } from "../lib/constants"
|
|
9
|
+
import GutterCell from "../cells/GutterCell.svelte"
|
|
10
|
+
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
rows,
|
|
14
|
+
selectedRows,
|
|
15
|
+
stickyColumn,
|
|
16
|
+
renderedColumns,
|
|
17
|
+
renderedRows,
|
|
18
|
+
focusedCellId,
|
|
19
|
+
hoveredRowId,
|
|
20
|
+
config,
|
|
21
|
+
selectedCellMap,
|
|
22
|
+
focusedRow,
|
|
23
|
+
scrollLeft,
|
|
24
|
+
dispatch,
|
|
25
|
+
contentLines,
|
|
26
|
+
} = getContext("grid")
|
|
27
|
+
|
|
28
|
+
$: rowCount = $rows.length
|
|
29
|
+
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
|
30
|
+
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
|
31
|
+
|
|
32
|
+
const selectAll = () => {
|
|
33
|
+
const allSelected = selectedRowCount === rowCount
|
|
34
|
+
if (allSelected) {
|
|
35
|
+
$selectedRows = {}
|
|
36
|
+
} else {
|
|
37
|
+
let allRows = {}
|
|
38
|
+
$rows.forEach(row => {
|
|
39
|
+
allRows[row._id] = true
|
|
40
|
+
})
|
|
41
|
+
$selectedRows = allRows
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
class="sticky-column"
|
|
48
|
+
style="flex: 0 0 {width}px"
|
|
49
|
+
class:scrolled={$scrollLeft > 0}
|
|
50
|
+
>
|
|
51
|
+
<div class="header row">
|
|
52
|
+
<GutterCell
|
|
53
|
+
disableExpand
|
|
54
|
+
disableNumber
|
|
55
|
+
on:select={selectAll}
|
|
56
|
+
defaultHeight
|
|
57
|
+
rowSelected={selectedRowCount && selectedRowCount === rowCount}
|
|
58
|
+
disabled={!$renderedRows.length}
|
|
59
|
+
/>
|
|
60
|
+
{#if $stickyColumn}
|
|
61
|
+
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
|
62
|
+
{/if}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
|
66
|
+
<GridScrollWrapper scrollVertically wheelInteractive>
|
|
67
|
+
{#each $renderedRows as row, idx}
|
|
68
|
+
{@const rowSelected = !!$selectedRows[row._id]}
|
|
69
|
+
{@const rowHovered = $hoveredRowId === row._id}
|
|
70
|
+
{@const rowFocused = $focusedRow?._id === row._id}
|
|
71
|
+
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
|
72
|
+
<div
|
|
73
|
+
class="row"
|
|
74
|
+
on:mouseenter={() => ($hoveredRowId = row._id)}
|
|
75
|
+
on:mouseleave={() => ($hoveredRowId = null)}
|
|
76
|
+
>
|
|
77
|
+
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
|
78
|
+
{#if $stickyColumn}
|
|
79
|
+
<DataCell
|
|
80
|
+
{row}
|
|
81
|
+
{cellId}
|
|
82
|
+
{rowFocused}
|
|
83
|
+
selected={rowSelected}
|
|
84
|
+
highlighted={rowHovered || rowFocused}
|
|
85
|
+
rowIdx={idx}
|
|
86
|
+
focused={$focusedCellId === cellId}
|
|
87
|
+
selectedUser={$selectedCellMap[cellId]}
|
|
88
|
+
width={$stickyColumn.width}
|
|
89
|
+
column={$stickyColumn}
|
|
90
|
+
contentLines={$contentLines}
|
|
91
|
+
/>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
{/each}
|
|
95
|
+
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
|
|
96
|
+
<div
|
|
97
|
+
class="row new"
|
|
98
|
+
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
|
99
|
+
on:mouseleave={() => ($hoveredRowId = null)}
|
|
100
|
+
on:click={() => dispatch("add-row-inline")}
|
|
101
|
+
>
|
|
102
|
+
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
|
|
103
|
+
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
|
104
|
+
</GutterCell>
|
|
105
|
+
{#if $stickyColumn}
|
|
106
|
+
<GridCell
|
|
107
|
+
width={$stickyColumn.width}
|
|
108
|
+
highlighted={$hoveredRowId === BlankRowID}
|
|
109
|
+
>
|
|
110
|
+
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
|
111
|
+
</GridCell>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
</GridScrollWrapper>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
.sticky-column {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
position: relative;
|
|
124
|
+
z-index: 2;
|
|
125
|
+
background: var(--cell-background);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Add right border */
|
|
129
|
+
.sticky-column:before {
|
|
130
|
+
position: absolute;
|
|
131
|
+
content: "";
|
|
132
|
+
width: 0;
|
|
133
|
+
height: 100%;
|
|
134
|
+
left: calc(100% - 1px);
|
|
135
|
+
top: 0;
|
|
136
|
+
border-left: var(--cell-border);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Add shadow when scrolled */
|
|
140
|
+
.sticky-column.scrolled:after {
|
|
141
|
+
position: absolute;
|
|
142
|
+
content: "";
|
|
143
|
+
width: 8px;
|
|
144
|
+
height: 100%;
|
|
145
|
+
left: 100%;
|
|
146
|
+
top: 0;
|
|
147
|
+
opacity: 1;
|
|
148
|
+
background: linear-gradient(to right, var(--drop-shadow), transparent);
|
|
149
|
+
z-index: -1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Don't show borders between cells in the sticky column */
|
|
153
|
+
.sticky-column :global(.cell:not(:last-child)) {
|
|
154
|
+
border-right: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.header {
|
|
158
|
+
z-index: 1;
|
|
159
|
+
}
|
|
160
|
+
.header :global(.cell) {
|
|
161
|
+
background: var(--spectrum-global-color-gray-100);
|
|
162
|
+
}
|
|
163
|
+
.row {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: row;
|
|
166
|
+
justify-content: flex-start;
|
|
167
|
+
align-items: stretch;
|
|
168
|
+
}
|
|
169
|
+
.content {
|
|
170
|
+
position: relative;
|
|
171
|
+
flex: 1 1 auto;
|
|
172
|
+
}
|
|
173
|
+
.row.new :global(*:hover) {
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import Avatar from "./Avatar.svelte"
|
|
4
|
+
|
|
5
|
+
const { users } = getContext("grid")
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div class="users">
|
|
9
|
+
{#each $users as user}
|
|
10
|
+
<Avatar {user} />
|
|
11
|
+
{/each}
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.users {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: row;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const Padding = 128
|
|
2
|
+
export const MaxCellRenderHeight = 252
|
|
3
|
+
export const MaxCellRenderWidthOverflow = 200
|
|
4
|
+
export const ScrollBarSize = 8
|
|
5
|
+
export const GutterWidth = 72
|
|
6
|
+
export const DefaultColumnWidth = 200
|
|
7
|
+
export const MinColumnWidth = 80
|
|
8
|
+
export const SmallRowHeight = 36
|
|
9
|
+
export const MediumRowHeight = 64
|
|
10
|
+
export const LargeRowHeight = 92
|
|
11
|
+
export const DefaultRowHeight = SmallRowHeight
|
|
12
|
+
export const NewRowID = "new"
|
|
13
|
+
export const BlankRowID = "blank"
|
|
14
|
+
export const RowPageSize = 100
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createEventDispatcher } from "svelte"
|
|
2
|
+
|
|
3
|
+
export const createEventManagers = () => {
|
|
4
|
+
const svelteDispatch = createEventDispatcher()
|
|
5
|
+
let subscribers = {}
|
|
6
|
+
|
|
7
|
+
// Dispatches an event, notifying subscribers and also emitting a normal
|
|
8
|
+
// svelte event
|
|
9
|
+
const dispatch = (event, payload) => {
|
|
10
|
+
svelteDispatch(event, payload)
|
|
11
|
+
const subs = subscribers[event] || []
|
|
12
|
+
for (let i = 0; i < subs.length; i++) {
|
|
13
|
+
subs[i](payload)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Subscribes to events
|
|
18
|
+
const subscribe = (event, callback) => {
|
|
19
|
+
const subs = subscribers[event] || []
|
|
20
|
+
subscribers[event] = [...subs, callback]
|
|
21
|
+
|
|
22
|
+
// Return unsubscribe function
|
|
23
|
+
return () => {
|
|
24
|
+
subscribers[event] = subscribers[event].filter(cb => cb !== callback)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { dispatch, subscribe }
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import OptionsCell from "../cells/OptionsCell.svelte"
|
|
2
|
+
import DateCell from "../cells/DateCell.svelte"
|
|
3
|
+
import MultiSelectCell from "../cells/MultiSelectCell.svelte"
|
|
4
|
+
import NumberCell from "../cells/NumberCell.svelte"
|
|
5
|
+
import RelationshipCell from "../cells/RelationshipCell.svelte"
|
|
6
|
+
import TextCell from "../cells/TextCell.svelte"
|
|
7
|
+
import LongFormCell from "../cells/LongFormCell.svelte"
|
|
8
|
+
import BooleanCell from "../cells/BooleanCell.svelte"
|
|
9
|
+
import FormulaCell from "../cells/FormulaCell.svelte"
|
|
10
|
+
import JSONCell from "../cells/JSONCell.svelte"
|
|
11
|
+
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
|
12
|
+
|
|
13
|
+
const TypeComponentMap = {
|
|
14
|
+
text: TextCell,
|
|
15
|
+
options: OptionsCell,
|
|
16
|
+
datetime: DateCell,
|
|
17
|
+
barcodeqr: TextCell,
|
|
18
|
+
longform: LongFormCell,
|
|
19
|
+
array: MultiSelectCell,
|
|
20
|
+
number: NumberCell,
|
|
21
|
+
boolean: BooleanCell,
|
|
22
|
+
attachment: AttachmentCell,
|
|
23
|
+
link: RelationshipCell,
|
|
24
|
+
formula: FormulaCell,
|
|
25
|
+
json: JSONCell,
|
|
26
|
+
}
|
|
27
|
+
export const getCellRenderer = column => {
|
|
28
|
+
return TypeComponentMap[column?.schema?.type] || TextCell
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const getColor = (idx, opacity = 0.3) => {
|
|
2
|
+
if (idx == null || idx === -1) {
|
|
3
|
+
return null
|
|
4
|
+
}
|
|
5
|
+
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const TypeIconMap = {
|
|
9
|
+
text: "Text",
|
|
10
|
+
options: "Dropdown",
|
|
11
|
+
datetime: "Date",
|
|
12
|
+
barcodeqr: "Camera",
|
|
13
|
+
longform: "TextAlignLeft",
|
|
14
|
+
array: "Dropdown",
|
|
15
|
+
number: "123",
|
|
16
|
+
boolean: "Boolean",
|
|
17
|
+
attachment: "AppleFiles",
|
|
18
|
+
link: "DataCorrelated",
|
|
19
|
+
formula: "Calculator",
|
|
20
|
+
json: "Brackets",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getColumnIcon = column => {
|
|
24
|
+
const type = column.schema.type
|
|
25
|
+
return TypeIconMap[type] || "Text"
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { get } from "svelte/store"
|
|
2
|
+
import { io } from "socket.io-client"
|
|
3
|
+
|
|
4
|
+
export const createWebsocket = context => {
|
|
5
|
+
const { rows, tableId, users, userId, focusedCellId } = context
|
|
6
|
+
|
|
7
|
+
// Determine connection info
|
|
8
|
+
const tls = location.protocol === "https:"
|
|
9
|
+
const proto = tls ? "wss:" : "ws:"
|
|
10
|
+
const host = location.hostname
|
|
11
|
+
const port = location.port || (tls ? 443 : 80)
|
|
12
|
+
const socket = io(`${proto}//${host}:${port}`, {
|
|
13
|
+
path: "/socket/grid",
|
|
14
|
+
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
15
|
+
reconnectionAttempts: 3,
|
|
16
|
+
// Delay reconnection attempt by 5 seconds
|
|
17
|
+
reconnectionDelay: 5000,
|
|
18
|
+
reconnectionDelayMax: 5000,
|
|
19
|
+
// Timeout after 4 seconds so we never stack requests
|
|
20
|
+
timeout: 4000,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const connectToTable = tableId => {
|
|
24
|
+
if (!socket.connected) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
// Identify which table we are editing
|
|
28
|
+
socket.emit("select-table", tableId, response => {
|
|
29
|
+
// handle initial connection info
|
|
30
|
+
users.set(response.users)
|
|
31
|
+
userId.set(response.id)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Event handlers
|
|
36
|
+
socket.on("connect", () => {
|
|
37
|
+
connectToTable(get(tableId))
|
|
38
|
+
})
|
|
39
|
+
socket.on("row-update", data => {
|
|
40
|
+
if (data.id) {
|
|
41
|
+
rows.actions.refreshRow(data.id)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
socket.on("user-update", user => {
|
|
45
|
+
users.actions.updateUser(user)
|
|
46
|
+
})
|
|
47
|
+
socket.on("user-disconnect", user => {
|
|
48
|
+
users.actions.removeUser(user)
|
|
49
|
+
})
|
|
50
|
+
socket.on("connect_error", err => {
|
|
51
|
+
console.log("Failed to connect to grid websocket:", err.message)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Change websocket connection when table changes
|
|
55
|
+
tableId.subscribe(connectToTable)
|
|
56
|
+
|
|
57
|
+
// Notify selected cell changes
|
|
58
|
+
focusedCellId.subscribe($focusedCellId => {
|
|
59
|
+
socket.emit("select-cell", $focusedCellId)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return () => socket?.disconnect()
|
|
63
|
+
}
|