create-grist-widget 0.3.6 → 0.3.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 +79 -7
- package/bin/create-grist-widget.mjs +485 -90
- package/examples/attachment-gallery/WidgetApp.tsx +202 -0
- package/examples/bulk-mark-done/WidgetApp.tsx +43 -0
- package/examples/declared-columns/WidgetApp.tsx +42 -0
- package/examples/form-edit/WidgetApp.tsx +225 -0
- package/examples/hello-world/WidgetApp.tsx +15 -0
- package/examples/mark-done/WidgetApp.tsx +44 -0
- package/examples/reads-demo/WidgetApp.tsx +69 -0
- package/examples/rest-fetch/WidgetApp.tsx +49 -0
- package/examples/safe-parse/WidgetApp.tsx +61 -0
- package/examples/schema-migration/WidgetApp.tsx +67 -0
- package/examples/schema-preview/WidgetApp.tsx +26 -0
- package/examples/slice-hooks/WidgetApp.tsx +47 -0
- package/examples/spreadsheet-grid/GristDataSheetGrid.tsx +376 -0
- package/examples/spreadsheet-grid/GristRdgGrid.tsx +189 -0
- package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +293 -0
- package/examples/spreadsheet-grid/WidgetApp.tsx +133 -0
- package/examples/spreadsheet-grid/grist-rdg-choice-column.tsx +616 -0
- package/examples/spreadsheet-grid/grist-rdg-columns.ts +397 -0
- package/examples/task-board/WidgetApp.tsx +229 -0
- package/examples/theme-demo/WidgetApp.tsx +26 -0
- package/examples/widget-options/WidgetApp.tsx +34 -0
- package/examples/writes-demo/WidgetApp.tsx +75 -0
- package/manifest.json +304 -0
- package/package.json +14 -2
- package/src/git-gate.mjs +30 -0
- package/template/.github/workflows/deploy.yml +7 -3
- package/template/README.md +3 -2
- package/template/scripts/deploy.mjs +128 -18
- package/ui-source/ShadcnDataSheetGrid.css +117 -0
- package/ui-source/ShadcnDataSheetGrid.tsx +46 -0
- package/ui-source/badge.tsx +49 -0
- package/vendor/compose-example.mjs +252 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { memo, useLayoutEffect, useMemo, useRef } from "react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
import { X } from "lucide-react"
|
|
4
|
+
import type { CellComponent, CellProps, Column } from "react-datasheet-grid"
|
|
5
|
+
import { textColumn } from "react-datasheet-grid"
|
|
6
|
+
|
|
7
|
+
import type { GristChoiceListEntry } from "grist-widget-sdk"
|
|
8
|
+
|
|
9
|
+
export type GristRdgChoiceColumnData = { choices: GristChoiceListEntry[] }
|
|
10
|
+
export type GristRdgChoiceListColumnData = { choices: GristChoiceListEntry[] }
|
|
11
|
+
export type GristRdgRefListValue = { rowIds: number[]; displays?: string[] }
|
|
12
|
+
|
|
13
|
+
function matchPastedChoice(
|
|
14
|
+
raw: string,
|
|
15
|
+
choices: GristChoiceListEntry[]
|
|
16
|
+
): string | null {
|
|
17
|
+
const t = raw.replace(/[\n\r]+/g, " ").trim()
|
|
18
|
+
if (!t) return null
|
|
19
|
+
const lower = t.toLowerCase()
|
|
20
|
+
for (const c of choices) {
|
|
21
|
+
if (c.value === t) return c.value
|
|
22
|
+
if (c.label.toLowerCase() === lower) return c.value
|
|
23
|
+
}
|
|
24
|
+
return t
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ChoiceSelectCell = memo(function ChoiceSelectCell({
|
|
28
|
+
active,
|
|
29
|
+
focus,
|
|
30
|
+
rowData,
|
|
31
|
+
setRowData,
|
|
32
|
+
stopEditing,
|
|
33
|
+
disabled,
|
|
34
|
+
columnData,
|
|
35
|
+
}: CellProps<string | null, GristRdgChoiceColumnData>) {
|
|
36
|
+
const { choices } = columnData
|
|
37
|
+
const selectRef = useRef<HTMLSelectElement>(null)
|
|
38
|
+
const current = rowData == null || rowData === "" ? "" : String(rowData)
|
|
39
|
+
const meta = choices.find((c) => c.value === current)
|
|
40
|
+
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (focus && selectRef.current) {
|
|
43
|
+
selectRef.current.focus()
|
|
44
|
+
}
|
|
45
|
+
}, [focus])
|
|
46
|
+
|
|
47
|
+
if (focus && active) {
|
|
48
|
+
return (
|
|
49
|
+
<select
|
|
50
|
+
ref={selectRef}
|
|
51
|
+
className="dsg-input"
|
|
52
|
+
style={{ width: "100%", pointerEvents: "auto" }}
|
|
53
|
+
value={current}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
tabIndex={-1}
|
|
56
|
+
onChange={(e) => {
|
|
57
|
+
const v = e.target.value
|
|
58
|
+
setRowData(v === "" ? null : v)
|
|
59
|
+
stopEditing({ nextRow: false })
|
|
60
|
+
}}
|
|
61
|
+
onKeyDown={(e) => {
|
|
62
|
+
if (e.key === "Escape") {
|
|
63
|
+
e.stopPropagation()
|
|
64
|
+
stopEditing({ nextRow: false })
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<option value="" />
|
|
69
|
+
{choices.map((c) => (
|
|
70
|
+
<option key={c.value} value={c.value}>
|
|
71
|
+
{c.label}
|
|
72
|
+
</option>
|
|
73
|
+
))}
|
|
74
|
+
</select>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className="dsg-input"
|
|
81
|
+
style={{
|
|
82
|
+
pointerEvents: "none",
|
|
83
|
+
minHeight: 22,
|
|
84
|
+
display: "flex",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
padding: "0 6px",
|
|
87
|
+
overflow: "hidden",
|
|
88
|
+
textOverflow: "ellipsis",
|
|
89
|
+
whiteSpace: "nowrap",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{current ? (
|
|
93
|
+
<span
|
|
94
|
+
style={{
|
|
95
|
+
display: "inline-flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
backgroundColor: meta?.fillColor ?? "transparent",
|
|
98
|
+
color: meta?.textColor ?? "inherit",
|
|
99
|
+
borderRadius: 10,
|
|
100
|
+
padding: "1px 6px",
|
|
101
|
+
lineHeight: 1.4,
|
|
102
|
+
border: meta ? "1px solid transparent" : "1px solid #dc2626",
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{meta?.label ?? current}
|
|
106
|
+
</span>
|
|
107
|
+
) : (
|
|
108
|
+
""
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
ChoiceSelectCell.displayName = "ChoiceSelectCell"
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* react-datasheet-grid column for Grist `Choice` cells (string value + optional colors).
|
|
118
|
+
*/
|
|
119
|
+
export function createGristChoiceColumn(
|
|
120
|
+
choices: GristChoiceListEntry[]
|
|
121
|
+
): Partial<Column<string | null, GristRdgChoiceColumnData, string>> {
|
|
122
|
+
return {
|
|
123
|
+
...textColumn,
|
|
124
|
+
component: ChoiceSelectCell as unknown as CellComponent<string | null, GristRdgChoiceColumnData>,
|
|
125
|
+
columnData: { choices },
|
|
126
|
+
copyValue: ({ rowData }) =>
|
|
127
|
+
rowData == null || rowData === "" ? "" : String(rowData),
|
|
128
|
+
pasteValue: ({ value }) => matchPastedChoice(value, choices),
|
|
129
|
+
deleteValue: () => null,
|
|
130
|
+
isCellEmpty: ({ rowData }) =>
|
|
131
|
+
rowData === null || rowData === undefined || rowData === "",
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseChoiceListInput(
|
|
136
|
+
raw: string,
|
|
137
|
+
choices: GristChoiceListEntry[]
|
|
138
|
+
): string[] | null {
|
|
139
|
+
const tokens = raw
|
|
140
|
+
.split(",")
|
|
141
|
+
.map((part) => matchPastedChoice(part, choices))
|
|
142
|
+
.filter((part): part is string => Boolean(part && part.trim() !== ""))
|
|
143
|
+
if (tokens.length === 0) return null
|
|
144
|
+
return Array.from(new Set(tokens))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeChoiceListValue(
|
|
148
|
+
value: string[] | string | null | undefined,
|
|
149
|
+
choices: GristChoiceListEntry[]
|
|
150
|
+
): string[] {
|
|
151
|
+
if (Array.isArray(value)) return value
|
|
152
|
+
if (typeof value === "string")
|
|
153
|
+
return parseChoiceListInput(value, choices) ?? []
|
|
154
|
+
return []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ChoiceListCell = memo(function ChoiceListCell({
|
|
158
|
+
active,
|
|
159
|
+
focus,
|
|
160
|
+
rowData,
|
|
161
|
+
setRowData,
|
|
162
|
+
stopEditing,
|
|
163
|
+
disabled,
|
|
164
|
+
columnData,
|
|
165
|
+
}: CellProps<string[] | null, GristRdgChoiceListColumnData>) {
|
|
166
|
+
const { choices } = columnData
|
|
167
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
168
|
+
const values = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
normalizeChoiceListValue(
|
|
171
|
+
rowData as string[] | string | null | undefined,
|
|
172
|
+
choices
|
|
173
|
+
),
|
|
174
|
+
[choices, rowData]
|
|
175
|
+
)
|
|
176
|
+
const metaByValue = useMemo(
|
|
177
|
+
() => new Map(choices.map((c) => [c.value, c])),
|
|
178
|
+
[choices]
|
|
179
|
+
)
|
|
180
|
+
const [pendingText, setPendingText] = useState("")
|
|
181
|
+
|
|
182
|
+
const setValues = (next: string[]) => {
|
|
183
|
+
setRowData(next.length > 0 ? Array.from(new Set(next)) : null)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
useLayoutEffect(() => {
|
|
187
|
+
if (focus || active) {
|
|
188
|
+
const id = window.setTimeout(() => inputRef.current?.focus(), 0)
|
|
189
|
+
return () => window.clearTimeout(id)
|
|
190
|
+
}
|
|
191
|
+
}, [focus, active])
|
|
192
|
+
|
|
193
|
+
if (focus || active) {
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className="dsg-input"
|
|
197
|
+
style={{ width: "100%", pointerEvents: "auto", padding: "2px 4px" }}
|
|
198
|
+
onMouseDown={(e) => {
|
|
199
|
+
// Keep the edit interaction owned by this cell, not the grid wrapper.
|
|
200
|
+
e.stopPropagation()
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
display: "flex",
|
|
206
|
+
alignItems: "center",
|
|
207
|
+
gap: 4,
|
|
208
|
+
flexWrap: "wrap",
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{values.map((v, index) => {
|
|
212
|
+
const meta = metaByValue.get(v)
|
|
213
|
+
const label = meta?.label ?? v
|
|
214
|
+
const invalid = !meta
|
|
215
|
+
return (
|
|
216
|
+
<span
|
|
217
|
+
key={`${v}-${index}`}
|
|
218
|
+
style={{
|
|
219
|
+
display: "inline-flex",
|
|
220
|
+
alignItems: "center",
|
|
221
|
+
gap: 4,
|
|
222
|
+
backgroundColor: meta?.fillColor ?? "transparent",
|
|
223
|
+
color: meta?.textColor ?? "inherit",
|
|
224
|
+
borderRadius: 10,
|
|
225
|
+
padding: "1px 4px 1px 6px",
|
|
226
|
+
lineHeight: 1.4,
|
|
227
|
+
border: invalid
|
|
228
|
+
? "1px solid #dc2626"
|
|
229
|
+
: "1px solid transparent",
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<span>{label}</span>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
aria-label={`Remove ${label}`}
|
|
236
|
+
disabled={disabled}
|
|
237
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
238
|
+
onClick={() => {
|
|
239
|
+
setValues(values.filter((_, i) => i !== index))
|
|
240
|
+
}}
|
|
241
|
+
style={{
|
|
242
|
+
border: "none",
|
|
243
|
+
background: "transparent",
|
|
244
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
245
|
+
lineHeight: 1,
|
|
246
|
+
padding: 0,
|
|
247
|
+
color: "inherit",
|
|
248
|
+
display: "inline-flex",
|
|
249
|
+
alignItems: "center",
|
|
250
|
+
justifyContent: "center",
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<X size={10} strokeWidth={2.5} />
|
|
254
|
+
</button>
|
|
255
|
+
</span>
|
|
256
|
+
)
|
|
257
|
+
})}
|
|
258
|
+
<input
|
|
259
|
+
ref={inputRef}
|
|
260
|
+
autoFocus
|
|
261
|
+
size={Math.max(6, pendingText.length + 1)}
|
|
262
|
+
style={{
|
|
263
|
+
height: 28,
|
|
264
|
+
width: "auto",
|
|
265
|
+
minWidth: 48,
|
|
266
|
+
maxWidth: 260,
|
|
267
|
+
flex: "0 1 auto",
|
|
268
|
+
display: "inline-flex",
|
|
269
|
+
border: "none",
|
|
270
|
+
outline: "none",
|
|
271
|
+
background: "transparent",
|
|
272
|
+
}}
|
|
273
|
+
value={pendingText}
|
|
274
|
+
disabled={disabled}
|
|
275
|
+
placeholder="Type value, Enter to add"
|
|
276
|
+
onChange={(e) => setPendingText(e.target.value)}
|
|
277
|
+
onFocus={(e) => {
|
|
278
|
+
e.stopPropagation()
|
|
279
|
+
}}
|
|
280
|
+
onMouseDown={(e) => {
|
|
281
|
+
e.stopPropagation()
|
|
282
|
+
}}
|
|
283
|
+
onPointerDown={(e) => {
|
|
284
|
+
e.stopPropagation()
|
|
285
|
+
window.setTimeout(() => inputRef.current?.focus(), 0)
|
|
286
|
+
}}
|
|
287
|
+
onKeyDown={(e) => {
|
|
288
|
+
// Keep keyboard input inside the text field; RDG listens globally
|
|
289
|
+
// and can steal these events before the input consumes them.
|
|
290
|
+
e.stopPropagation()
|
|
291
|
+
if (e.key === "Enter") {
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
const next = pendingText.trim()
|
|
294
|
+
if (next) {
|
|
295
|
+
setValues([...values, next])
|
|
296
|
+
setPendingText("")
|
|
297
|
+
}
|
|
298
|
+
stopEditing({ nextRow: false })
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (e.key === "Escape") {
|
|
302
|
+
e.stopPropagation()
|
|
303
|
+
stopEditing({ nextRow: false })
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div
|
|
314
|
+
className="dsg-input"
|
|
315
|
+
style={{
|
|
316
|
+
pointerEvents: "none",
|
|
317
|
+
minHeight: 22,
|
|
318
|
+
display: "flex",
|
|
319
|
+
alignItems: "center",
|
|
320
|
+
gap: 4,
|
|
321
|
+
flexWrap: "wrap",
|
|
322
|
+
padding: "2px 6px",
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
{values.map((v, index) => {
|
|
326
|
+
const meta = metaByValue.get(v)
|
|
327
|
+
const label = meta?.label ?? v
|
|
328
|
+
const invalid = !meta
|
|
329
|
+
return (
|
|
330
|
+
<span
|
|
331
|
+
key={`${v}-${index}`}
|
|
332
|
+
style={{
|
|
333
|
+
backgroundColor: meta?.fillColor ?? "transparent",
|
|
334
|
+
color: meta?.textColor ?? "inherit",
|
|
335
|
+
borderRadius: 10,
|
|
336
|
+
padding: "1px 6px",
|
|
337
|
+
lineHeight: 1.4,
|
|
338
|
+
border: invalid ? "1px solid #dc2626" : "1px solid transparent",
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
{label}
|
|
342
|
+
</span>
|
|
343
|
+
)
|
|
344
|
+
})}
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
ChoiceListCell.displayName = "ChoiceListCell"
|
|
350
|
+
|
|
351
|
+
export function createGristChoiceListColumn(
|
|
352
|
+
choices: GristChoiceListEntry[]
|
|
353
|
+
): Partial<Column<string[] | null, GristRdgChoiceListColumnData, string>> {
|
|
354
|
+
return {
|
|
355
|
+
component: ChoiceListCell as unknown as CellComponent<string[] | null, GristRdgChoiceListColumnData>,
|
|
356
|
+
columnData: { choices },
|
|
357
|
+
copyValue: ({ rowData }) =>
|
|
358
|
+
Array.isArray(rowData) ? rowData.join(", ") : "",
|
|
359
|
+
pasteValue: ({ value }) => parseChoiceListInput(value, choices),
|
|
360
|
+
deleteValue: () => null,
|
|
361
|
+
isCellEmpty: ({ rowData }) =>
|
|
362
|
+
!Array.isArray(rowData) || rowData.length === 0,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function normalizeRefListValue(
|
|
367
|
+
value: GristRdgRefListValue | number[] | null | undefined
|
|
368
|
+
): GristRdgRefListValue {
|
|
369
|
+
if (!value) return { rowIds: [], displays: [] }
|
|
370
|
+
if (Array.isArray(value)) {
|
|
371
|
+
return {
|
|
372
|
+
rowIds: value.filter(
|
|
373
|
+
(v): v is number => typeof v === "number" && Number.isFinite(v) && v > 0
|
|
374
|
+
),
|
|
375
|
+
displays: [],
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (typeof value === "object" && Array.isArray(value.rowIds)) {
|
|
379
|
+
const rowIds = value.rowIds.filter(
|
|
380
|
+
(v): v is number => typeof v === "number" && Number.isFinite(v) && v > 0
|
|
381
|
+
)
|
|
382
|
+
const displays = Array.isArray(value.displays)
|
|
383
|
+
? value.displays.map((v) => String(v))
|
|
384
|
+
: []
|
|
385
|
+
return { rowIds, displays }
|
|
386
|
+
}
|
|
387
|
+
return { rowIds: [], displays: [] }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const RefListCell = memo(function RefListCell({
|
|
391
|
+
active,
|
|
392
|
+
focus,
|
|
393
|
+
rowData,
|
|
394
|
+
setRowData,
|
|
395
|
+
stopEditing,
|
|
396
|
+
disabled,
|
|
397
|
+
}: CellProps<GristRdgRefListValue | number[] | null, unknown>) {
|
|
398
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
399
|
+
const normalized = useMemo(
|
|
400
|
+
() =>
|
|
401
|
+
normalizeRefListValue(
|
|
402
|
+
rowData as GristRdgRefListValue | number[] | null | undefined
|
|
403
|
+
),
|
|
404
|
+
[rowData]
|
|
405
|
+
)
|
|
406
|
+
const rowIds = normalized.rowIds
|
|
407
|
+
const displays = normalized.displays ?? []
|
|
408
|
+
const [pendingText, setPendingText] = useState("")
|
|
409
|
+
|
|
410
|
+
const setValues = (nextRowIds: number[], nextDisplays?: string[]) => {
|
|
411
|
+
const ids = Array.from(
|
|
412
|
+
new Set(
|
|
413
|
+
nextRowIds.filter(
|
|
414
|
+
(v) => typeof v === "number" && Number.isFinite(v) && v > 0
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
if (ids.length === 0) {
|
|
419
|
+
setRowData(null)
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
setRowData({
|
|
423
|
+
rowIds: ids,
|
|
424
|
+
displays: Array.isArray(nextDisplays) ? nextDisplays : [],
|
|
425
|
+
} satisfies GristRdgRefListValue)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
useLayoutEffect(() => {
|
|
429
|
+
if (focus || active) {
|
|
430
|
+
const id = window.setTimeout(() => inputRef.current?.focus(), 0)
|
|
431
|
+
return () => window.clearTimeout(id)
|
|
432
|
+
}
|
|
433
|
+
}, [focus, active])
|
|
434
|
+
|
|
435
|
+
if (focus || active) {
|
|
436
|
+
return (
|
|
437
|
+
<div
|
|
438
|
+
className="dsg-input"
|
|
439
|
+
style={{ width: "100%", pointerEvents: "auto", padding: "2px 4px" }}
|
|
440
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
441
|
+
>
|
|
442
|
+
<div
|
|
443
|
+
style={{
|
|
444
|
+
display: "flex",
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
gap: 4,
|
|
447
|
+
flexWrap: "wrap",
|
|
448
|
+
}}
|
|
449
|
+
>
|
|
450
|
+
{rowIds.map((id, index) => {
|
|
451
|
+
const display =
|
|
452
|
+
typeof displays[index] === "string" ? displays[index].trim() : ""
|
|
453
|
+
const label = display !== "" ? display : String(id)
|
|
454
|
+
const ok = display !== ""
|
|
455
|
+
return (
|
|
456
|
+
<span
|
|
457
|
+
key={`${id}-${index}`}
|
|
458
|
+
style={{
|
|
459
|
+
display: "inline-flex",
|
|
460
|
+
alignItems: "center",
|
|
461
|
+
gap: 4,
|
|
462
|
+
backgroundColor: ok ? "#f0fdf4" : "#fef2f2",
|
|
463
|
+
color: ok ? "#166534" : "#991b1b",
|
|
464
|
+
borderRadius: 10,
|
|
465
|
+
padding: "1px 4px 1px 6px",
|
|
466
|
+
lineHeight: 1.4,
|
|
467
|
+
border: ok ? "1px solid #86efac" : "1px solid #dc2626",
|
|
468
|
+
}}
|
|
469
|
+
>
|
|
470
|
+
<span>{label}</span>
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
aria-label={`Remove ${label}`}
|
|
474
|
+
disabled={disabled}
|
|
475
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
476
|
+
onClick={() => {
|
|
477
|
+
const nextIds = rowIds.filter((_, i) => i !== index)
|
|
478
|
+
const nextDisplays = displays.filter((_, i) => i !== index)
|
|
479
|
+
setValues(nextIds, nextDisplays)
|
|
480
|
+
}}
|
|
481
|
+
style={{
|
|
482
|
+
border: "none",
|
|
483
|
+
background: "transparent",
|
|
484
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
485
|
+
lineHeight: 1,
|
|
486
|
+
padding: 0,
|
|
487
|
+
color: "inherit",
|
|
488
|
+
display: "inline-flex",
|
|
489
|
+
alignItems: "center",
|
|
490
|
+
justifyContent: "center",
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<X size={10} strokeWidth={2.5} />
|
|
494
|
+
</button>
|
|
495
|
+
</span>
|
|
496
|
+
)
|
|
497
|
+
})}
|
|
498
|
+
<input
|
|
499
|
+
ref={inputRef}
|
|
500
|
+
autoFocus
|
|
501
|
+
size={Math.max(6, pendingText.length + 1)}
|
|
502
|
+
style={{
|
|
503
|
+
height: 28,
|
|
504
|
+
width: "auto",
|
|
505
|
+
minWidth: 48,
|
|
506
|
+
maxWidth: 220,
|
|
507
|
+
flex: "0 1 auto",
|
|
508
|
+
display: "inline-flex",
|
|
509
|
+
border: "none",
|
|
510
|
+
outline: "none",
|
|
511
|
+
background: "transparent",
|
|
512
|
+
}}
|
|
513
|
+
value={pendingText}
|
|
514
|
+
disabled={disabled}
|
|
515
|
+
placeholder="Type ID, Enter to add"
|
|
516
|
+
onChange={(e) => setPendingText(e.target.value)}
|
|
517
|
+
onFocus={(e) => e.stopPropagation()}
|
|
518
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
519
|
+
onPointerDown={(e) => {
|
|
520
|
+
e.stopPropagation()
|
|
521
|
+
window.setTimeout(() => inputRef.current?.focus(), 0)
|
|
522
|
+
}}
|
|
523
|
+
onKeyDown={(e) => {
|
|
524
|
+
e.stopPropagation()
|
|
525
|
+
if (e.key === "Enter") {
|
|
526
|
+
e.preventDefault()
|
|
527
|
+
const n = Number.parseInt(pendingText.trim(), 10)
|
|
528
|
+
if (Number.isFinite(n) && n > 0) {
|
|
529
|
+
setValues([...rowIds, n], displays)
|
|
530
|
+
setPendingText("")
|
|
531
|
+
}
|
|
532
|
+
stopEditing({ nextRow: false })
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (e.key === "Escape") {
|
|
536
|
+
e.stopPropagation()
|
|
537
|
+
stopEditing({ nextRow: false })
|
|
538
|
+
}
|
|
539
|
+
}}
|
|
540
|
+
/>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<div
|
|
548
|
+
className="dsg-input"
|
|
549
|
+
style={{
|
|
550
|
+
pointerEvents: "none",
|
|
551
|
+
minHeight: 22,
|
|
552
|
+
display: "flex",
|
|
553
|
+
alignItems: "center",
|
|
554
|
+
gap: 4,
|
|
555
|
+
flexWrap: "wrap",
|
|
556
|
+
padding: "2px 6px",
|
|
557
|
+
}}
|
|
558
|
+
>
|
|
559
|
+
{rowIds.map((id, index) => {
|
|
560
|
+
const display =
|
|
561
|
+
typeof displays[index] === "string" ? displays[index].trim() : ""
|
|
562
|
+
const label = display !== "" ? display : String(id)
|
|
563
|
+
const ok = display !== ""
|
|
564
|
+
return (
|
|
565
|
+
<span
|
|
566
|
+
key={`${id}-${index}`}
|
|
567
|
+
style={{
|
|
568
|
+
backgroundColor: ok ? "#f0fdf4" : "#fef2f2",
|
|
569
|
+
color: ok ? "#166534" : "#991b1b",
|
|
570
|
+
borderRadius: 10,
|
|
571
|
+
padding: "1px 6px",
|
|
572
|
+
lineHeight: 1.4,
|
|
573
|
+
border: ok ? "1px solid #86efac" : "1px solid #dc2626",
|
|
574
|
+
}}
|
|
575
|
+
>
|
|
576
|
+
{label}
|
|
577
|
+
</span>
|
|
578
|
+
)
|
|
579
|
+
})}
|
|
580
|
+
</div>
|
|
581
|
+
)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
RefListCell.displayName = "RefListCell"
|
|
585
|
+
|
|
586
|
+
export function createGristRefListColumn(): Partial<
|
|
587
|
+
Column<GristRdgRefListValue | number[] | null, unknown, string>
|
|
588
|
+
> {
|
|
589
|
+
return {
|
|
590
|
+
component: RefListCell as unknown as CellComponent<GristRdgRefListValue | number[] | null, unknown>,
|
|
591
|
+
copyValue: ({ rowData }) => {
|
|
592
|
+
const normalized = normalizeRefListValue(
|
|
593
|
+
rowData as GristRdgRefListValue | number[] | null | undefined
|
|
594
|
+
)
|
|
595
|
+
return normalized.rowIds.join(", ")
|
|
596
|
+
},
|
|
597
|
+
pasteValue: ({ value }) => {
|
|
598
|
+
const ids = value
|
|
599
|
+
.split(",")
|
|
600
|
+
.map((token) => Number.parseInt(token.trim(), 10))
|
|
601
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
602
|
+
if (ids.length === 0) return null
|
|
603
|
+
return {
|
|
604
|
+
rowIds: Array.from(new Set(ids)),
|
|
605
|
+
displays: [],
|
|
606
|
+
} satisfies GristRdgRefListValue
|
|
607
|
+
},
|
|
608
|
+
deleteValue: () => null,
|
|
609
|
+
isCellEmpty: ({ rowData }) => {
|
|
610
|
+
const normalized = normalizeRefListValue(
|
|
611
|
+
rowData as GristRdgRefListValue | number[] | null | undefined
|
|
612
|
+
)
|
|
613
|
+
return normalized.rowIds.length === 0
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
}
|