@carto/ps-react-ui 4.11.3 → 4.12.0
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/dist/chat.js +962 -733
- package/dist/chat.js.map +1 -1
- package/dist/csv-item-hH_Gt7ur.js +32 -0
- package/dist/csv-item-hH_Gt7ur.js.map +1 -0
- package/dist/png-item-9dNbB37T.js +57 -0
- package/dist/png-item-9dNbB37T.js.map +1 -0
- package/dist/table-B3ZWWhJt.js +383 -0
- package/dist/table-B3ZWWhJt.js.map +1 -0
- package/dist/types/chat/containers/chat-footer.d.ts +1 -1
- package/dist/types/chat/containers/styles.d.ts +79 -12
- package/dist/types/chat/index.d.ts +1 -1
- package/dist/types/chat/types.d.ts +21 -0
- package/dist/types/chat/use-typewriter.d.ts +5 -3
- package/dist/types/widgets-v2/actions/download/constants.d.ts +12 -0
- package/dist/types/widgets-v2/actions/download/csv-item.d.ts +38 -0
- package/dist/types/widgets-v2/actions/download/icons.d.ts +6 -0
- package/dist/types/widgets-v2/actions/download/index.d.ts +3 -1
- package/dist/types/widgets-v2/actions/index.d.ts +1 -1
- package/dist/widgets-v2/actions.js +40 -36
- package/dist/widgets-v2/actions.js.map +1 -1
- package/dist/widgets-v2/bar.js +77 -84
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +50 -55
- package/dist/widgets-v2/category.js.map +1 -1
- package/dist/widgets-v2/formula.js +37 -43
- package/dist/widgets-v2/formula.js.map +1 -1
- package/dist/widgets-v2/histogram.js +138 -144
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +18 -17
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2/pie.js +67 -73
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +75 -81
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +36 -41
- package/dist/widgets-v2/spread.js.map +1 -1
- package/dist/widgets-v2/table.js +46 -55
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +81 -87
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +1 -1
- package/package.json +1 -1
- package/src/chat/bubbles/styles.ts +5 -1
- package/src/chat/containers/chat-content.tsx +4 -1
- package/src/chat/containers/chat-footer.test.tsx +59 -0
- package/src/chat/containers/chat-footer.tsx +124 -36
- package/src/chat/containers/styles.ts +107 -16
- package/src/chat/feedback/styles.ts +11 -4
- package/src/chat/index.ts +1 -0
- package/src/chat/types.ts +22 -0
- package/src/chat/use-typewriter.ts +32 -24
- package/src/widgets-v2/actions/download/constants.ts +14 -0
- package/src/widgets-v2/actions/download/csv-item.test.tsx +77 -0
- package/src/widgets-v2/actions/download/csv-item.tsx +71 -0
- package/src/widgets-v2/actions/download/icons.tsx +10 -1
- package/src/widgets-v2/actions/download/index.ts +3 -1
- package/src/widgets-v2/actions/download/png-item.tsx +2 -1
- package/src/widgets-v2/actions/index.ts +5 -0
- package/src/widgets-v2/bar/download.tsx +16 -22
- package/src/widgets-v2/category/download.test.ts +9 -0
- package/src/widgets-v2/category/download.ts +16 -20
- package/src/widgets-v2/formula/download.tsx +23 -29
- package/src/widgets-v2/histogram/download.ts +22 -26
- package/src/widgets-v2/markdown/{download.ts → download.tsx} +5 -2
- package/src/widgets-v2/pie/download.ts +16 -20
- package/src/widgets-v2/scatterplot/download.ts +16 -20
- package/src/widgets-v2/spread/download.ts +23 -27
- package/src/widgets-v2/table/download.test.ts +10 -0
- package/src/widgets-v2/table/download.ts +11 -15
- package/src/widgets-v2/table/helpers.test.ts +19 -0
- package/src/widgets-v2/table/helpers.ts +7 -12
- package/src/widgets-v2/timeseries/download.ts +36 -40
- package/dist/png-item-BE9uEqlD.js +0 -45
- package/dist/png-item-BE9uEqlD.js.map +0 -1
- package/dist/table-C9IMbTr0.js +0 -385
- package/dist/table-C9IMbTr0.js.map +0 -1
- package/dist/types/chat/feedback/styles.d.ts +0 -211
package/src/chat/types.ts
CHANGED
|
@@ -110,9 +110,31 @@ export interface ChatFooterProps extends ChatSxProps {
|
|
|
110
110
|
send?: string
|
|
111
111
|
/** Defaults to `'Stop'`. */
|
|
112
112
|
stop?: string
|
|
113
|
+
/** `aria-label` for the model-selector trigger. Defaults to `'Select model'`. */
|
|
114
|
+
model?: string
|
|
113
115
|
}
|
|
114
116
|
/** Helper text rendered under the input. Defaults to an AI disclaimer; pass `null` to hide. */
|
|
115
117
|
caption?: ReactNode
|
|
118
|
+
/**
|
|
119
|
+
* Selectable models for the in-toolbar model selector. The selector is only
|
|
120
|
+
* rendered when this is a non-empty array; the component is otherwise
|
|
121
|
+
* model-agnostic (the host owns the list, selection, and default).
|
|
122
|
+
*/
|
|
123
|
+
models?: ChatModelOption[]
|
|
124
|
+
/** Currently selected model `value`. Controlled — the host owns the state. */
|
|
125
|
+
selectedModel?: string
|
|
126
|
+
/** Called with the picked model `value` when the user selects one. */
|
|
127
|
+
onModelChange?: (value: string) => void
|
|
128
|
+
/** Extra controls rendered at the start (left) of the toolbar, before the model selector. */
|
|
129
|
+
startToolbarSlot?: ReactNode
|
|
130
|
+
/** Extra controls rendered at the end (right) of the toolbar, before the send/stop button. */
|
|
131
|
+
endToolbarSlot?: ReactNode
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** An option for the `ChatFooter` model selector. */
|
|
135
|
+
export interface ChatModelOption {
|
|
136
|
+
value: string
|
|
137
|
+
label: string
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
// === Extras props ===
|
|
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
3
3
|
interface UseTypewriterOptions {
|
|
4
4
|
/** Characters revealed per second (default: `500`). */
|
|
5
5
|
speed?: number
|
|
6
|
-
/** When true
|
|
6
|
+
/** When true, reveal the full text immediately (and snap to it if it grows). */
|
|
7
7
|
skipAnimation?: boolean
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -16,8 +16,10 @@ interface UseTypewriterResult {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Reveals a string character-by-character at a steady rate via
|
|
19
|
-
* `requestAnimationFrame`.
|
|
20
|
-
*
|
|
19
|
+
* `requestAnimationFrame`. Designed for bursty, streamed agent text: as
|
|
20
|
+
* `fullText` grows the reveal keeps chasing the new end, and flipping
|
|
21
|
+
* `skipAnimation` to `true` (e.g. once generation finishes) snaps to the full
|
|
22
|
+
* text. Pair it with `ChatAgentMessage`.
|
|
21
23
|
*
|
|
22
24
|
* @example
|
|
23
25
|
* ```tsx
|
|
@@ -36,47 +38,53 @@ export function useTypewriter(
|
|
|
36
38
|
): UseTypewriterResult {
|
|
37
39
|
const { speed = 500, skipAnimation = false } = options
|
|
38
40
|
|
|
39
|
-
const skipRef = useRef(skipAnimation)
|
|
40
|
-
|
|
41
41
|
const [charIndex, setCharIndex] = useState(() =>
|
|
42
|
-
|
|
42
|
+
skipAnimation ? fullText.length : 0,
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
// Mirror of the revealed count, read/written ONLY inside the rAF loop (never
|
|
46
|
+
// during render). This lets the animation effect omit `charIndex` from its
|
|
47
|
+
// deps: were it a dep, the loop would tear down and restart on every revealed
|
|
48
|
+
// frame — resetting its frame timer — and never accumulate the elapsed time
|
|
49
|
+
// needed to advance, which stalls completely while `fullText` is also growing
|
|
50
|
+
// (the streaming case).
|
|
51
|
+
const charRef = useRef(charIndex)
|
|
52
|
+
|
|
45
53
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// When `skipAnimation` is set there's nothing to animate — the full text is
|
|
55
|
+
// shown directly via the derived `displayedText` below (no setState here, so
|
|
56
|
+
// a streamed message snaps to its full text the moment generation finishes,
|
|
57
|
+
// without an extra render or a `set-state-in-effect` cascade).
|
|
58
|
+
if (skipAnimation) return
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
60
|
+
// Already fully revealed (e.g. text hasn't grown since the last frame).
|
|
61
|
+
if (charRef.current >= fullText.length) return
|
|
54
62
|
|
|
55
63
|
const msPerChar = 1000 / speed
|
|
56
64
|
let rafId: number
|
|
57
65
|
let lastTime: number | null = null
|
|
58
66
|
|
|
59
67
|
function tick(timestamp: number) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const charsToAdd = Math.floor(
|
|
64
|
-
|
|
68
|
+
// Plain `?? =` not `??=`: the React Compiler can't lower logical-assignment
|
|
69
|
+
// operators and would bail this hook out of optimization.
|
|
70
|
+
lastTime = lastTime ?? timestamp
|
|
71
|
+
const charsToAdd = Math.floor((timestamp - lastTime) / msPerChar)
|
|
65
72
|
if (charsToAdd > 0) {
|
|
66
73
|
lastTime = timestamp
|
|
67
|
-
|
|
74
|
+
const next = Math.min(charRef.current + charsToAdd, fullText.length)
|
|
75
|
+
charRef.current = next
|
|
76
|
+
setCharIndex(next)
|
|
77
|
+
if (next >= fullText.length) return // fully revealed — stop the loop
|
|
68
78
|
}
|
|
69
|
-
|
|
70
79
|
rafId = requestAnimationFrame(tick)
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
rafId = requestAnimationFrame(tick)
|
|
74
|
-
|
|
75
83
|
return () => cancelAnimationFrame(rafId)
|
|
76
|
-
}, [fullText,
|
|
84
|
+
}, [fullText, speed, skipAnimation])
|
|
77
85
|
|
|
78
86
|
return {
|
|
79
|
-
displayedText: fullText.slice(0, charIndex),
|
|
80
|
-
isTyping: charIndex < fullText.length,
|
|
87
|
+
displayedText: skipAnimation ? fullText : fullText.slice(0, charIndex),
|
|
88
|
+
isTyping: !skipAnimation && charIndex < fullText.length,
|
|
81
89
|
}
|
|
82
90
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `DownloadItem.id`s for the built-in download formats. Centralised
|
|
3
|
+
* so the per-format builders — and anything that keys off the id (menu logic,
|
|
4
|
+
* tests) — reference one source of truth instead of re-typing string literals.
|
|
5
|
+
* The Markdown format keeps the short `'md'` id to match its `.md` extension.
|
|
6
|
+
*/
|
|
7
|
+
export const DOWNLOAD_ITEM_IDS = {
|
|
8
|
+
png: 'png',
|
|
9
|
+
csv: 'csv',
|
|
10
|
+
markdown: 'md',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export type DownloadItemId =
|
|
14
|
+
(typeof DOWNLOAD_ITEM_IDS)[keyof typeof DOWNLOAD_ITEM_IDS]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { buildCsvDownloadItem } from './csv-item'
|
|
3
|
+
|
|
4
|
+
let csvText = ''
|
|
5
|
+
let revokeSpy: ReturnType<typeof vi.spyOn>
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
csvText = ''
|
|
9
|
+
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:csv')
|
|
10
|
+
revokeSpy = vi
|
|
11
|
+
.spyOn(URL, 'revokeObjectURL')
|
|
12
|
+
.mockImplementation(() => undefined)
|
|
13
|
+
const RealBlob = global.Blob
|
|
14
|
+
vi.stubGlobal(
|
|
15
|
+
'Blob',
|
|
16
|
+
class extends RealBlob {
|
|
17
|
+
constructor(parts: BlobPart[], opts?: BlobPropertyBag) {
|
|
18
|
+
csvText = typeof parts[0] === 'string' ? parts[0] : ''
|
|
19
|
+
super(parts, opts)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('buildCsvDownloadItem', () => {
|
|
26
|
+
it('returns an item with the canonical CSV shape (id, label, icon)', () => {
|
|
27
|
+
const item = buildCsvDownloadItem({ filename: 'demo', getRows: () => [] })
|
|
28
|
+
expect(item.id).toBe('csv')
|
|
29
|
+
expect(item.label).toBe('CSV')
|
|
30
|
+
expect(item.icon).toBeTruthy()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('honours a label override', () => {
|
|
34
|
+
const item = buildCsvDownloadItem({
|
|
35
|
+
filename: 'demo',
|
|
36
|
+
getRows: () => [],
|
|
37
|
+
label: 'Export data',
|
|
38
|
+
})
|
|
39
|
+
expect(item.label).toBe('Export data')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('getRows path serialises rows via toCsvString and returns a .csv handle', async () => {
|
|
43
|
+
const item = buildCsvDownloadItem({
|
|
44
|
+
filename: 'sales',
|
|
45
|
+
getRows: () => [
|
|
46
|
+
['name', 'value'],
|
|
47
|
+
['a', 1],
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
const handle = await item.resolve()
|
|
51
|
+
expect(handle.url).toBe('blob:csv')
|
|
52
|
+
expect(handle.filename).toBe('sales.csv')
|
|
53
|
+
expect(csvText).toBe('name,value\na,1')
|
|
54
|
+
handle.revoke?.()
|
|
55
|
+
expect(revokeSpy).toHaveBeenCalledWith('blob:csv')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('getRows path applies the formula-injection guard', async () => {
|
|
59
|
+
const item = buildCsvDownloadItem({
|
|
60
|
+
filename: 'sales',
|
|
61
|
+
getRows: () => [['=SUM(A1)']],
|
|
62
|
+
})
|
|
63
|
+
await item.resolve()
|
|
64
|
+
expect(csvText).toBe("'=SUM(A1)")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('getCsv path passes the pre-built CSV through verbatim', async () => {
|
|
68
|
+
const item = buildCsvDownloadItem({
|
|
69
|
+
filename: 'table',
|
|
70
|
+
getCsv: () => 'col\n=raw',
|
|
71
|
+
})
|
|
72
|
+
const handle = await item.resolve()
|
|
73
|
+
expect(handle.filename).toBe('table.csv')
|
|
74
|
+
// getCsv is an escape hatch — content is used as-is, no extra escaping.
|
|
75
|
+
expect(csvText).toBe('col\n=raw')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
2
|
+
import { downloadToCSV } from './exports'
|
|
3
|
+
import { CSVIcon } from './icons'
|
|
4
|
+
import type { DownloadItem } from './types'
|
|
5
|
+
|
|
6
|
+
interface BuildCsvDownloadItemBase {
|
|
7
|
+
/** Base filename (without extension). The item appends `.csv`. */
|
|
8
|
+
filename: string
|
|
9
|
+
/** Override the menu label. Default `'CSV'`. */
|
|
10
|
+
label?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Args for {@link buildCsvDownloadItem}. A discriminated union: a caller must
|
|
15
|
+
* supply exactly one content source — `getRows` (rows serialised through the
|
|
16
|
+
* shared `toCsvString`) or `getCsv` (a pre-built CSV string). The `?: never`
|
|
17
|
+
* arms make passing both — or neither — a compile-time error.
|
|
18
|
+
*/
|
|
19
|
+
export type BuildCsvDownloadItemArgs =
|
|
20
|
+
| (BuildCsvDownloadItemBase & {
|
|
21
|
+
/**
|
|
22
|
+
* Builds the CSV rows at click time. Used by most widgets — rows are run
|
|
23
|
+
* through `toCsvString` so escaping (incl. the spreadsheet
|
|
24
|
+
* formula-injection guard) stays consistent across widgets.
|
|
25
|
+
*/
|
|
26
|
+
getRows: () => readonly (readonly unknown[])[]
|
|
27
|
+
getCsv?: never
|
|
28
|
+
})
|
|
29
|
+
| (BuildCsvDownloadItemBase & {
|
|
30
|
+
/**
|
|
31
|
+
* Returns a pre-built CSV string at click time. Escape hatch for widgets
|
|
32
|
+
* (e.g. Table) that already serialise their own CSV with bespoke
|
|
33
|
+
* header/cell handling.
|
|
34
|
+
*/
|
|
35
|
+
getCsv: () => string
|
|
36
|
+
getRows?: never
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds the standard CSV `DownloadItem` used by every per-widget download
|
|
41
|
+
* config. Centralised so the menu label, icon, and `.csv` filename suffix stay
|
|
42
|
+
* consistent across widgets — mirrors {@link buildPngDownloadItem} so neither
|
|
43
|
+
* format can drift again.
|
|
44
|
+
*/
|
|
45
|
+
export function buildCsvDownloadItem(
|
|
46
|
+
args: BuildCsvDownloadItemArgs,
|
|
47
|
+
): DownloadItem {
|
|
48
|
+
return {
|
|
49
|
+
id: DOWNLOAD_ITEM_IDS.csv,
|
|
50
|
+
label: args.label ?? 'CSV',
|
|
51
|
+
icon: <CSVIcon fontSize='small' />,
|
|
52
|
+
resolve: () => {
|
|
53
|
+
if (args.getCsv) {
|
|
54
|
+
const csv = args.getCsv()
|
|
55
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
56
|
+
const url = URL.createObjectURL(blob)
|
|
57
|
+
return Promise.resolve({
|
|
58
|
+
url,
|
|
59
|
+
filename: `${args.filename}.csv`,
|
|
60
|
+
revoke: () => URL.revokeObjectURL(url),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const handle = downloadToCSV(args.getRows())
|
|
64
|
+
return Promise.resolve({
|
|
65
|
+
url: handle.url,
|
|
66
|
+
filename: `${args.filename}.csv`,
|
|
67
|
+
revoke: handle.revoke,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SvgIcon, type SvgIconProps } from '@mui/material'
|
|
2
|
-
import { ImageOutlined } from '@mui/icons-material'
|
|
2
|
+
import { ArticleOutlined, ImageOutlined } from '@mui/icons-material'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Generic "image" glyph used for the PNG download item. Wraps MUI's
|
|
@@ -10,6 +10,15 @@ export function PNGIcon(props: SvgIconProps) {
|
|
|
10
10
|
return <ImageOutlined {...props} />
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Generic "document" glyph used for the Markdown download item. Wraps MUI's
|
|
15
|
+
* `ArticleOutlined`, mirroring {@link PNGIcon}, so the Markdown menu item
|
|
16
|
+
* carries an icon consistent with the other download formats.
|
|
17
|
+
*/
|
|
18
|
+
export function MarkdownIcon(props: SvgIconProps) {
|
|
19
|
+
return <ArticleOutlined {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* "CSV" rectangle with the letters spelled inside — matches v1 visual and is
|
|
15
24
|
* easier to recognise in a download menu than a generic table glyph.
|
|
@@ -7,7 +7,9 @@ export {
|
|
|
7
7
|
type DownloadHandle,
|
|
8
8
|
type DownloadDOMToPNGOptions,
|
|
9
9
|
} from './exports'
|
|
10
|
-
export { CSVIcon, PNGIcon } from './icons'
|
|
10
|
+
export { CSVIcon, PNGIcon, MarkdownIcon } from './icons'
|
|
11
11
|
export { buildPngDownloadItem, type BuildPngDownloadItemArgs } from './png-item'
|
|
12
|
+
export { buildCsvDownloadItem, type BuildCsvDownloadItemArgs } from './csv-item'
|
|
13
|
+
export { DOWNLOAD_ITEM_IDS, type DownloadItemId } from './constants'
|
|
12
14
|
export type { DownloadItem } from './types'
|
|
13
15
|
export { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
1
2
|
import { downloadDOMToPNG } from './exports'
|
|
2
3
|
import { PNGIcon } from './icons'
|
|
3
4
|
import type { DownloadItem } from './types'
|
|
@@ -29,7 +30,7 @@ export function buildPngDownloadItem(
|
|
|
29
30
|
args: BuildPngDownloadItemArgs,
|
|
30
31
|
): DownloadItem {
|
|
31
32
|
return {
|
|
32
|
-
id:
|
|
33
|
+
id: DOWNLOAD_ITEM_IDS.png,
|
|
33
34
|
label: args.label ?? 'PNG',
|
|
34
35
|
icon: <PNGIcon fontSize='small' />,
|
|
35
36
|
resolve: async () => {
|
|
@@ -54,14 +54,19 @@ export {
|
|
|
54
54
|
toCsvString,
|
|
55
55
|
triggerLinkDownload,
|
|
56
56
|
buildPngDownloadItem,
|
|
57
|
+
buildCsvDownloadItem,
|
|
57
58
|
CSVIcon,
|
|
58
59
|
PNGIcon,
|
|
60
|
+
MarkdownIcon,
|
|
59
61
|
DEFAULT_DOWNLOAD_LABELS,
|
|
62
|
+
DOWNLOAD_ITEM_IDS,
|
|
60
63
|
type DownloadProps,
|
|
61
64
|
type DownloadItem,
|
|
65
|
+
type DownloadItemId,
|
|
62
66
|
type DownloadHandle,
|
|
63
67
|
type DownloadDOMToPNGOptions,
|
|
64
68
|
type BuildPngDownloadItemArgs,
|
|
69
|
+
type BuildCsvDownloadItemArgs,
|
|
65
70
|
type DownloadLabels,
|
|
66
71
|
} from './download'
|
|
67
72
|
export {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildCsvDownloadItem,
|
|
3
3
|
buildPngDownloadItem,
|
|
4
|
-
downloadToCSV,
|
|
5
4
|
type DownloadItem,
|
|
6
5
|
} from '../actions/download'
|
|
7
6
|
import type { BarWidgetData } from './types'
|
|
@@ -42,25 +41,20 @@ export function createBarDownloadConfig(
|
|
|
42
41
|
}),
|
|
43
42
|
)
|
|
44
43
|
}
|
|
45
|
-
items.push(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
filename: `${args.filename}.csv`,
|
|
61
|
-
revoke: handle.revoke,
|
|
62
|
-
})
|
|
63
|
-
},
|
|
64
|
-
})
|
|
44
|
+
items.push(
|
|
45
|
+
buildCsvDownloadItem({
|
|
46
|
+
filename: args.filename,
|
|
47
|
+
getRows: () => {
|
|
48
|
+
const data = args.getData()
|
|
49
|
+
const rows: unknown[][] = []
|
|
50
|
+
for (const [i, series] of data.entries()) {
|
|
51
|
+
if (i > 0) rows.push([])
|
|
52
|
+
rows.push(['name', 'value'])
|
|
53
|
+
for (const d of series) rows.push([d.name, d.value])
|
|
54
|
+
}
|
|
55
|
+
return rows
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
65
59
|
return items
|
|
66
60
|
}
|
|
@@ -37,6 +37,15 @@ describe('createCategoryDownloadConfig', () => {
|
|
|
37
37
|
).toEqual(['csv'])
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
it('CSV item carries the canonical icon and short label', () => {
|
|
41
|
+
const item = createCategoryDownloadConfig({
|
|
42
|
+
filename: 'c',
|
|
43
|
+
getData: () => data,
|
|
44
|
+
}).find((i) => i.id === 'csv')!
|
|
45
|
+
expect(item.label).toBe('CSV')
|
|
46
|
+
expect(item.icon).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
40
49
|
it('prepends PNG when getCaptureEl is provided', () => {
|
|
41
50
|
const items = createCategoryDownloadConfig({
|
|
42
51
|
filename: 'c',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildCsvDownloadItem,
|
|
2
3
|
buildPngDownloadItem,
|
|
3
|
-
downloadToCSV,
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
} from '../actions/download'
|
|
6
6
|
import type { CategoryWidgetData } from './types'
|
|
@@ -30,25 +30,21 @@ export function createCategoryDownloadConfig(args: {
|
|
|
30
30
|
}),
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
|
-
items.push(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
items.push(
|
|
34
|
+
buildCsvDownloadItem({
|
|
35
|
+
filename: args.filename,
|
|
36
|
+
getRows: () => {
|
|
37
|
+
const data = args.getData()
|
|
38
|
+
const rows: unknown[][] = [['series', 'name', 'value']]
|
|
39
|
+
for (const [i, series] of data.entries()) {
|
|
40
|
+
const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
|
|
41
|
+
for (const item of series) {
|
|
42
|
+
rows.push([seriesName, item.name, item.value])
|
|
43
|
+
}
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
filename: `${args.filename}.csv`,
|
|
49
|
-
revoke: handle.revoke,
|
|
50
|
-
})
|
|
51
|
-
},
|
|
52
|
-
})
|
|
45
|
+
return rows
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
53
49
|
return items
|
|
54
50
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildCsvDownloadItem,
|
|
3
3
|
buildPngDownloadItem,
|
|
4
|
-
downloadToCSV,
|
|
5
4
|
type DownloadItem,
|
|
6
5
|
} from '../actions/download'
|
|
7
6
|
import type { FormulaWidgetData } from './types'
|
|
@@ -38,32 +37,27 @@ export function createFormulaDownloadConfig(
|
|
|
38
37
|
}),
|
|
39
38
|
)
|
|
40
39
|
}
|
|
41
|
-
items.push(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
filename: `${args.filename}.csv`,
|
|
64
|
-
revoke: handle.revoke,
|
|
65
|
-
})
|
|
66
|
-
},
|
|
67
|
-
})
|
|
40
|
+
items.push(
|
|
41
|
+
buildCsvDownloadItem({
|
|
42
|
+
filename: args.filename,
|
|
43
|
+
getRows: () => {
|
|
44
|
+
const data = args.getData()
|
|
45
|
+
const rows: unknown[][] = [
|
|
46
|
+
['series', 'prefix', 'value', 'suffix', 'note', 'delta'],
|
|
47
|
+
]
|
|
48
|
+
for (const d of data) {
|
|
49
|
+
rows.push([
|
|
50
|
+
d.series?.name ?? '',
|
|
51
|
+
d.prefix ?? '',
|
|
52
|
+
d.value,
|
|
53
|
+
d.suffix ?? '',
|
|
54
|
+
d.note ?? '',
|
|
55
|
+
d.delta?.value ?? '',
|
|
56
|
+
])
|
|
57
|
+
}
|
|
58
|
+
return rows
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
68
62
|
return items
|
|
69
63
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildCsvDownloadItem,
|
|
2
3
|
buildPngDownloadItem,
|
|
3
|
-
downloadToCSV,
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
} from '../actions/download'
|
|
6
6
|
import type { HistogramWidgetData } from './types'
|
|
@@ -31,30 +31,26 @@ export function createHistogramDownloadConfig(args: {
|
|
|
31
31
|
}),
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
|
-
items.push(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
revoke: handle.revoke,
|
|
56
|
-
})
|
|
57
|
-
},
|
|
58
|
-
})
|
|
34
|
+
items.push(
|
|
35
|
+
buildCsvDownloadItem({
|
|
36
|
+
filename: args.filename,
|
|
37
|
+
getRows: () => {
|
|
38
|
+
const data = args.getData()
|
|
39
|
+
const ticks = args.getTicks()
|
|
40
|
+
const seriesCount = data.length
|
|
41
|
+
const header: unknown[] = ['bin_low', 'bin_high']
|
|
42
|
+
for (let i = 0; i < seriesCount; i++) {
|
|
43
|
+
header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)
|
|
44
|
+
}
|
|
45
|
+
const rows: unknown[][] = [header]
|
|
46
|
+
for (let bin = 0; bin < Math.max(0, ticks.length - 1); bin++) {
|
|
47
|
+
const row: unknown[] = [ticks[bin], ticks[bin + 1]]
|
|
48
|
+
for (let s = 0; s < seriesCount; s++) row.push(data[s]?.[bin] ?? 0)
|
|
49
|
+
rows.push(row)
|
|
50
|
+
}
|
|
51
|
+
return rows
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
59
55
|
return items
|
|
60
56
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DOWNLOAD_ITEM_IDS,
|
|
3
|
+
MarkdownIcon,
|
|
2
4
|
buildPngDownloadItem,
|
|
3
5
|
toCsvString,
|
|
4
6
|
triggerLinkDownload,
|
|
@@ -31,8 +33,9 @@ export function createMarkdownDownloadConfig(args: {
|
|
|
31
33
|
)
|
|
32
34
|
}
|
|
33
35
|
items.push({
|
|
34
|
-
id:
|
|
35
|
-
label: '
|
|
36
|
+
id: DOWNLOAD_ITEM_IDS.markdown,
|
|
37
|
+
label: 'Markdown',
|
|
38
|
+
icon: <MarkdownIcon fontSize='small' />,
|
|
36
39
|
resolve: () => {
|
|
37
40
|
const data = args.getData()
|
|
38
41
|
const blob = new Blob([data.content ?? ''], {
|