@svgrid/enterprise 3.1.7 → 3.1.9
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/README.md +1 -0
- package/dist/SvAlertRuleEditor.svelte +16 -5
- package/dist/SvGridAlerts.svelte +8 -0
- package/dist/SvGridAlerts.svelte.d.ts +4 -0
- package/dist/SvGridBoard.svelte +69 -10
- package/dist/SvGridBulkEditDrawer.svelte +13 -5
- package/dist/SvGridGallery.svelte +286 -0
- package/dist/SvGridGallery.svelte.d.ts +30 -0
- package/dist/SvGridScheduler.svelte +33 -11
- package/dist/SvGridSelectionBar.svelte +47 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
- package/dist/advanced-filter-enable.d.ts +6 -0
- package/dist/advanced-filter-enable.js +8 -0
- package/dist/alerts/alert-engine.d.ts +7 -0
- package/dist/alerts/alert-engine.js +5 -2
- package/dist/alerts/condition-editor.d.ts +26 -0
- package/dist/alerts/condition-editor.js +17 -0
- package/dist/base/index.d.ts +20 -0
- package/dist/base/index.js +20 -0
- package/dist/bulk-edit.d.ts +11 -0
- package/dist/bulk-edit.js +18 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +11375 -10805
- package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
- package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
- package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
- package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
- package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
- package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
- package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
- package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
- package/dist/designer/assets/{index-DtBIaQno.js → index-BPU2KNc5.js} +396 -395
- package/dist/designer/assets/index-BmvG99kR.css +1 -0
- package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
- package/dist/designer/assets/src-C_U1NKOY.js +58 -0
- package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
- package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
- package/dist/designer/index.html +11 -7
- package/dist/expressions/compile.js +10 -0
- package/dist/expressions/evaluate.js +18 -0
- package/dist/expressions/expression-types.d.ts +10 -0
- package/dist/expressions/kinds.d.ts +28 -0
- package/dist/expressions/kinds.js +19 -0
- package/dist/expressions/parse.js +9 -1
- package/dist/fields/AttachmentCell.svelte +242 -0
- package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
- package/dist/fields/AttachmentDialog.svelte +357 -0
- package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
- package/dist/fields/AttachmentEditor.svelte +41 -0
- package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
- package/dist/fields/AttachmentPreview.svelte +72 -0
- package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
- package/dist/fields/BarcodeCell.svelte +20 -0
- package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
- package/dist/fields/BaseAutomations.svelte +558 -0
- package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
- package/dist/fields/BaseChartView.svelte +47 -0
- package/dist/fields/BaseChartView.svelte.d.ts +8 -0
- package/dist/fields/BaseTableView.svelte +185 -0
- package/dist/fields/BaseTableView.svelte.d.ts +37 -0
- package/dist/fields/BaseTabs.svelte +308 -0
- package/dist/fields/BaseTabs.svelte.d.ts +21 -0
- package/dist/fields/ButtonCell.svelte +47 -0
- package/dist/fields/ButtonCell.svelte.d.ts +20 -0
- package/dist/fields/CheckboxCell.svelte +46 -0
- package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
- package/dist/fields/DeleteFieldConfirm.svelte +80 -0
- package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
- package/dist/fields/DeleteRecordConfirm.svelte +48 -0
- package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
- package/dist/fields/FieldCell.svelte +80 -0
- package/dist/fields/FieldCell.svelte.d.ts +9 -0
- package/dist/fields/FieldEditor.svelte +321 -0
- package/dist/fields/FieldEditor.svelte.d.ts +10 -0
- package/dist/fields/FieldInput.svelte +188 -0
- package/dist/fields/FieldInput.svelte.d.ts +29 -0
- package/dist/fields/FieldsHeaderEnd.svelte +146 -0
- package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
- package/dist/fields/FootRowAdder.svelte +8 -0
- package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
- package/dist/fields/GroupHeader.svelte +127 -0
- package/dist/fields/GroupHeader.svelte.d.ts +24 -0
- package/dist/fields/LinkCell.svelte +46 -0
- package/dist/fields/LinkCell.svelte.d.ts +9 -0
- package/dist/fields/LinkPickerEditor.svelte +172 -0
- package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
- package/dist/fields/LinkPreview.svelte +60 -0
- package/dist/fields/LinkPreview.svelte.d.ts +7 -0
- package/dist/fields/LinkedRecordView.svelte +94 -0
- package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
- package/dist/fields/LinkedRecordsCell.svelte +59 -0
- package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
- package/dist/fields/LongTextEditor.svelte +157 -0
- package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
- package/dist/fields/NumberEditor.svelte +309 -0
- package/dist/fields/NumberEditor.svelte.d.ts +12 -0
- package/dist/fields/OptionsEditor.svelte +117 -0
- package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
- package/dist/fields/PercentCell.svelte +21 -0
- package/dist/fields/PercentCell.svelte.d.ts +8 -0
- package/dist/fields/PropertyInput.svelte +204 -0
- package/dist/fields/PropertyInput.svelte.d.ts +11 -0
- package/dist/fields/RatingCell.svelte +75 -0
- package/dist/fields/RatingCell.svelte.d.ts +16 -0
- package/dist/fields/RatingEditor.svelte +82 -0
- package/dist/fields/RatingEditor.svelte.d.ts +9 -0
- package/dist/fields/RecordView.svelte +310 -0
- package/dist/fields/RecordView.svelte.d.ts +11 -0
- package/dist/fields/RichTextCell.svelte +30 -0
- package/dist/fields/RichTextCell.svelte.d.ts +5 -0
- package/dist/fields/RowAdder.svelte +93 -0
- package/dist/fields/RowAdder.svelte.d.ts +7 -0
- package/dist/fields/RowHeader.svelte +99 -0
- package/dist/fields/RowHeader.svelte.d.ts +11 -0
- package/dist/fields/SelectCell.svelte +35 -0
- package/dist/fields/SelectCell.svelte.d.ts +15 -0
- package/dist/fields/SelectEditor.svelte +350 -0
- package/dist/fields/SelectEditor.svelte.d.ts +17 -0
- package/dist/fields/SummaryCell.svelte +160 -0
- package/dist/fields/SummaryCell.svelte.d.ts +11 -0
- package/dist/fields/SvBase.svelte +545 -0
- package/dist/fields/SvBase.svelte.d.ts +30 -0
- package/dist/fields/SvBaseForm.svelte +295 -0
- package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
- package/dist/fields/SvBaseViewBar.svelte +644 -0
- package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
- package/dist/fields/UserCell.svelte +39 -0
- package/dist/fields/UserCell.svelte.d.ts +9 -0
- package/dist/fields/UserFace.svelte +30 -0
- package/dist/fields/UserFace.svelte.d.ts +12 -0
- package/dist/fields/activity.svelte.d.ts +50 -0
- package/dist/fields/activity.svelte.js +110 -0
- package/dist/fields/attachments.d.ts +95 -0
- package/dist/fields/attachments.js +145 -0
- package/dist/fields/automation-outbound.d.ts +46 -0
- package/dist/fields/automation-outbound.js +61 -0
- package/dist/fields/automation-server.d.ts +53 -0
- package/dist/fields/automation-server.js +333 -0
- package/dist/fields/automations.svelte.d.ts +208 -0
- package/dist/fields/automations.svelte.js +437 -0
- package/dist/fields/bar-request.svelte.d.ts +16 -0
- package/dist/fields/bar-request.svelte.js +15 -0
- package/dist/fields/base-alerts.d.ts +24 -0
- package/dist/fields/base-alerts.js +64 -0
- package/dist/fields/base-connect.d.ts +82 -0
- package/dist/fields/base-connect.js +271 -0
- package/dist/fields/base-schema.d.ts +104 -0
- package/dist/fields/base-schema.js +266 -0
- package/dist/fields/base-sql.d.ts +62 -0
- package/dist/fields/base-sql.js +251 -0
- package/dist/fields/base-sync.d.ts +142 -0
- package/dist/fields/base-sync.js +499 -0
- package/dist/fields/base-tables.d.ts +89 -0
- package/dist/fields/base-tables.js +349 -0
- package/dist/fields/builtin-types.d.ts +13 -0
- package/dist/fields/builtin-types.js +773 -0
- package/dist/fields/button.d.ts +2 -0
- package/dist/fields/button.js +17 -0
- package/dist/fields/chart-view.d.ts +35 -0
- package/dist/fields/chart-view.js +154 -0
- package/dist/fields/column-order.d.ts +6 -0
- package/dist/fields/column-order.js +12 -0
- package/dist/fields/convert.d.ts +16 -0
- package/dist/fields/convert.js +176 -0
- package/dist/fields/copy-text.d.ts +13 -0
- package/dist/fields/copy-text.js +45 -0
- package/dist/fields/editor-request.svelte.d.ts +71 -0
- package/dist/fields/editor-request.svelte.js +52 -0
- package/dist/fields/field-ops.d.ts +72 -0
- package/dist/fields/field-ops.js +334 -0
- package/dist/fields/filter-model.d.ts +84 -0
- package/dist/fields/filter-model.js +446 -0
- package/dist/fields/format.d.ts +109 -0
- package/dist/fields/format.js +288 -0
- package/dist/fields/formula.d.ts +50 -0
- package/dist/fields/formula.js +530 -0
- package/dist/fields/grid-access.d.ts +60 -0
- package/dist/fields/grid-access.js +120 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +312 -0
- package/dist/fields/linked-fields.d.ts +8 -0
- package/dist/fields/linked-fields.js +40 -0
- package/dist/fields/links.d.ts +10 -0
- package/dist/fields/links.js +46 -0
- package/dist/fields/name-list.d.ts +16 -0
- package/dist/fields/name-list.js +49 -0
- package/dist/fields/palette.d.ts +47 -0
- package/dist/fields/palette.js +69 -0
- package/dist/fields/record-delete.svelte.d.ts +12 -0
- package/dist/fields/record-delete.svelte.js +5 -0
- package/dist/fields/record-menu.d.ts +9 -0
- package/dist/fields/record-menu.js +78 -0
- package/dist/fields/registry.svelte.d.ts +176 -0
- package/dist/fields/registry.svelte.js +51 -0
- package/dist/fields/rich-text.d.ts +23 -0
- package/dist/fields/rich-text.js +48 -0
- package/dist/fields/row-color-choice.svelte.d.ts +31 -0
- package/dist/fields/row-color-choice.svelte.js +25 -0
- package/dist/fields/row-colors.d.ts +36 -0
- package/dist/fields/row-colors.js +90 -0
- package/dist/fields/row-height.d.ts +28 -0
- package/dist/fields/row-height.js +50 -0
- package/dist/fields/sources.svelte.d.ts +80 -0
- package/dist/fields/sources.svelte.js +131 -0
- package/dist/fields/summaries.d.ts +18 -0
- package/dist/fields/summaries.js +138 -0
- package/dist/fields/view-model.d.ts +129 -0
- package/dist/fields/view-model.js +492 -0
- package/dist/fields/viewbar/ChartPanel.svelte +61 -0
- package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ColorPanel.svelte +112 -0
- package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ConditionValue.svelte +153 -0
- package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
- package/dist/fields/viewbar/FieldIcon.svelte +18 -0
- package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
- package/dist/fields/viewbar/FilterPanel.svelte +192 -0
- package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
- package/dist/fields/viewbar/GanttPanel.svelte +60 -0
- package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
- package/dist/fields/viewbar/SortPanel.svelte +85 -0
- package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
- package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
- package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
- package/dist/fields/viewbar/panel-focus.d.ts +3 -0
- package/dist/fields/viewbar/panel-focus.js +22 -0
- package/dist/fields/viewbar/sort-state.d.ts +20 -0
- package/dist/fields/viewbar/sort-state.js +24 -0
- package/dist/gallery.d.ts +7 -0
- package/dist/gallery.js +36 -0
- package/dist/gantt/SvGridGantt.svelte +88 -16
- package/dist/gantt/gantt-config.d.ts +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/install.js +3 -0
- package/dist/license-core.d.ts +3 -3
- package/dist/license.js +1 -0
- package/dist/node/studio.js +141 -20
- package/dist/scheduler-config.d.ts +11 -0
- package/dist/sources/introspect-supabase.js +4 -1
- package/dist/sources/rest.js +6 -2
- package/dist/studio/emit-project.js +25 -6
- package/dist/studio/sveltekit3.js +13 -7
- package/dist/studio/ui-components.generated.js +94 -2
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
- package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
- package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
- package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
- package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
- package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
- package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
- package/dist/wc/sv-sheet-element.js +7561 -7618
- package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
- package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
- package/package.json +14 -3
- package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
- package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
- package/dist/designer/assets/src-DqHCqori.js +0 -58
- package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
- package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
- package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Attachment cells in read mode: images as square thumbnails, other files as
|
|
4
|
+
* chips (type badge + name), as many as the column width holds, then "+N".
|
|
5
|
+
* Thumbnails are a fixed size with lazy loading and async decoding, so a
|
|
6
|
+
* scroll never waits on an image or re-lays out the row.
|
|
7
|
+
*
|
|
8
|
+
* Files dropped on the cell are added to it (uploaded with the column's
|
|
9
|
+
* uploader, written through the grid's edit path). Each file uploads on
|
|
10
|
+
* its own: the ones that land are written, a refused or failed one is
|
|
11
|
+
* reported in a toast, and uploads still running when the cell goes away
|
|
12
|
+
* are aborted.
|
|
13
|
+
*/
|
|
14
|
+
import { toast } from '@svgrid/grid'
|
|
15
|
+
import type { FieldCellProps } from './registry.svelte'
|
|
16
|
+
import { isImage, limitAttachments, rejectReason, safeUrl, toAttachments, uploadAttachment, type Attachment, type AttachmentFieldProps } from './attachments'
|
|
17
|
+
import { writeCell } from './grid-access'
|
|
18
|
+
|
|
19
|
+
let { ctrl, row, column, value, props }: FieldCellProps<AttachmentFieldProps> = $props()
|
|
20
|
+
|
|
21
|
+
const THUMB = 26
|
|
22
|
+
const CHIP = 96
|
|
23
|
+
const GAP = 4
|
|
24
|
+
const PLUS = 30
|
|
25
|
+
|
|
26
|
+
const items = $derived(toAttachments(value))
|
|
27
|
+
const width = $derived(
|
|
28
|
+
Number((ctrl as { getColumnWidth?: (id: string) => number }).getColumnWidth?.(column.id)) ||
|
|
29
|
+
Number(column.columnDef.width) ||
|
|
30
|
+
200,
|
|
31
|
+
)
|
|
32
|
+
// How many fit, leaving room for the "+N" badge when some do not.
|
|
33
|
+
const shown = $derived.by(() => {
|
|
34
|
+
const room = width - 12
|
|
35
|
+
let used = 0
|
|
36
|
+
let n = 0
|
|
37
|
+
for (const a of items) {
|
|
38
|
+
const w = (isImage(a) ? THUMB : CHIP) + GAP
|
|
39
|
+
const rest = items.length - n - 1
|
|
40
|
+
if (used + w + (rest > 0 ? PLUS : 0) > room) break
|
|
41
|
+
used += w
|
|
42
|
+
n += 1
|
|
43
|
+
}
|
|
44
|
+
return Math.max(items.length ? 1 : 0, n)
|
|
45
|
+
})
|
|
46
|
+
const label = $derived(
|
|
47
|
+
items.length === 0
|
|
48
|
+
? 'No attachments'
|
|
49
|
+
: `${items.length} attachment${items.length === 1 ? '' : 's'}: ${items.map((a) => a.name).join(', ')}`,
|
|
50
|
+
)
|
|
51
|
+
const ext = (a: Attachment) => (a.name.split('.').pop() ?? '').slice(0, 4).toUpperCase() || 'FILE'
|
|
52
|
+
|
|
53
|
+
// ---- Files dropped on the cell ----
|
|
54
|
+
const canDrop = $derived(!!(ctrl as { editingEnabled?: boolean } | null)?.editingEnabled && column.columnDef.editable !== false && !!row?.id)
|
|
55
|
+
let over = $state(false)
|
|
56
|
+
const hasFiles = (e: DragEvent) => !!e.dataTransfer && [...e.dataTransfer.types].includes('Files')
|
|
57
|
+
function dragOver(e: DragEvent) {
|
|
58
|
+
if (!canDrop || !hasFiles(e)) return
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
|
|
61
|
+
over = true
|
|
62
|
+
}
|
|
63
|
+
// Each running upload's progress (0..1), averaged into the bar under the cell.
|
|
64
|
+
let uploads = $state<Record<string, number>>({})
|
|
65
|
+
const progress = $derived.by(() => {
|
|
66
|
+
const parts = Object.values(uploads)
|
|
67
|
+
return parts.length ? parts.reduce((s, p) => s + p, 0) / parts.length : null
|
|
68
|
+
})
|
|
69
|
+
const running = new Set<AbortController>()
|
|
70
|
+
$effect(() => () => {
|
|
71
|
+
for (const ctl of running) ctl.abort()
|
|
72
|
+
})
|
|
73
|
+
let seq = 0
|
|
74
|
+
|
|
75
|
+
async function drop(e: DragEvent) {
|
|
76
|
+
over = false
|
|
77
|
+
if (!canDrop || !hasFiles(e)) return
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
e.stopPropagation()
|
|
80
|
+
// The row as it was dropped on; the write goes to it even if the cell is reused.
|
|
81
|
+
const rowId = row.id
|
|
82
|
+
const original = row.original as Record<string, unknown> | undefined
|
|
83
|
+
const field = column.columnDef.field as string | undefined
|
|
84
|
+
const current = () => (original && field ? toAttachments(original[field]) : items)
|
|
85
|
+
const refused: string[] = []
|
|
86
|
+
const queue: File[] = []
|
|
87
|
+
for (const file of [...(e.dataTransfer?.files ?? [])]) {
|
|
88
|
+
const why = rejectReason(file, props, items.length + queue.length)
|
|
89
|
+
if (why) refused.push(`${file.name}: ${why}`)
|
|
90
|
+
else queue.push(file)
|
|
91
|
+
}
|
|
92
|
+
if (refused.length) toast.warning(refused.join('\n'), { title: 'Not added' })
|
|
93
|
+
if (!queue.length) return
|
|
94
|
+
const ctl = new AbortController()
|
|
95
|
+
running.add(ctl)
|
|
96
|
+
const keys = queue.map(() => `u${++seq}`)
|
|
97
|
+
uploads = { ...uploads, ...Object.fromEntries(keys.map((k) => [k, 0])) }
|
|
98
|
+
const results = await Promise.allSettled(
|
|
99
|
+
queue.map((file, i) =>
|
|
100
|
+
uploadAttachment(file, props, {
|
|
101
|
+
row: original,
|
|
102
|
+
columnId: column.id,
|
|
103
|
+
signal: ctl.signal,
|
|
104
|
+
onProgress: (f) => {
|
|
105
|
+
if (keys[i]! in uploads) uploads = { ...uploads, [keys[i]!]: Math.max(0, Math.min(1, f)) }
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
running.delete(ctl)
|
|
111
|
+
if (ctl.signal.aborted) return
|
|
112
|
+
uploads = Object.fromEntries(Object.entries(uploads).filter(([k]) => !keys.includes(k)))
|
|
113
|
+
const failed: string[] = []
|
|
114
|
+
const added: Attachment[] = []
|
|
115
|
+
results.forEach((r, i) => {
|
|
116
|
+
if (r.status === 'fulfilled') added.push(r.value)
|
|
117
|
+
else failed.push(`${queue[i]!.name}: ${r.reason instanceof Error && r.reason.message ? r.reason.message : 'Upload failed'}`)
|
|
118
|
+
})
|
|
119
|
+
// Another drop may have filled the cell meanwhile; maxFiles still holds.
|
|
120
|
+
const base = current()
|
|
121
|
+
const { kept, rejected } = limitAttachments(added, props, base.length)
|
|
122
|
+
if (kept.length) writeCell(ctrl, rowId, column.id, [...base, ...kept])
|
|
123
|
+
if (rejected.length) toast.warning(rejected.join('\n'), { title: 'Not added' })
|
|
124
|
+
if (failed.length) toast.error(failed.join('\n'), { title: failed.length === 1 ? 'Upload failed' : `${failed.length} uploads failed` })
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<span
|
|
129
|
+
class="sv-att"
|
|
130
|
+
class:is-drop={over}
|
|
131
|
+
role="img"
|
|
132
|
+
aria-label={label}
|
|
133
|
+
title={items.map((a) => a.name).join('\n')}
|
|
134
|
+
ondragover={dragOver}
|
|
135
|
+
ondragleave={() => (over = false)}
|
|
136
|
+
ondrop={drop}
|
|
137
|
+
>
|
|
138
|
+
{#each items.slice(0, shown) as a (a.id)}
|
|
139
|
+
{#if isImage(a) && safeUrl(a.thumbnailUrl ?? a.url, true)}
|
|
140
|
+
<img
|
|
141
|
+
class="sv-att-thumb"
|
|
142
|
+
src={safeUrl(a.thumbnailUrl ?? a.url, true)}
|
|
143
|
+
alt=""
|
|
144
|
+
width={THUMB}
|
|
145
|
+
height={THUMB}
|
|
146
|
+
loading="lazy"
|
|
147
|
+
decoding="async"
|
|
148
|
+
draggable="false"
|
|
149
|
+
/>
|
|
150
|
+
{:else}
|
|
151
|
+
<span class="sv-att-chip">
|
|
152
|
+
<span class="sv-att-ext">{ext(a)}</span>
|
|
153
|
+
<span class="sv-att-name">{a.name}</span>
|
|
154
|
+
</span>
|
|
155
|
+
{/if}
|
|
156
|
+
{/each}
|
|
157
|
+
{#if items.length > shown}
|
|
158
|
+
<span class="sv-att-more">+{items.length - shown}</span>
|
|
159
|
+
{/if}
|
|
160
|
+
{#if progress !== null}
|
|
161
|
+
<span
|
|
162
|
+
class="sv-att-progress"
|
|
163
|
+
role="progressbar"
|
|
164
|
+
aria-label="Uploading"
|
|
165
|
+
aria-valuemin="0"
|
|
166
|
+
aria-valuemax="100"
|
|
167
|
+
aria-valuenow={Math.round(progress * 100)}
|
|
168
|
+
style:--sv-att-done={`${Math.round(progress * 100)}%`}
|
|
169
|
+
></span>
|
|
170
|
+
{/if}
|
|
171
|
+
</span>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
.sv-att {
|
|
175
|
+
position: relative;
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 4px;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
width: 100%;
|
|
181
|
+
min-height: 100%;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
}
|
|
184
|
+
/* Files over the cell: where they will land. */
|
|
185
|
+
.sv-att.is-drop {
|
|
186
|
+
outline: 2px dashed var(--sg-accent, #2563eb);
|
|
187
|
+
outline-offset: -2px;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
}
|
|
190
|
+
.sv-att-thumb {
|
|
191
|
+
flex: none;
|
|
192
|
+
width: 26px;
|
|
193
|
+
height: 26px;
|
|
194
|
+
object-fit: cover;
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
border: 1px solid var(--sg-border, #e2e8f0);
|
|
197
|
+
background: var(--sg-header-bg, #f8fafc);
|
|
198
|
+
}
|
|
199
|
+
.sv-att-chip {
|
|
200
|
+
flex: none;
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 4px;
|
|
204
|
+
width: 96px;
|
|
205
|
+
height: 24px;
|
|
206
|
+
padding: 0 6px 0 3px;
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
border: 1px solid var(--sg-border, #e2e8f0);
|
|
209
|
+
background: var(--sg-header-bg, #f8fafc);
|
|
210
|
+
font-size: 11px;
|
|
211
|
+
}
|
|
212
|
+
.sv-att-ext {
|
|
213
|
+
flex: none;
|
|
214
|
+
padding: 1px 3px;
|
|
215
|
+
border-radius: 3px;
|
|
216
|
+
background: var(--sg-bg, #fff);
|
|
217
|
+
color: var(--sg-muted, #64748b);
|
|
218
|
+
font-size: 9px;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
letter-spacing: 0.02em;
|
|
221
|
+
}
|
|
222
|
+
.sv-att-name {
|
|
223
|
+
min-width: 0;
|
|
224
|
+
overflow: hidden;
|
|
225
|
+
text-overflow: ellipsis;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
}
|
|
228
|
+
.sv-att-more {
|
|
229
|
+
flex: none;
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
color: var(--sg-muted, #64748b);
|
|
232
|
+
}
|
|
233
|
+
/* Uploads from a drop: a thin bar along the bottom of the cell. */
|
|
234
|
+
.sv-att-progress {
|
|
235
|
+
position: absolute;
|
|
236
|
+
left: 0;
|
|
237
|
+
right: 0;
|
|
238
|
+
bottom: 0;
|
|
239
|
+
height: 2px;
|
|
240
|
+
background: linear-gradient(to right, var(--sg-accent, #2563eb) var(--sv-att-done), transparent var(--sv-att-done));
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FieldCellProps } from './registry.svelte.js';
|
|
2
|
+
import { type AttachmentFieldProps } from './attachments.js';
|
|
3
|
+
declare const AttachmentCell: import("svelte").Component<FieldCellProps<AttachmentFieldProps>, {}, "">;
|
|
4
|
+
type AttachmentCell = ReturnType<typeof AttachmentCell>;
|
|
5
|
+
export default AttachmentCell;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The attachment field's editor: every file in the cell as a tile, with
|
|
4
|
+
* add (file picker, drop, paste, URL), remove, reorder (drag, or Alt+Left /
|
|
5
|
+
* Alt+Right on a tile) and preview. Uploads run through the column's
|
|
6
|
+
* uploader with progress; a failed one stays as a tile with Retry. Done
|
|
7
|
+
* commits the list, Cancel (or Escape) leaves the cell as it was.
|
|
8
|
+
*/
|
|
9
|
+
import { SvModal } from '@svgrid/grid'
|
|
10
|
+
import AttachmentPreview from './AttachmentPreview.svelte'
|
|
11
|
+
import {
|
|
12
|
+
attachmentsFromText,
|
|
13
|
+
formatBytes,
|
|
14
|
+
isImage,
|
|
15
|
+
limitAttachments,
|
|
16
|
+
rejectReason,
|
|
17
|
+
safeUrl,
|
|
18
|
+
toAttachments,
|
|
19
|
+
uploadAttachment,
|
|
20
|
+
type Attachment,
|
|
21
|
+
type AttachmentFieldProps,
|
|
22
|
+
} from './attachments'
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
value,
|
|
26
|
+
title = 'Attachments',
|
|
27
|
+
props,
|
|
28
|
+
row,
|
|
29
|
+
columnId,
|
|
30
|
+
onDone,
|
|
31
|
+
onCancel,
|
|
32
|
+
}: {
|
|
33
|
+
value: unknown
|
|
34
|
+
title?: string
|
|
35
|
+
props: AttachmentFieldProps
|
|
36
|
+
row: unknown
|
|
37
|
+
columnId: string
|
|
38
|
+
onDone: (next: Attachment[]) => void
|
|
39
|
+
onCancel: () => void
|
|
40
|
+
} = $props()
|
|
41
|
+
|
|
42
|
+
type Pending = { key: string; file: File; progress: number; error: string | null; ctl: AbortController }
|
|
43
|
+
|
|
44
|
+
// svelte-ignore state_referenced_locally
|
|
45
|
+
let items = $state<Attachment[]>(toAttachments(value))
|
|
46
|
+
let pending = $state<Pending[]>([])
|
|
47
|
+
let rejected = $state<string[]>([])
|
|
48
|
+
let urlText = $state('')
|
|
49
|
+
let previewIndex = $state<number | null>(null)
|
|
50
|
+
let dragFrom = $state<number | null>(null)
|
|
51
|
+
let dropping = $state(false)
|
|
52
|
+
let fileInput = $state<HTMLInputElement | null>(null)
|
|
53
|
+
let open = $state(true)
|
|
54
|
+
let finished = false
|
|
55
|
+
|
|
56
|
+
const busy = $derived(pending.some((p) => !p.error))
|
|
57
|
+
|
|
58
|
+
function addFiles(files: Iterable<File>) {
|
|
59
|
+
rejected = []
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const why = rejectReason(file, props, items.length + pending.length)
|
|
62
|
+
if (why) {
|
|
63
|
+
rejected = [...rejected, `${file.name}: ${why}`]
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
start({ key: `${file.name}-${file.size}-${Math.random()}`, file, progress: 0, error: null, ctl: new AbortController() })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function start(p: Pending) {
|
|
71
|
+
pending = [...pending.filter((x) => x.key !== p.key), p]
|
|
72
|
+
const set = (patch: Partial<Pending>) => (pending = pending.map((x) => (x.key === p.key ? { ...x, ...patch } : x)))
|
|
73
|
+
uploadAttachment(p.file, props, {
|
|
74
|
+
row,
|
|
75
|
+
columnId,
|
|
76
|
+
signal: p.ctl.signal,
|
|
77
|
+
onProgress: (f) => set({ progress: Math.max(0, Math.min(1, f)) }),
|
|
78
|
+
}).then(
|
|
79
|
+
(att) => {
|
|
80
|
+
if (p.ctl.signal.aborted) return
|
|
81
|
+
items = [...items, att]
|
|
82
|
+
pending = pending.filter((x) => x.key !== p.key)
|
|
83
|
+
},
|
|
84
|
+
(err) => {
|
|
85
|
+
if (p.ctl.signal.aborted) return
|
|
86
|
+
set({ error: err instanceof Error ? err.message : 'Upload failed' })
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function retry(p: Pending) {
|
|
92
|
+
start({ ...p, progress: 0, error: null, ctl: new AbortController() })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function drop(p: Pending) {
|
|
96
|
+
p.ctl.abort()
|
|
97
|
+
pending = pending.filter((x) => x.key !== p.key)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Kept out of the markup: AGENTS.md keeps `as` casts in <script>.
|
|
101
|
+
function onPick(e: Event) {
|
|
102
|
+
const input = e.currentTarget as HTMLInputElement
|
|
103
|
+
if (input.files) addFiles(input.files)
|
|
104
|
+
input.value = ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addUrls() {
|
|
108
|
+
const found = attachmentsFromText(urlText)
|
|
109
|
+
if (!found.length) {
|
|
110
|
+
rejected = ['No http(s) URL found']
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
// The same checks as a picked file: accept (by the name's extension) and maxFiles.
|
|
114
|
+
const { kept, rejected: refused } = limitAttachments(found, props, items.length + pending.length)
|
|
115
|
+
items = [...items, ...kept]
|
|
116
|
+
urlText = ''
|
|
117
|
+
rejected = refused
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function remove(i: number) {
|
|
121
|
+
items = items.filter((_, j) => j !== i)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function move(from: number, to: number) {
|
|
125
|
+
if (to < 0 || to >= items.length || from === to) return
|
|
126
|
+
const next = items.slice()
|
|
127
|
+
const [it] = next.splice(from, 1)
|
|
128
|
+
next.splice(to, 0, it!)
|
|
129
|
+
items = next
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function onTileKey(e: KeyboardEvent, i: number) {
|
|
133
|
+
if (e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
const to = i + (e.key === 'ArrowRight' ? 1 : -1)
|
|
136
|
+
move(i, to)
|
|
137
|
+
queueMicrotask(() => (document.querySelectorAll<HTMLElement>('.sv-attd-tile')[to])?.focus())
|
|
138
|
+
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
remove(i)
|
|
141
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
142
|
+
e.preventDefault()
|
|
143
|
+
previewIndex = i
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onPaste(e: ClipboardEvent) {
|
|
148
|
+
const files = [...(e.clipboardData?.files ?? [])]
|
|
149
|
+
if (files.length) {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
addFiles(files)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const text = e.clipboardData?.getData('text') ?? ''
|
|
155
|
+
if (!(e.target as HTMLElement).closest('input') && attachmentsFromText(text).length) {
|
|
156
|
+
e.preventDefault()
|
|
157
|
+
urlText = text
|
|
158
|
+
addUrls()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function done() {
|
|
163
|
+
if (busy) return
|
|
164
|
+
finished = true
|
|
165
|
+
open = false
|
|
166
|
+
onDone(items)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function cancel() {
|
|
170
|
+
if (finished) return
|
|
171
|
+
finished = true
|
|
172
|
+
for (const p of pending) p.ctl.abort()
|
|
173
|
+
open = false
|
|
174
|
+
onCancel()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ext = (a: Attachment) => (a.name.split('.').pop() ?? '').slice(0, 4).toUpperCase() || 'FILE'
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<SvModal bind:open {title} width={680} onClose={cancel}>
|
|
181
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
182
|
+
<div
|
|
183
|
+
class="sv-attd"
|
|
184
|
+
class:is-dropping={dropping}
|
|
185
|
+
onpaste={onPaste}
|
|
186
|
+
ondragover={(e) => {
|
|
187
|
+
if (dragFrom !== null || !e.dataTransfer?.types.includes('Files')) return
|
|
188
|
+
e.preventDefault()
|
|
189
|
+
dropping = true
|
|
190
|
+
}}
|
|
191
|
+
ondragleave={(e) => {
|
|
192
|
+
if (e.currentTarget === e.target) dropping = false
|
|
193
|
+
}}
|
|
194
|
+
ondrop={(e) => {
|
|
195
|
+
dropping = false
|
|
196
|
+
if (dragFrom !== null || !e.dataTransfer?.files.length) return
|
|
197
|
+
e.preventDefault()
|
|
198
|
+
addFiles(e.dataTransfer.files)
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{#if items.length === 0 && pending.length === 0}
|
|
202
|
+
<p class="sv-attd-empty">No files yet. Drop files here, paste them, or use Add files.</p>
|
|
203
|
+
{/if}
|
|
204
|
+
<ul class="sv-attd-tiles" aria-label="Files">
|
|
205
|
+
{#each items as a, i (a.id)}
|
|
206
|
+
<li
|
|
207
|
+
class="sv-attd-tile"
|
|
208
|
+
class:is-dragging={dragFrom === i}
|
|
209
|
+
tabindex="0"
|
|
210
|
+
draggable="true"
|
|
211
|
+
aria-label={`${a.name}${a.size ? `, ${formatBytes(a.size)}` : ''}. Enter to preview, Delete to remove, Alt+Arrow to move.`}
|
|
212
|
+
onkeydown={(e) => onTileKey(e, i)}
|
|
213
|
+
ondragstart={(e) => {
|
|
214
|
+
dragFrom = i
|
|
215
|
+
e.dataTransfer?.setData('text/plain', a.name)
|
|
216
|
+
}}
|
|
217
|
+
ondragend={() => (dragFrom = null)}
|
|
218
|
+
ondragover={(e) => {
|
|
219
|
+
if (dragFrom === null) return
|
|
220
|
+
e.preventDefault()
|
|
221
|
+
}}
|
|
222
|
+
ondrop={(e) => {
|
|
223
|
+
if (dragFrom === null) return
|
|
224
|
+
e.preventDefault()
|
|
225
|
+
move(dragFrom, i)
|
|
226
|
+
dragFrom = null
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<button type="button" class="sv-attd-face" onclick={() => (previewIndex = i)} tabindex="-1" aria-hidden="true">
|
|
230
|
+
{#if isImage(a) && safeUrl(a.thumbnailUrl ?? a.url, true)}
|
|
231
|
+
<img src={safeUrl(a.thumbnailUrl ?? a.url, true)} alt="" loading="lazy" decoding="async" draggable="false" />
|
|
232
|
+
{:else}
|
|
233
|
+
<span class="sv-attd-ext">{ext(a)}</span>
|
|
234
|
+
{/if}
|
|
235
|
+
</button>
|
|
236
|
+
<span class="sv-attd-name" title={a.name}>{a.name}</span>
|
|
237
|
+
<span class="sv-attd-size">{formatBytes(a.size)}</span>
|
|
238
|
+
<button type="button" class="sv-attd-remove" aria-label={`Remove ${a.name}`} onclick={() => remove(i)}>
|
|
239
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
240
|
+
</button>
|
|
241
|
+
</li>
|
|
242
|
+
{/each}
|
|
243
|
+
{#each pending as p (p.key)}
|
|
244
|
+
<li class="sv-attd-tile is-pending" aria-label={p.error ? `${p.file.name}: ${p.error}` : `Uploading ${p.file.name}`}>
|
|
245
|
+
<span class="sv-attd-face"><span class="sv-attd-ext">{p.error ? '!' : `${Math.round(p.progress * 100)}%`}</span></span>
|
|
246
|
+
<span class="sv-attd-name" title={p.file.name}>{p.file.name}</span>
|
|
247
|
+
{#if p.error}
|
|
248
|
+
<span class="sv-attd-error">{p.error}</span>
|
|
249
|
+
<span class="sv-attd-actions">
|
|
250
|
+
<button type="button" class="sv-attd-link" onclick={() => retry(p)}>Retry</button>
|
|
251
|
+
<button type="button" class="sv-attd-link" onclick={() => drop(p)}>Remove</button>
|
|
252
|
+
</span>
|
|
253
|
+
{:else}
|
|
254
|
+
<span class="sv-attd-bar"><span style:width={`${Math.round(p.progress * 100)}%`}></span></span>
|
|
255
|
+
<button type="button" class="sv-attd-remove" aria-label={`Cancel ${p.file.name}`} onclick={() => drop(p)}>
|
|
256
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
257
|
+
</button>
|
|
258
|
+
{/if}
|
|
259
|
+
</li>
|
|
260
|
+
{/each}
|
|
261
|
+
</ul>
|
|
262
|
+
{#if rejected.length}
|
|
263
|
+
<ul class="sv-attd-rejected" role="alert">
|
|
264
|
+
{#each rejected as r, i (i)}<li>{r}</li>{/each}
|
|
265
|
+
</ul>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{#snippet footer()}
|
|
270
|
+
<div class="sv-attd-footer">
|
|
271
|
+
<input
|
|
272
|
+
bind:this={fileInput}
|
|
273
|
+
type="file"
|
|
274
|
+
multiple
|
|
275
|
+
accept={props.accept}
|
|
276
|
+
hidden
|
|
277
|
+
onchange={onPick}
|
|
278
|
+
/>
|
|
279
|
+
<button type="button" class="sv-attd-btn" onclick={() => fileInput?.click()}>Add files</button>
|
|
280
|
+
<input
|
|
281
|
+
class="sv-attd-url"
|
|
282
|
+
type="url"
|
|
283
|
+
placeholder="or paste a URL"
|
|
284
|
+
aria-label="Add a file by URL"
|
|
285
|
+
bind:value={urlText}
|
|
286
|
+
onkeydown={(e) => {
|
|
287
|
+
if (e.key === 'Enter') {
|
|
288
|
+
e.preventDefault()
|
|
289
|
+
addUrls()
|
|
290
|
+
}
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
<span class="sv-attd-spacer"></span>
|
|
294
|
+
<button type="button" class="sv-attd-btn" onclick={cancel}>Cancel</button>
|
|
295
|
+
<button type="button" class="sv-attd-btn is-primary" onclick={done} disabled={busy}>
|
|
296
|
+
{busy ? `Uploading ${pending.filter((p) => !p.error).length}...` : 'Done'}
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
{/snippet}
|
|
300
|
+
</SvModal>
|
|
301
|
+
|
|
302
|
+
{#if previewIndex !== null}
|
|
303
|
+
<AttachmentPreview items={items} bind:index={previewIndex} />
|
|
304
|
+
{/if}
|
|
305
|
+
|
|
306
|
+
<style>
|
|
307
|
+
.sv-attd { min-height: 160px; border: 2px dashed transparent; border-radius: 8px; padding: 4px; }
|
|
308
|
+
.sv-attd.is-dropping { border-color: var(--sg-accent, #2563eb); }
|
|
309
|
+
.sv-attd-empty { margin: 40px 0; text-align: center; color: var(--sg-muted, #64748b); font-size: 13px; }
|
|
310
|
+
.sv-attd-tiles {
|
|
311
|
+
list-style: none; margin: 0; padding: 0;
|
|
312
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px;
|
|
313
|
+
}
|
|
314
|
+
.sv-attd-tile {
|
|
315
|
+
position: relative; display: flex; flex-direction: column; gap: 3px;
|
|
316
|
+
padding: 6px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px;
|
|
317
|
+
background: var(--sg-bg, #fff); cursor: grab; min-width: 0;
|
|
318
|
+
}
|
|
319
|
+
.sv-attd-tile:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
|
|
320
|
+
.sv-attd-tile.is-dragging { opacity: 0.4; }
|
|
321
|
+
.sv-attd-tile.is-pending { cursor: default; }
|
|
322
|
+
.sv-attd-face {
|
|
323
|
+
display: flex; align-items: center; justify-content: center;
|
|
324
|
+
height: 96px; border: 0; padding: 0; border-radius: 6px; overflow: hidden;
|
|
325
|
+
background: var(--sg-header-bg, #f8fafc); cursor: zoom-in;
|
|
326
|
+
}
|
|
327
|
+
.sv-attd-face img { width: 100%; height: 100%; object-fit: cover; }
|
|
328
|
+
.sv-attd-ext { font-size: 13px; font-weight: 600; color: var(--sg-muted, #64748b); }
|
|
329
|
+
.sv-attd-name { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
330
|
+
.sv-attd-size, .sv-attd-error { font-size: 11px; color: var(--sg-muted, #64748b); }
|
|
331
|
+
.sv-attd-error { color: var(--sg-danger, #dc2626); }
|
|
332
|
+
.sv-attd-actions { display: flex; gap: 8px; }
|
|
333
|
+
.sv-attd-link { border: 0; padding: 0; background: none; color: inherit; text-decoration: underline; font: inherit; font-size: 12px; cursor: pointer; }
|
|
334
|
+
.sv-attd-bar { height: 4px; border-radius: 2px; background: var(--sg-header-bg, #f8fafc); overflow: hidden; }
|
|
335
|
+
.sv-attd-bar > span { display: block; height: 100%; background: var(--sg-muted, #64748b); transition: width 120ms linear; }
|
|
336
|
+
.sv-attd-remove {
|
|
337
|
+
position: absolute; top: 10px; inset-inline-end: 10px;
|
|
338
|
+
display: inline-flex; padding: 3px; border: 0; border-radius: 999px;
|
|
339
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer; opacity: 0.85;
|
|
340
|
+
}
|
|
341
|
+
.sv-attd-remove:hover { opacity: 1; }
|
|
342
|
+
.sv-attd-rejected { margin: 10px 0 0; padding-inline-start: 18px; font-size: 12px; color: var(--sg-danger, #dc2626); }
|
|
343
|
+
.sv-attd-footer { display: flex; align-items: center; gap: 8px; width: 100%; }
|
|
344
|
+
.sv-attd-spacer { flex: 1; }
|
|
345
|
+
.sv-attd-url {
|
|
346
|
+
width: 220px; height: 30px; padding: 0 8px; font: inherit; font-size: 13px;
|
|
347
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px;
|
|
348
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
349
|
+
}
|
|
350
|
+
.sv-attd-btn {
|
|
351
|
+
height: 30px; padding: 0 12px; font: inherit; font-size: 13px; cursor: pointer;
|
|
352
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px;
|
|
353
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
354
|
+
}
|
|
355
|
+
.sv-attd-btn.is-primary { background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff); border-color: transparent; }
|
|
356
|
+
.sv-attd-btn:disabled { opacity: 0.6; cursor: default; }
|
|
357
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Attachment, type AttachmentFieldProps } from './attachments.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
value: unknown;
|
|
4
|
+
title?: string;
|
|
5
|
+
props: AttachmentFieldProps;
|
|
6
|
+
row: unknown;
|
|
7
|
+
columnId: string;
|
|
8
|
+
onDone: (next: Attachment[]) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
};
|
|
11
|
+
declare const AttachmentDialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type AttachmentDialog = ReturnType<typeof AttachmentDialog>;
|
|
13
|
+
export default AttachmentDialog;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The grid mounts this when an attachment cell is edited (Enter, double
|
|
4
|
+
* click, typing): it opens the attachment dialog over the page and commits
|
|
5
|
+
* the dialog's list, or cancels. Registered with registerCellEditor as
|
|
6
|
+
* 'attachment' by enableFields().
|
|
7
|
+
*/
|
|
8
|
+
import AttachmentDialog from './AttachmentDialog.svelte'
|
|
9
|
+
import { fieldPropsOf } from './registry.svelte'
|
|
10
|
+
import type { Attachment, AttachmentFieldProps } from './attachments'
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
value,
|
|
14
|
+
columnDef,
|
|
15
|
+
columnId,
|
|
16
|
+
row,
|
|
17
|
+
onCommit,
|
|
18
|
+
onCancel,
|
|
19
|
+
}: {
|
|
20
|
+
value: unknown
|
|
21
|
+
columnDef?: Record<string, unknown>
|
|
22
|
+
columnId: string
|
|
23
|
+
row?: unknown
|
|
24
|
+
onCommit: (v?: unknown) => void
|
|
25
|
+
onCancel: () => void
|
|
26
|
+
} = $props()
|
|
27
|
+
|
|
28
|
+
const props = $derived(fieldPropsOf((columnDef ?? {}) as { fieldType?: unknown; fieldProps?: Record<string, unknown> }) as AttachmentFieldProps)
|
|
29
|
+
const title = $derived(typeof columnDef?.header === 'string' ? columnDef.header : 'Attachments')
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<span class="sv-att-editing" aria-hidden="true"></span>
|
|
33
|
+
<AttachmentDialog
|
|
34
|
+
{value}
|
|
35
|
+
{title}
|
|
36
|
+
{props}
|
|
37
|
+
{row}
|
|
38
|
+
{columnId}
|
|
39
|
+
onDone={(next: Attachment[]) => onCommit(next)}
|
|
40
|
+
onCancel={() => onCancel()}
|
|
41
|
+
/>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value: unknown;
|
|
3
|
+
columnDef?: Record<string, unknown>;
|
|
4
|
+
columnId: string;
|
|
5
|
+
row?: unknown;
|
|
6
|
+
onCommit: (v?: unknown) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
};
|
|
9
|
+
declare const AttachmentEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type AttachmentEditor = ReturnType<typeof AttachmentEditor>;
|
|
11
|
+
export default AttachmentEditor;
|