@carto/ps-react-ui 4.9.1 → 4.11.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/dist/category-Dnd2_j0x.js +719 -0
- package/dist/category-Dnd2_j0x.js.map +1 -0
- package/dist/change-column-DjjwoPt1.js +1143 -0
- package/dist/change-column-DjjwoPt1.js.map +1 -0
- package/dist/chat.js +1507 -0
- package/dist/chat.js.map +1 -0
- package/dist/components.js +122 -120
- package/dist/components.js.map +1 -1
- package/dist/copy-button-DGL1tyli.js +26 -0
- package/dist/copy-button-DGL1tyli.js.map +1 -0
- package/dist/{data-zoom-layout-0QSptXG_.js → data-zoom-layout-CkVnm6ej.js} +3 -3
- package/dist/{data-zoom-layout-0QSptXG_.js.map → data-zoom-layout-CkVnm6ej.js.map} +1 -1
- package/dist/{download-config-CzmjOT2T.js → download-config-oJIFZ2WC.js} +9 -8
- package/dist/{download-config-CzmjOT2T.js.map → download-config-oJIFZ2WC.js.map} +1 -1
- package/dist/{png-item-CS4z1iSH.js → png-item-BE9uEqlD.js} +2 -2
- package/dist/png-item-BE9uEqlD.js.map +1 -0
- package/dist/{spread-Y9R1f5dm.js → spread-DYNpzgh_.js} +10 -11
- package/dist/{spread-Y9R1f5dm.js.map → spread-DYNpzgh_.js.map} +1 -1
- package/dist/{table-CQCAnDLb.js → table-C9IMbTr0.js} +50 -53
- package/dist/table-C9IMbTr0.js.map +1 -0
- package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
- package/dist/types/chat/bubbles/index.d.ts +4 -0
- package/dist/types/chat/const.d.ts +4 -0
- package/dist/types/chat/containers/chat-content.d.ts +2 -0
- package/dist/types/chat/containers/chat-footer.d.ts +2 -0
- package/dist/types/chat/containers/chat-header.d.ts +2 -0
- package/dist/types/chat/containers/chat-starter.d.ts +2 -0
- package/dist/types/chat/containers/index.d.ts +4 -0
- package/dist/types/chat/containers/styles.d.ts +93 -0
- package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
- package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
- package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
- package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
- package/dist/types/chat/feedback/index.d.ts +8 -0
- package/dist/types/chat/feedback/styles.d.ts +211 -0
- package/dist/types/chat/index.d.ts +20 -0
- package/dist/types/chat/types.d.ts +184 -0
- package/dist/types/chat/use-typewriter.d.ts +30 -0
- package/dist/types/components/copy-button/copy-button.d.ts +2 -0
- package/dist/types/components/copy-button/types.d.ts +6 -0
- package/dist/types/components/index.d.ts +2 -0
- package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
- package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets/echart/types.d.ts +1 -1
- package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
- package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/index.d.ts +1 -0
- package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
- package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
- package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
- package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
- package/dist/types/widgets-v2/category/category.d.ts +9 -2
- package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
- package/dist/types/widgets-v2/category/style.d.ts +21 -2
- package/dist/types/widgets-v2/category/types.d.ts +2 -0
- package/dist/types/widgets-v2/index.d.ts +3 -2
- package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
- package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
- package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
- package/dist/widgets/actions.js +115 -114
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/category.js +9 -8
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/formula.js +11 -10
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +7 -6
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +9 -8
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +1 -1
- package/dist/widgets/spread.js +9 -8
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/table.js +17 -16
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +3 -2
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets-v2/actions.js +41 -37
- package/dist/widgets-v2/bar.js +9 -10
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +25 -26
- package/dist/widgets-v2/category.js.map +1 -1
- package/dist/widgets-v2/formula.js +3 -3
- package/dist/widgets-v2/histogram.js +11 -13
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +26 -27
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2/pie.js +8 -10
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +10 -12
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +15 -16
- package/dist/widgets-v2/spread.js.map +1 -1
- package/dist/widgets-v2/table.js +39 -40
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +9 -11
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2/utils.js +1 -1
- package/dist/widgets-v2.js +284 -282
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +5 -1
- package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
- package/src/chat/bubbles/chat-agent-message.tsx +11 -0
- package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
- package/src/chat/bubbles/chat-error-message.tsx +47 -0
- package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
- package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.tsx +27 -0
- package/src/chat/bubbles/index.ts +4 -0
- package/src/chat/bubbles/styles.ts +148 -0
- package/src/chat/const.ts +4 -0
- package/src/chat/containers/chat-content.test.tsx +269 -0
- package/src/chat/containers/chat-content.tsx +142 -0
- package/src/chat/containers/chat-footer.test.tsx +34 -0
- package/src/chat/containers/chat-footer.tsx +78 -0
- package/src/chat/containers/chat-header.test.tsx +28 -0
- package/src/chat/containers/chat-header.tsx +29 -0
- package/src/chat/containers/chat-starter.test.tsx +32 -0
- package/src/chat/containers/chat-starter.tsx +75 -0
- package/src/chat/containers/index.ts +4 -0
- package/src/chat/containers/styles.ts +96 -0
- package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
- package/src/chat/feedback/chat-actions-container.tsx +7 -0
- package/src/chat/feedback/chat-loader.test.tsx +10 -0
- package/src/chat/feedback/chat-loader.tsx +31 -0
- package/src/chat/feedback/chat-rating-action.tsx +43 -0
- package/src/chat/feedback/chat-thinking.test.tsx +15 -0
- package/src/chat/feedback/chat-thinking.tsx +23 -0
- package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
- package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
- package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
- package/src/chat/feedback/chat-tool-group.tsx +156 -0
- package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
- package/src/chat/feedback/chat-tool-trace.tsx +192 -0
- package/src/chat/feedback/get-tool-label.test.tsx +91 -0
- package/src/chat/feedback/get-tool-label.ts +13 -0
- package/src/chat/feedback/index.ts +8 -0
- package/src/chat/feedback/styles.ts +229 -0
- package/src/chat/index.ts +59 -0
- package/src/chat/types.ts +215 -0
- package/src/chat/use-typewriter.test.tsx +38 -0
- package/src/chat/use-typewriter.ts +82 -0
- package/src/components/copy-button/copy-button.test.tsx +41 -0
- package/src/components/copy-button/copy-button.tsx +31 -0
- package/src/components/copy-button/types.ts +10 -0
- package/src/components/index.ts +3 -0
- package/src/widgets/echart/types.ts +1 -1
- package/src/widgets-v2/actions/brush-toggle/brush-toggle.tsx +1 -1
- package/src/widgets-v2/actions/change-column/sortable-column-item.tsx +1 -1
- package/src/widgets-v2/actions/download/download.tsx +1 -1
- package/src/widgets-v2/actions/download/icons.tsx +1 -1
- package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +3 -3
- package/src/widgets-v2/actions/index.ts +8 -0
- package/src/widgets-v2/actions/lock-selection/lock-selection.tsx +2 -2
- package/src/widgets-v2/actions/relative-data/relative-data.tsx +1 -1
- package/src/widgets-v2/actions/searcher/searcher-toggle.tsx +1 -1
- package/src/widgets-v2/actions/searcher/searcher.tsx +2 -2
- package/src/widgets-v2/actions/show-all/index.ts +7 -0
- package/src/widgets-v2/actions/show-all/labels.ts +8 -0
- package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
- package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
- package/src/widgets-v2/actions/show-all/style.ts +8 -0
- package/src/widgets-v2/actions/stack-toggle/stack-toggle.tsx +1 -1
- package/src/widgets-v2/actions/zoom-toggle/zoom-toggle.tsx +1 -1
- package/src/widgets-v2/category/category-ui.test.tsx +26 -10
- package/src/widgets-v2/category/category-ui.tsx +13 -3
- package/src/widgets-v2/category/category.test.tsx +4 -4
- package/src/widgets-v2/category/category.tsx +10 -1
- package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
- package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
- package/src/widgets-v2/category/style.ts +35 -4
- package/src/widgets-v2/category/types.ts +2 -0
- package/src/widgets-v2/index.ts +3 -0
- package/src/widgets-v2/selection-summary/labels.ts +8 -4
- package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
- package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
- package/src/widgets-v2/selection-summary/style.ts +15 -0
- package/src/widgets-v2/table/table-ui.tsx +4 -4
- package/src/widgets-v2/toolbox/toolbox.tsx +1 -1
- package/src/widgets-v2/wrapper/widget-wrapper.tsx +1 -1
- package/dist/category-DwaeYjpX.js +0 -656
- package/dist/category-DwaeYjpX.js.map +0 -1
- package/dist/change-column-B4IT0rh6.js +0 -1110
- package/dist/change-column-B4IT0rh6.js.map +0 -1
- package/dist/png-item-CS4z1iSH.js.map +0 -1
- package/dist/table-CQCAnDLb.js.map +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseTypewriterOptions {
|
|
4
|
+
/** Characters revealed per second (default: `500`). */
|
|
5
|
+
speed?: number
|
|
6
|
+
/** When true on mount, skip the animation and reveal the full text immediately. */
|
|
7
|
+
skipAnimation?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseTypewriterResult {
|
|
11
|
+
/** The portion of `fullText` revealed so far. */
|
|
12
|
+
displayedText: string
|
|
13
|
+
/** `true` while characters are still being revealed. */
|
|
14
|
+
isTyping: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reveals a string character-by-character at a steady rate via
|
|
19
|
+
* `requestAnimationFrame`. Useful for smoothing out bursty WebSocket-streamed
|
|
20
|
+
* agent message text — pair it with `ChatAgentMessage`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { displayedText, isTyping } = useTypewriter(message)
|
|
25
|
+
* return (
|
|
26
|
+
* <ChatAgentMessage>
|
|
27
|
+
* <Markdown>{displayedText}</Markdown>
|
|
28
|
+
* {isTyping ? <Cursor /> : null}
|
|
29
|
+
* </ChatAgentMessage>
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useTypewriter(
|
|
34
|
+
fullText: string,
|
|
35
|
+
options: UseTypewriterOptions = {},
|
|
36
|
+
): UseTypewriterResult {
|
|
37
|
+
const { speed = 500, skipAnimation = false } = options
|
|
38
|
+
|
|
39
|
+
const skipRef = useRef(skipAnimation)
|
|
40
|
+
|
|
41
|
+
const [charIndex, setCharIndex] = useState(() =>
|
|
42
|
+
skipRef.current ? fullText.length : 0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (skipRef.current) {
|
|
47
|
+
setCharIndex(fullText.length)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (charIndex >= fullText.length) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const msPerChar = 1000 / speed
|
|
56
|
+
let rafId: number
|
|
57
|
+
let lastTime: number | null = null
|
|
58
|
+
|
|
59
|
+
function tick(timestamp: number) {
|
|
60
|
+
lastTime ??= timestamp
|
|
61
|
+
|
|
62
|
+
const elapsed = timestamp - lastTime
|
|
63
|
+
const charsToAdd = Math.floor(elapsed / msPerChar)
|
|
64
|
+
|
|
65
|
+
if (charsToAdd > 0) {
|
|
66
|
+
lastTime = timestamp
|
|
67
|
+
setCharIndex((prev) => Math.min(prev + charsToAdd, fullText.length))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
rafId = requestAnimationFrame(tick)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rafId = requestAnimationFrame(tick)
|
|
74
|
+
|
|
75
|
+
return () => cancelAnimationFrame(rafId)
|
|
76
|
+
}, [fullText, charIndex, speed])
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
displayedText: fullText.slice(0, charIndex),
|
|
80
|
+
isTyping: charIndex < fullText.length,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { CopyButton } from './copy-button'
|
|
4
|
+
|
|
5
|
+
vi.mock('@carto/ps-utils', () => ({
|
|
6
|
+
copy: vi.fn(() => Promise.resolve()),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
describe('CopyButton', () => {
|
|
10
|
+
test('renders copy button', () => {
|
|
11
|
+
render(
|
|
12
|
+
<CopyButton copyText='hello' onSuccess={vi.fn()} onError={vi.fn()} />,
|
|
13
|
+
)
|
|
14
|
+
expect(screen.getByRole('button')).toBeTruthy()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('renders with custom aria-label', () => {
|
|
18
|
+
render(
|
|
19
|
+
<CopyButton
|
|
20
|
+
copyText='hello'
|
|
21
|
+
onSuccess={vi.fn()}
|
|
22
|
+
onError={vi.fn()}
|
|
23
|
+
aria-label='Copy response'
|
|
24
|
+
/>,
|
|
25
|
+
)
|
|
26
|
+
expect(screen.getByLabelText('Copy response')).toBeTruthy()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('calls copy utility when clicked', async () => {
|
|
30
|
+
const { copy } = await import('@carto/ps-utils')
|
|
31
|
+
render(
|
|
32
|
+
<CopyButton
|
|
33
|
+
copyText='hello world'
|
|
34
|
+
onSuccess={vi.fn()}
|
|
35
|
+
onError={vi.fn()}
|
|
36
|
+
/>,
|
|
37
|
+
)
|
|
38
|
+
fireEvent.click(screen.getByRole('button'))
|
|
39
|
+
expect(copy).toHaveBeenCalledWith('hello world')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IconButton } from '@mui/material'
|
|
2
|
+
import { ContentCopyOutlined } from '@mui/icons-material'
|
|
3
|
+
import type { CopyButtonProps } from './types'
|
|
4
|
+
import { copy } from '@carto/ps-utils'
|
|
5
|
+
|
|
6
|
+
export function CopyButton({
|
|
7
|
+
copyText,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError,
|
|
10
|
+
'aria-label': ariaLabel,
|
|
11
|
+
...props
|
|
12
|
+
}: CopyButtonProps) {
|
|
13
|
+
async function onCopy() {
|
|
14
|
+
try {
|
|
15
|
+
await copy(copyText)
|
|
16
|
+
onSuccess?.()
|
|
17
|
+
} catch (err) {
|
|
18
|
+
onError?.(err as Error)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return (
|
|
22
|
+
<IconButton
|
|
23
|
+
size='small'
|
|
24
|
+
onClick={() => void onCopy()}
|
|
25
|
+
aria-label={ariaLabel ?? 'Copy'}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<ContentCopyOutlined fontSize='small' />
|
|
29
|
+
</IconButton>
|
|
30
|
+
)
|
|
31
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -62,3 +62,6 @@ export type { BasemapsUIProps } from './basemaps/types'
|
|
|
62
62
|
|
|
63
63
|
export { Tooltip, setTooltipEnterDelay } from './tooltip/tooltip'
|
|
64
64
|
export { SmartTooltip } from './smart-tooltip/smart-tooltip'
|
|
65
|
+
|
|
66
|
+
export type { CopyButtonProps } from './copy-button/types'
|
|
67
|
+
export { CopyButton } from './copy-button/copy-button'
|
|
@@ -2,7 +2,7 @@ import type { EChartsOption } from 'echarts'
|
|
|
2
2
|
import type * as echarts from 'echarts'
|
|
3
3
|
import type { BaseWidgetState } from '../stores/types'
|
|
4
4
|
import type { Ref, RefObject } from 'react'
|
|
5
|
-
import { theme as CartoTheme } from '
|
|
5
|
+
import { theme as CartoTheme } from '@carto/meridian-ds/theme'
|
|
6
6
|
|
|
7
7
|
export type EchartOptionsProps = EChartsOption
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import HighlightAltIcon from '@mui/icons-material
|
|
3
|
+
import { HighlightAlt as HighlightAltIcon } from '@mui/icons-material'
|
|
4
4
|
import { Tooltip } from '../../../components'
|
|
5
5
|
import {
|
|
6
6
|
useEchartInstance,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useSortable } from '@dnd-kit/sortable'
|
|
2
2
|
import { CSS } from '@dnd-kit/utilities'
|
|
3
3
|
import { ListItemText, MenuItem } from '@mui/material'
|
|
4
|
-
import DragIndicatorIcon from '@mui/icons-material
|
|
4
|
+
import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'
|
|
5
5
|
import type { ComponentType, ReactNode } from 'react'
|
|
6
6
|
import type { SvgIconProps } from '@mui/material'
|
|
7
7
|
import { styles } from './style'
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
MenuItem,
|
|
9
9
|
type SvgIconProps,
|
|
10
10
|
} from '@mui/material'
|
|
11
|
-
import DownloadIcon from '@mui/icons-material
|
|
11
|
+
import { FileDownload as DownloadIcon } from '@mui/icons-material'
|
|
12
12
|
import { Tooltip } from '../../../components'
|
|
13
13
|
import { triggerLinkDownload } from './exports'
|
|
14
14
|
import type { DownloadItem } from './types'
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
Typography,
|
|
16
16
|
type SvgIconProps,
|
|
17
17
|
} from '@mui/material'
|
|
18
|
-
import FullscreenIcon from '@mui/icons-material
|
|
19
|
-
import FullscreenExitIcon from '@mui/icons-material
|
|
20
|
-
import CloseIcon from '@mui/icons-material
|
|
18
|
+
import { Fullscreen as FullscreenIcon } from '@mui/icons-material'
|
|
19
|
+
import { FullscreenExit as FullscreenExitIcon } from '@mui/icons-material'
|
|
20
|
+
import { Close as CloseIcon } from '@mui/icons-material'
|
|
21
21
|
import { Tooltip } from '../../../components'
|
|
22
22
|
import { getWidgetStore, useWidget, useWidgetId } from '../../stores'
|
|
23
23
|
import { DEFAULT_FULLSCREEN_LABELS, type FullScreenLabels } from './labels'
|
|
@@ -15,6 +15,14 @@ export {
|
|
|
15
15
|
type StackToggleProps,
|
|
16
16
|
type StackToggleLabels,
|
|
17
17
|
} from './stack-toggle'
|
|
18
|
+
export {
|
|
19
|
+
ShowAllToggle,
|
|
20
|
+
setShowAll,
|
|
21
|
+
SHOW_ALL_ID,
|
|
22
|
+
DEFAULT_SHOW_ALL_LABELS,
|
|
23
|
+
type ShowAllToggleProps,
|
|
24
|
+
type ShowAllLabels,
|
|
25
|
+
} from './show-all'
|
|
18
26
|
export {
|
|
19
27
|
ZoomToggle,
|
|
20
28
|
addZoom,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import LockIcon from '@mui/icons-material
|
|
4
|
-
import LockOpenIcon from '@mui/icons-material
|
|
3
|
+
import { Lock as LockIcon } from '@mui/icons-material'
|
|
4
|
+
import { LockOpen as LockOpenIcon } from '@mui/icons-material'
|
|
5
5
|
import { Tooltip } from '../../../components'
|
|
6
6
|
import { useTransform, useWidgetId } from '../../stores'
|
|
7
7
|
import type { TransformPair } from '../../stores'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import PercentIcon from '@mui/icons-material
|
|
3
|
+
import { Percent as PercentIcon } from '@mui/icons-material'
|
|
4
4
|
import { Tooltip } from '../../../components'
|
|
5
5
|
import {
|
|
6
6
|
getWidgetStore,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import SearchIcon from '@mui/icons-material
|
|
3
|
+
import { Search as SearchIcon } from '@mui/icons-material'
|
|
4
4
|
import { Tooltip } from '../../../components'
|
|
5
5
|
import {
|
|
6
6
|
getWidgetStore,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { IconButton, InputAdornment, TextField } from '@mui/material'
|
|
3
|
-
import SearchIcon from '@mui/icons-material
|
|
4
|
-
import ClearIcon from '@mui/icons-material
|
|
3
|
+
import { Search as SearchIcon } from '@mui/icons-material'
|
|
4
|
+
import { Clear as ClearIcon } from '@mui/icons-material'
|
|
5
5
|
import { debounce } from '@carto/ps-utils'
|
|
6
6
|
import { getWidgetStore, useWidget, useWidgetId } from '../../stores'
|
|
7
7
|
import { setSearcherText } from './searcher-toggle'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { Provider } from '../../provider/widget-provider'
|
|
4
|
+
import { clearAllWidgetStores, getWidgetStore } from '../../stores'
|
|
5
|
+
import { ShowAllToggle, setShowAll, SHOW_ALL_ID } from './show-all'
|
|
6
|
+
|
|
7
|
+
beforeEach(() => clearAllWidgetStores())
|
|
8
|
+
afterEach(() => clearAllWidgetStores())
|
|
9
|
+
|
|
10
|
+
describe('setShowAll', () => {
|
|
11
|
+
it('writes the flag under transformStates[SHOW_ALL_ID]', () => {
|
|
12
|
+
render(
|
|
13
|
+
<Provider id='sa1' data={[]}>
|
|
14
|
+
<span />
|
|
15
|
+
</Provider>,
|
|
16
|
+
)
|
|
17
|
+
setShowAll('sa1', true)
|
|
18
|
+
expect(
|
|
19
|
+
getWidgetStore('sa1').getState().transformStates[SHOW_ALL_ID]?.enabled,
|
|
20
|
+
).toBe(true)
|
|
21
|
+
setShowAll('sa1', false)
|
|
22
|
+
expect(
|
|
23
|
+
getWidgetStore('sa1').getState().transformStates[SHOW_ALL_ID]?.enabled,
|
|
24
|
+
).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('<ShowAllToggle>', () => {
|
|
29
|
+
it('renders a collapse (✕) button', () => {
|
|
30
|
+
render(
|
|
31
|
+
<Provider id='sa2' data={[]}>
|
|
32
|
+
<ShowAllToggle />
|
|
33
|
+
</Provider>,
|
|
34
|
+
)
|
|
35
|
+
expect(screen.getByRole('button', { name: 'Show less' })).toBeTruthy()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('clears the flag on click (collapses)', () => {
|
|
39
|
+
render(
|
|
40
|
+
<Provider id='sa3' data={[]}>
|
|
41
|
+
<ShowAllToggle />
|
|
42
|
+
</Provider>,
|
|
43
|
+
)
|
|
44
|
+
setShowAll('sa3', true)
|
|
45
|
+
fireEvent.click(screen.getByRole('button', { name: 'Show less' }))
|
|
46
|
+
expect(
|
|
47
|
+
getWidgetStore('sa3').getState().transformStates[SHOW_ALL_ID]?.enabled,
|
|
48
|
+
).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ComponentType } from 'react'
|
|
2
|
+
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
+
import { Close as CloseIcon } from '@mui/icons-material'
|
|
4
|
+
import { Tooltip } from '../../../components'
|
|
5
|
+
import { getWidgetStore, useWidgetId } from '../../stores'
|
|
6
|
+
import { DEFAULT_SHOW_ALL_LABELS, type ShowAllLabels } from './labels'
|
|
7
|
+
import { styles } from './style'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `show-all` flag is a pure UI signal (no data/config transform): the
|
|
11
|
+
* Category overflow row writes it `true` via {@link setShowAll}, the
|
|
12
|
+
* composer reads it with `useTransformEnabled(id, SHOW_ALL_ID)` to drop the
|
|
13
|
+
* row cap, and this button writes it back `false` to collapse. Stored under
|
|
14
|
+
* `transformStates` so `useTransformEnabled` can observe it like any other
|
|
15
|
+
* action flag — but it never registers a pipeline transform, so toggling it
|
|
16
|
+
* never re-runs the data pipeline.
|
|
17
|
+
*/
|
|
18
|
+
export const SHOW_ALL_ID = 'show-all'
|
|
19
|
+
|
|
20
|
+
export interface ShowAllToggleProps {
|
|
21
|
+
labels?: Partial<ShowAllLabels>
|
|
22
|
+
icon?: ComponentType<SvgIconProps>
|
|
23
|
+
iconProps?: SvgIconProps
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collapse (✕) affordance shown while a widget is expanded into its
|
|
28
|
+
* "show all" state. Clicking it clears the `show-all` flag, returning the
|
|
29
|
+
* widget to its capped view. Mount it conditionally (only while the flag is
|
|
30
|
+
* set) so it doesn't occupy a `Widget.Toolbox` visibility-budget slot when
|
|
31
|
+
* the widget is collapsed.
|
|
32
|
+
*/
|
|
33
|
+
export function ShowAllToggle({
|
|
34
|
+
labels,
|
|
35
|
+
icon: Icon = CloseIcon,
|
|
36
|
+
iconProps,
|
|
37
|
+
}: ShowAllToggleProps) {
|
|
38
|
+
const id = useWidgetId()
|
|
39
|
+
const _labels = { ...DEFAULT_SHOW_ALL_LABELS, ...labels }
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Tooltip title={_labels.toggle}>
|
|
43
|
+
<IconButton
|
|
44
|
+
size='small'
|
|
45
|
+
aria-label={_labels.toggle}
|
|
46
|
+
aria-pressed
|
|
47
|
+
onClick={() => setShowAll(id, false)}
|
|
48
|
+
sx={styles.toggle}
|
|
49
|
+
>
|
|
50
|
+
<Icon fontSize='small' {...iconProps} />
|
|
51
|
+
</IconButton>
|
|
52
|
+
</Tooltip>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Imperatively writes the `show-all` flag into the widget store. Mirrors
|
|
58
|
+
* {@link setSearcherText} — used by the Category overflow row (to expand)
|
|
59
|
+
* and the {@link ShowAllToggle} button (to collapse) without routing through
|
|
60
|
+
* a re-rendered subscription.
|
|
61
|
+
*/
|
|
62
|
+
export function setShowAll(widgetId: string, value: boolean): void {
|
|
63
|
+
getWidgetStore(widgetId).setState((s) => ({
|
|
64
|
+
transformStates: {
|
|
65
|
+
...s.transformStates,
|
|
66
|
+
[SHOW_ALL_ID]: {
|
|
67
|
+
...s.transformStates[SHOW_ALL_ID],
|
|
68
|
+
enabled: value,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import StackedBarChartIcon from '@mui/icons-material
|
|
3
|
+
import { StackedBarChart as StackedBarChartIcon } from '@mui/icons-material'
|
|
4
4
|
import { Tooltip } from '../../../components'
|
|
5
5
|
import { useSingleTransform, useWidgetId } from '../../stores'
|
|
6
6
|
import { addStack } from './transforms'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, type ComponentType } from 'react'
|
|
2
2
|
import { IconButton, useTheme, type SvgIconProps } from '@mui/material'
|
|
3
|
-
import ZoomInIcon from '@mui/icons-material
|
|
3
|
+
import { ZoomIn as ZoomInIcon } from '@mui/icons-material'
|
|
4
4
|
import { Tooltip } from '../../../components'
|
|
5
5
|
import {
|
|
6
6
|
getEchartInstance,
|
|
@@ -48,15 +48,15 @@ describe('<CategoryUI>', () => {
|
|
|
48
48
|
expect(screen.getAllByRole('button')).toHaveLength(12)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('default caps at 20 when data exceeds it → "
|
|
51
|
+
it('default caps at 20 when data exceeds it → "Others <count>" footer', () => {
|
|
52
52
|
const twentyFive = Array.from({ length: 25 }, (_, i) => ({
|
|
53
53
|
name: `n${i}`,
|
|
54
54
|
value: 25 - i,
|
|
55
55
|
}))
|
|
56
56
|
render(<CategoryUI data={[twentyFive]} />)
|
|
57
57
|
expect(screen.getAllByRole('button')).toHaveLength(20)
|
|
58
|
-
expect(screen.getByText('
|
|
59
|
-
expect(screen.getByText('
|
|
58
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
59
|
+
expect(screen.getByText('5')).toBeTruthy()
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('maxItems=null → no cap, no scroll (canonical form)', () => {
|
|
@@ -70,7 +70,7 @@ describe('<CategoryUI>', () => {
|
|
|
70
70
|
const { container } = render(
|
|
71
71
|
<CategoryUI data={[twentyFive]} maxItems={null} />,
|
|
72
72
|
)
|
|
73
|
-
expect(screen.queryByText('
|
|
73
|
+
expect(screen.queryByText('Others')).toBeNull()
|
|
74
74
|
expect(screen.getAllByRole('button')).toHaveLength(25)
|
|
75
75
|
const root = container.firstChild as HTMLElement
|
|
76
76
|
const list = root.firstChild as HTMLElement
|
|
@@ -86,15 +86,31 @@ describe('<CategoryUI>', () => {
|
|
|
86
86
|
render(
|
|
87
87
|
<CategoryUI data={[twentyFive]} maxItems={Number.POSITIVE_INFINITY} />,
|
|
88
88
|
)
|
|
89
|
-
expect(screen.queryByText('
|
|
89
|
+
expect(screen.queryByText('Others')).toBeNull()
|
|
90
90
|
expect(screen.getAllByRole('button')).toHaveLength(25)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
it('slices to maxItems and shows "
|
|
93
|
+
it('slices to maxItems and shows "Others <count>" footer', () => {
|
|
94
94
|
render(<CategoryUI data={[oneToTwelve()]} maxItems={5} />)
|
|
95
95
|
expect(screen.getAllByRole('button')).toHaveLength(5)
|
|
96
|
-
expect(screen.getByText('
|
|
97
|
-
expect(screen.getByText('
|
|
96
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
97
|
+
expect(screen.getByText('7')).toBeTruthy()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('onShowAll makes the Other row a button that fires the callback', () => {
|
|
101
|
+
const onShowAll = vi.fn()
|
|
102
|
+
render(
|
|
103
|
+
<CategoryUI
|
|
104
|
+
data={[oneToTwelve()]}
|
|
105
|
+
maxItems={5}
|
|
106
|
+
onShowAll={onShowAll}
|
|
107
|
+
/>,
|
|
108
|
+
)
|
|
109
|
+
// 5 category rows + the now-interactive "Others" row = 6 buttons.
|
|
110
|
+
const buttons = screen.getAllByRole('button')
|
|
111
|
+
expect(buttons).toHaveLength(6)
|
|
112
|
+
fireEvent.click(screen.getByText('Others'))
|
|
113
|
+
expect(onShowAll).toHaveBeenCalledTimes(1)
|
|
98
114
|
})
|
|
99
115
|
|
|
100
116
|
it('maxItems=0 is the no-cap-with-scroll sentinel: every row renders inside a fixed scroll viewport', () => {
|
|
@@ -107,7 +123,7 @@ describe('<CategoryUI>', () => {
|
|
|
107
123
|
<CategoryUI data={[oneToTwelve()]} maxItems={0} />,
|
|
108
124
|
)
|
|
109
125
|
expect(screen.getAllByRole('button')).toHaveLength(12)
|
|
110
|
-
expect(screen.queryByText('
|
|
126
|
+
expect(screen.queryByText('Others')).toBeNull()
|
|
111
127
|
expect(screen.queryByText(/more/)).toBeNull()
|
|
112
128
|
// List has the scroll viewport inline style applied.
|
|
113
129
|
const root = container.firstChild as HTMLElement
|
|
@@ -217,7 +233,7 @@ describe('<CategoryUI>', () => {
|
|
|
217
233
|
/>,
|
|
218
234
|
)
|
|
219
235
|
expect(screen.getByText('Others')).toBeTruthy()
|
|
220
|
-
expect(screen.getByText('
|
|
236
|
+
expect(screen.getByText('+9 hidden')).toBeTruthy()
|
|
221
237
|
})
|
|
222
238
|
|
|
223
239
|
it('large maxItems = no Other row', () => {
|
|
@@ -50,7 +50,7 @@ export interface CategoryUIProps {
|
|
|
50
50
|
* Caps the number of visible category rows.
|
|
51
51
|
*
|
|
52
52
|
* - `undefined` (omitted) — caps at {@link DEFAULT_MAX_ITEMS} (20).
|
|
53
|
-
* Surplus rows fold into a single
|
|
53
|
+
* Surplus rows fold into a single "Others <count>" footer row.
|
|
54
54
|
* `undefined` cannot mean "no cap" because it's consumed by the
|
|
55
55
|
* default-parameter syntax — pass `null` instead.
|
|
56
56
|
* - positive finite N — same as the default but with a custom cap.
|
|
@@ -71,6 +71,13 @@ export interface CategoryUIProps {
|
|
|
71
71
|
maxItems?: number | null
|
|
72
72
|
/** Labels for the "Other" overflow row. `{count}` placeholder is replaced. */
|
|
73
73
|
labels?: CategoryLabels
|
|
74
|
+
/**
|
|
75
|
+
* When provided, the "Other" overflow row becomes a button — clicking it
|
|
76
|
+
* fires `onShowAll`, which a composer typically wires to expand the widget
|
|
77
|
+
* (drop the row cap) so every category becomes reachable. When omitted the
|
|
78
|
+
* overflow row stays a static summary.
|
|
79
|
+
*/
|
|
80
|
+
onShowAll?: () => void
|
|
74
81
|
/** Manual override for the bar-width denominator. */
|
|
75
82
|
maxOverride?: number
|
|
76
83
|
/**
|
|
@@ -156,6 +163,7 @@ export function CategoryUI({
|
|
|
156
163
|
series,
|
|
157
164
|
maxItems = DEFAULT_MAX_ITEMS,
|
|
158
165
|
labels,
|
|
166
|
+
onShowAll,
|
|
159
167
|
maxOverride,
|
|
160
168
|
size = 'small',
|
|
161
169
|
stacked = false,
|
|
@@ -224,7 +232,7 @@ export function CategoryUI({
|
|
|
224
232
|
|
|
225
233
|
// Three modes:
|
|
226
234
|
// * positive finite N (default `DEFAULT_MAX_ITEMS = 20` when prop
|
|
227
|
-
// omitted): cap at N rows; surplus folds into "
|
|
235
|
+
// omitted): cap at N rows; surplus folds into "Others <count>".
|
|
228
236
|
// * `0`: no cap WITH scroll — every row renders, the list locks to a
|
|
229
237
|
// fixed viewport and scrolls internally. Composers use this to
|
|
230
238
|
// bypass pagination while the user is searching:
|
|
@@ -296,7 +304,7 @@ export function CategoryUI({
|
|
|
296
304
|
<Box sx={styles.root}>
|
|
297
305
|
<Box
|
|
298
306
|
ref={listRefCallback}
|
|
299
|
-
sx={styles.list}
|
|
307
|
+
sx={scrollMode ? { ...styles.list, ...styles.listScroll } : styles.list}
|
|
300
308
|
style={
|
|
301
309
|
scrollMaxHeight !== undefined
|
|
302
310
|
? { maxHeight: scrollMaxHeight, overflowY: 'auto' }
|
|
@@ -378,6 +386,8 @@ export function CategoryUI({
|
|
|
378
386
|
hiddenCount={hiddenCount}
|
|
379
387
|
otherLabel={labels?.other}
|
|
380
388
|
otherCountLabel={labels?.otherCount}
|
|
389
|
+
showAllLabel={labels?.showAll}
|
|
390
|
+
onShowAll={onShowAll}
|
|
381
391
|
/>
|
|
382
392
|
)}
|
|
383
393
|
</Box>
|
|
@@ -96,7 +96,7 @@ describe('<Category> bridge', () => {
|
|
|
96
96
|
// 3 visible rows + Other footer.
|
|
97
97
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
98
98
|
expect(screen.getByText('Rest')).toBeTruthy()
|
|
99
|
-
expect(screen.getByText('(
|
|
99
|
+
expect(screen.getByText('(3 hidden)')).toBeTruthy()
|
|
100
100
|
})
|
|
101
101
|
|
|
102
102
|
it('does NOT read `transformStates.searcher.enabled` from the store (composer-mediation contract)', () => {
|
|
@@ -119,7 +119,7 @@ describe('<Category> bridge', () => {
|
|
|
119
119
|
</Provider>,
|
|
120
120
|
)
|
|
121
121
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
122
|
-
expect(screen.getByText('
|
|
122
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
123
123
|
|
|
124
124
|
// Flip the searcher flag in the store — bridge should NOT react.
|
|
125
125
|
act(() => {
|
|
@@ -134,7 +134,7 @@ describe('<Category> bridge', () => {
|
|
|
134
134
|
|
|
135
135
|
// Still capped, Other still there.
|
|
136
136
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
137
|
-
expect(screen.getByText('
|
|
137
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
it('respects maxItems=0 (no-cap sentinel) from the consumer', () => {
|
|
@@ -153,7 +153,7 @@ describe('<Category> bridge', () => {
|
|
|
153
153
|
)
|
|
154
154
|
// No cap → all rows render, Other footer is suppressed.
|
|
155
155
|
expect(screen.getAllByRole('button')).toHaveLength(10)
|
|
156
|
-
expect(screen.queryByText('
|
|
156
|
+
expect(screen.queryByText('Others')).toBeNull()
|
|
157
157
|
})
|
|
158
158
|
|
|
159
159
|
it('forwards size="medium" through to the rendered bars', () => {
|