@startsimpli/ui 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +183 -200
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/index.ts +6 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +6 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/editor-sidebar.tsx +6 -731
- package/src/components/email-editor/email-editor.tsx +78 -467
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +1 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +134 -881
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
2
|
+
import { GlobalStylesPanel } from '../GlobalStylesPanel'
|
|
3
|
+
import type { GlobalStyles } from '../../types'
|
|
4
|
+
import { DEFAULT_GLOBAL_STYLES } from '../../types'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Fixtures
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const baseStyles: GlobalStyles = {
|
|
11
|
+
backgroundColor: '#f3f4f6',
|
|
12
|
+
contentWidth: 600,
|
|
13
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
14
|
+
theme: 'clean',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderPanel(styles: GlobalStyles = baseStyles, onChange = jest.fn()) {
|
|
18
|
+
return render(<GlobalStylesPanel globalStyles={styles} onChange={onChange} />)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Renders style inputs
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
describe('GlobalStylesPanel — renders style inputs', () => {
|
|
26
|
+
it('renders the Background Color label', () => {
|
|
27
|
+
renderPanel()
|
|
28
|
+
expect(screen.getByText('Background Color')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders the background color text input with current value', () => {
|
|
32
|
+
renderPanel()
|
|
33
|
+
const inputs = screen.getAllByDisplayValue('#f3f4f6')
|
|
34
|
+
// At least one text input shows the background color value
|
|
35
|
+
expect(inputs.length).toBeGreaterThan(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders the Content Width input', () => {
|
|
39
|
+
renderPanel()
|
|
40
|
+
expect(screen.getByText('Content Width (px)')).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders content width number input with current value', () => {
|
|
44
|
+
renderPanel()
|
|
45
|
+
expect(screen.getByDisplayValue('600')).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renders the Font Family label', () => {
|
|
49
|
+
renderPanel()
|
|
50
|
+
expect(screen.getByText('Font Family')).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders the Themes section heading', () => {
|
|
54
|
+
renderPanel()
|
|
55
|
+
expect(screen.getByText('Themes')).toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders Clean theme preset button', () => {
|
|
59
|
+
renderPanel()
|
|
60
|
+
expect(screen.getByText('Clean')).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('renders Minimal theme preset button', () => {
|
|
64
|
+
renderPanel()
|
|
65
|
+
expect(screen.getByText('Minimal')).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('renders Bold theme preset button', () => {
|
|
69
|
+
renderPanel()
|
|
70
|
+
expect(screen.getByText('Bold')).toBeInTheDocument()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('renders theme description text for each preset', () => {
|
|
74
|
+
renderPanel()
|
|
75
|
+
expect(screen.getByText('White background with subtle gray accents')).toBeInTheDocument()
|
|
76
|
+
expect(screen.getByText('Pure white with thin borders')).toBeInTheDocument()
|
|
77
|
+
expect(screen.getByText('Dark background with vivid accents')).toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('renders color picker input for background color', () => {
|
|
81
|
+
const { container } = renderPanel()
|
|
82
|
+
const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement
|
|
83
|
+
expect(colorInput).toBeInTheDocument()
|
|
84
|
+
expect(colorInput.value).toBe('#f3f4f6')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders Email Design section heading', () => {
|
|
88
|
+
renderPanel()
|
|
89
|
+
expect(screen.getByText('Email Design')).toBeInTheDocument()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// onChange fires with updated styles
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
describe('GlobalStylesPanel — onChange fires with updated styles', () => {
|
|
98
|
+
it('calls onChange when background color text input changes', () => {
|
|
99
|
+
const onChange = jest.fn()
|
|
100
|
+
renderPanel(baseStyles, onChange)
|
|
101
|
+
const inputs = screen.getAllByDisplayValue('#f3f4f6')
|
|
102
|
+
// Find the text input (not color picker) — the Input component has type="text" by default
|
|
103
|
+
const textInput = inputs.find(
|
|
104
|
+
(el) => (el as HTMLInputElement).type !== 'color'
|
|
105
|
+
) as HTMLInputElement
|
|
106
|
+
fireEvent.change(textInput, { target: { value: '#ffffff' } })
|
|
107
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
108
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({ backgroundColor: '#ffffff' })
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('calls onChange when color picker changes', () => {
|
|
114
|
+
const onChange = jest.fn()
|
|
115
|
+
const { container } = renderPanel(baseStyles, onChange)
|
|
116
|
+
const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement
|
|
117
|
+
fireEvent.change(colorInput, { target: { value: '#123456' } })
|
|
118
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
119
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
120
|
+
expect.objectContaining({ backgroundColor: '#123456' })
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('calls onChange with parsed integer when content width changes', () => {
|
|
125
|
+
const onChange = jest.fn()
|
|
126
|
+
renderPanel(baseStyles, onChange)
|
|
127
|
+
const widthInput = screen.getByDisplayValue('600')
|
|
128
|
+
fireEvent.change(widthInput, { target: { value: '700' } })
|
|
129
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
130
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
131
|
+
expect.objectContaining({ contentWidth: 700 })
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('defaults contentWidth to 600 when invalid value entered', () => {
|
|
136
|
+
const onChange = jest.fn()
|
|
137
|
+
renderPanel(baseStyles, onChange)
|
|
138
|
+
const widthInput = screen.getByDisplayValue('600')
|
|
139
|
+
fireEvent.change(widthInput, { target: { value: 'abc' } })
|
|
140
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
141
|
+
expect.objectContaining({ contentWidth: 600 })
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('calls onChange with Clean theme preset globalStyles on Clean button click', () => {
|
|
146
|
+
const onChange = jest.fn()
|
|
147
|
+
renderPanel(baseStyles, onChange)
|
|
148
|
+
fireEvent.click(screen.getByText('Clean'))
|
|
149
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
150
|
+
const called = onChange.mock.calls[0][0] as GlobalStyles
|
|
151
|
+
expect(called.theme).toBe('clean')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('calls onChange with Minimal theme preset globalStyles on Minimal button click', () => {
|
|
155
|
+
const onChange = jest.fn()
|
|
156
|
+
renderPanel(baseStyles, onChange)
|
|
157
|
+
fireEvent.click(screen.getByText('Minimal'))
|
|
158
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
159
|
+
const called = onChange.mock.calls[0][0] as GlobalStyles
|
|
160
|
+
expect(called.theme).toBe('minimal')
|
|
161
|
+
expect(called.backgroundColor).toBe('#ffffff')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('calls onChange with Bold theme preset globalStyles on Bold button click', () => {
|
|
165
|
+
const onChange = jest.fn()
|
|
166
|
+
renderPanel(baseStyles, onChange)
|
|
167
|
+
fireEvent.click(screen.getByText('Bold'))
|
|
168
|
+
expect(onChange).toHaveBeenCalledTimes(1)
|
|
169
|
+
const called = onChange.mock.calls[0][0] as GlobalStyles
|
|
170
|
+
expect(called.theme).toBe('bold')
|
|
171
|
+
expect(called.backgroundColor).toBe('#1f2937')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('preserves all other globalStyles fields when only backgroundColor changes', () => {
|
|
175
|
+
const onChange = jest.fn()
|
|
176
|
+
renderPanel(baseStyles, onChange)
|
|
177
|
+
const inputs = screen.getAllByDisplayValue('#f3f4f6')
|
|
178
|
+
const textInput = inputs.find(
|
|
179
|
+
(el) => (el as HTMLInputElement).type !== 'color'
|
|
180
|
+
) as HTMLInputElement
|
|
181
|
+
fireEvent.change(textInput, { target: { value: '#aaaaaa' } })
|
|
182
|
+
const result = onChange.mock.calls[0][0] as GlobalStyles
|
|
183
|
+
expect(result.contentWidth).toBe(baseStyles.contentWidth)
|
|
184
|
+
expect(result.fontFamily).toBe(baseStyles.fontFamily)
|
|
185
|
+
expect(result.theme).toBe(baseStyles.theme)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Active theme highlighting
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
describe('GlobalStylesPanel — active theme highlight', () => {
|
|
194
|
+
it('applies active border class to the currently selected theme button', () => {
|
|
195
|
+
const { container } = renderPanel({ ...baseStyles, theme: 'clean' })
|
|
196
|
+
const cleanButton = screen.getByText('Clean').closest('button') as HTMLElement
|
|
197
|
+
expect(cleanButton.className).toContain('border-primary')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('does not apply active border class to non-selected theme buttons', () => {
|
|
201
|
+
const { container } = renderPanel({ ...baseStyles, theme: 'clean' })
|
|
202
|
+
const minimalButton = screen.getByText('Minimal').closest('button') as HTMLElement
|
|
203
|
+
expect(minimalButton.className).not.toContain('border-primary')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('applies active class to Minimal when minimal theme is selected', () => {
|
|
207
|
+
renderPanel({ ...baseStyles, theme: 'minimal' })
|
|
208
|
+
const minimalButton = screen.getByText('Minimal').closest('button') as HTMLElement
|
|
209
|
+
expect(minimalButton.className).toContain('border-primary')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// DEFAULT_GLOBAL_STYLES as initial values
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
describe('GlobalStylesPanel — DEFAULT_GLOBAL_STYLES compatibility', () => {
|
|
218
|
+
it('renders correctly with DEFAULT_GLOBAL_STYLES', () => {
|
|
219
|
+
expect(() => renderPanel(DEFAULT_GLOBAL_STYLES)).not.toThrow()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('shows default content width 600', () => {
|
|
223
|
+
renderPanel(DEFAULT_GLOBAL_STYLES)
|
|
224
|
+
expect(screen.getByDisplayValue('600')).toBeInTheDocument()
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
2
|
+
import { QualityBadge } from '../QualityBadge'
|
|
3
|
+
import { EnrichButton } from '../EnrichButton'
|
|
4
|
+
import { EnrichmentProgress } from '../EnrichmentProgress'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// QualityBadge
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
describe('QualityBadge', () => {
|
|
11
|
+
it('renders grade A with correct label in title', () => {
|
|
12
|
+
render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
|
|
13
|
+
const badge = screen.getByTitle('Data quality: Excellent (95/100)')
|
|
14
|
+
expect(badge).toBeInTheDocument()
|
|
15
|
+
expect(badge).toHaveTextContent('A')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders grade B with correct title', () => {
|
|
19
|
+
render(<QualityBadge quality={{ grade: 'B', score: 80 }} />)
|
|
20
|
+
expect(screen.getByTitle('Data quality: Good (80/100)')).toBeInTheDocument()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('renders grade C with correct title', () => {
|
|
24
|
+
render(<QualityBadge quality={{ grade: 'C', score: 65 }} />)
|
|
25
|
+
expect(screen.getByTitle('Data quality: Fair (65/100)')).toBeInTheDocument()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders grade D with correct title', () => {
|
|
29
|
+
render(<QualityBadge quality={{ grade: 'D', score: 45 }} />)
|
|
30
|
+
expect(screen.getByTitle('Data quality: Poor (45/100)')).toBeInTheDocument()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('renders grade F with correct title', () => {
|
|
34
|
+
render(<QualityBadge quality={{ grade: 'F', score: 20 }} />)
|
|
35
|
+
expect(screen.getByTitle('Data quality: Incomplete (20/100)')).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does not show score by default', () => {
|
|
39
|
+
render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
|
|
40
|
+
expect(screen.queryByText('(95)')).not.toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('shows score when showScore is true', () => {
|
|
44
|
+
render(<QualityBadge quality={{ grade: 'A', score: 95 }} showScore />)
|
|
45
|
+
expect(screen.getByText('(95)')).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('applies green styles for grade A', () => {
|
|
49
|
+
render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
|
|
50
|
+
const badge = screen.getByTitle('Data quality: Excellent (95/100)')
|
|
51
|
+
expect(badge).toHaveClass('bg-green-100')
|
|
52
|
+
expect(badge).toHaveClass('text-green-800')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('applies red styles for grade F', () => {
|
|
56
|
+
render(<QualityBadge quality={{ grade: 'F', score: 20 }} />)
|
|
57
|
+
const badge = screen.getByTitle('Data quality: Incomplete (20/100)')
|
|
58
|
+
expect(badge).toHaveClass('bg-red-100')
|
|
59
|
+
expect(badge).toHaveClass('text-red-800')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('applies yellow styles for grade C', () => {
|
|
63
|
+
render(<QualityBadge quality={{ grade: 'C', score: 65 }} />)
|
|
64
|
+
const badge = screen.getByTitle('Data quality: Fair (65/100)')
|
|
65
|
+
expect(badge).toHaveClass('bg-yellow-100')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// EnrichButton
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
describe('EnrichButton', () => {
|
|
74
|
+
it('renders default label', () => {
|
|
75
|
+
render(<EnrichButton onClick={jest.fn()} />)
|
|
76
|
+
expect(screen.getByRole('button', { name: /enrich/i })).toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('renders custom label', () => {
|
|
80
|
+
render(<EnrichButton onClick={jest.fn()} label="Run Enrichment" />)
|
|
81
|
+
expect(screen.getByRole('button', { name: /run enrichment/i })).toBeInTheDocument()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('shows Enriching... when isLoading is true', () => {
|
|
85
|
+
render(<EnrichButton onClick={jest.fn()} isLoading />)
|
|
86
|
+
expect(screen.getByText('Enriching...')).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('is disabled when isLoading is true', () => {
|
|
90
|
+
render(<EnrichButton onClick={jest.fn()} isLoading />)
|
|
91
|
+
expect(screen.getByRole('button')).toBeDisabled()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('calls onClick when clicked', () => {
|
|
95
|
+
const onClick = jest.fn()
|
|
96
|
+
render(<EnrichButton onClick={onClick} />)
|
|
97
|
+
fireEvent.click(screen.getByRole('button'))
|
|
98
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('does not call onClick when disabled', () => {
|
|
102
|
+
const onClick = jest.fn()
|
|
103
|
+
render(<EnrichButton onClick={onClick} isLoading />)
|
|
104
|
+
fireEvent.click(screen.getByRole('button'))
|
|
105
|
+
expect(onClick).not.toHaveBeenCalled()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('applies sm size classes by default', () => {
|
|
109
|
+
render(<EnrichButton onClick={jest.fn()} />)
|
|
110
|
+
expect(screen.getByRole('button')).toHaveClass('px-3', 'py-1.5', 'text-xs')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('applies md size classes when size is md', () => {
|
|
114
|
+
render(<EnrichButton onClick={jest.fn()} size="md" />)
|
|
115
|
+
expect(screen.getByRole('button')).toHaveClass('px-4', 'py-2', 'text-sm')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('applies custom className', () => {
|
|
119
|
+
render(<EnrichButton onClick={jest.fn()} className="my-custom-class" />)
|
|
120
|
+
expect(screen.getByRole('button')).toHaveClass('my-custom-class')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// EnrichmentProgress
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe('EnrichmentProgress', () => {
|
|
129
|
+
const queueStatus = {
|
|
130
|
+
pending: 3,
|
|
131
|
+
processing: 1,
|
|
132
|
+
completed: 12,
|
|
133
|
+
failed: 0,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
it('renders unavailable message when queueStatus is null', () => {
|
|
137
|
+
render(<EnrichmentProgress queueStatus={null} />)
|
|
138
|
+
expect(screen.getByText('Queue status unavailable')).toBeInTheDocument()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('renders all four status counts', () => {
|
|
142
|
+
render(<EnrichmentProgress queueStatus={queueStatus} />)
|
|
143
|
+
expect(screen.getByText('3')).toBeInTheDocument() // pending
|
|
144
|
+
expect(screen.getByText('1')).toBeInTheDocument() // processing
|
|
145
|
+
expect(screen.getByText('12')).toBeInTheDocument() // completed
|
|
146
|
+
expect(screen.getByText('0')).toBeInTheDocument() // failed
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('renders all four status labels', () => {
|
|
150
|
+
render(<EnrichmentProgress queueStatus={queueStatus} />)
|
|
151
|
+
expect(screen.getByText('Pending')).toBeInTheDocument()
|
|
152
|
+
expect(screen.getByText('Processing')).toBeInTheDocument()
|
|
153
|
+
expect(screen.getByText('Completed')).toBeInTheDocument()
|
|
154
|
+
expect(screen.getByText('Failed')).toBeInTheDocument()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('does not render refresh button when onRefresh is not provided', () => {
|
|
158
|
+
render(<EnrichmentProgress queueStatus={queueStatus} />)
|
|
159
|
+
expect(screen.queryByText('Refresh')).not.toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('renders refresh button when onRefresh is provided', () => {
|
|
163
|
+
render(<EnrichmentProgress queueStatus={queueStatus} onRefresh={jest.fn()} />)
|
|
164
|
+
expect(screen.getByText('Refresh')).toBeInTheDocument()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('calls onRefresh when refresh button is clicked', () => {
|
|
168
|
+
const onRefresh = jest.fn()
|
|
169
|
+
render(<EnrichmentProgress queueStatus={queueStatus} onRefresh={onRefresh} />)
|
|
170
|
+
fireEvent.click(screen.getByText('Refresh'))
|
|
171
|
+
expect(onRefresh).toHaveBeenCalledTimes(1)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('disables refresh button when isLoading is true', () => {
|
|
175
|
+
render(
|
|
176
|
+
<EnrichmentProgress
|
|
177
|
+
queueStatus={queueStatus}
|
|
178
|
+
onRefresh={jest.fn()}
|
|
179
|
+
isLoading
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
expect(screen.getByText('Refresh').closest('button')).toBeDisabled()
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { format } from 'date-fns'
|
|
4
|
+
import type { TimelineItem } from './types'
|
|
5
|
+
import type { UseGanttStateReturn } from './hooks/useGanttState'
|
|
6
|
+
|
|
7
|
+
export interface GanttBoardViewProps {
|
|
8
|
+
boardColumns: UseGanttStateReturn['boardColumns']
|
|
9
|
+
boardDragItem: UseGanttStateReturn['boardDragItem']
|
|
10
|
+
setBoardDragItem: UseGanttStateReturn['setBoardDragItem']
|
|
11
|
+
onStatusChange: UseGanttStateReturn['onStatusChange']
|
|
12
|
+
categoryColors: UseGanttStateReturn['categoryColors']
|
|
13
|
+
handleItemClick: UseGanttStateReturn['handleItemClick']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function GanttBoardView({
|
|
17
|
+
boardColumns,
|
|
18
|
+
boardDragItem,
|
|
19
|
+
setBoardDragItem,
|
|
20
|
+
onStatusChange,
|
|
21
|
+
categoryColors,
|
|
22
|
+
handleItemClick,
|
|
23
|
+
}: GanttBoardViewProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="gantt-board">
|
|
26
|
+
{boardColumns.map((col) => (
|
|
27
|
+
<div
|
|
28
|
+
key={col.status}
|
|
29
|
+
className={`gantt-board-column ${boardDragItem ? 'gantt-board-drop-target' : ''}`}
|
|
30
|
+
onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('gantt-board-drag-over') }}
|
|
31
|
+
onDragLeave={(e) => { e.currentTarget.classList.remove('gantt-board-drag-over') }}
|
|
32
|
+
onDrop={(e) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
e.currentTarget.classList.remove('gantt-board-drag-over')
|
|
35
|
+
if (boardDragItem && onStatusChange) {
|
|
36
|
+
onStatusChange(boardDragItem, col.status)
|
|
37
|
+
}
|
|
38
|
+
setBoardDragItem(null)
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div className="gantt-board-column-header">
|
|
42
|
+
<span className={`gantt-status-badge gantt-status-${col.status}`}>{col.status.replace(/_/g, ' ')}</span>
|
|
43
|
+
<span className="gantt-board-count">{col.items.length}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="gantt-board-cards">
|
|
46
|
+
{col.items.map((item) => (
|
|
47
|
+
<div
|
|
48
|
+
key={item.id}
|
|
49
|
+
className="gantt-board-card"
|
|
50
|
+
draggable={!!onStatusChange}
|
|
51
|
+
onDragStart={() => setBoardDragItem(item.id)}
|
|
52
|
+
onDragEnd={() => setBoardDragItem(null)}
|
|
53
|
+
onClick={() => handleItemClick(item)}
|
|
54
|
+
>
|
|
55
|
+
<div className="gantt-board-card-title">{item.title}</div>
|
|
56
|
+
{item.category && (
|
|
57
|
+
<span className="gantt-category-badge" style={{ backgroundColor: categoryColors[item.category] || categoryColors.other || '#6b7280' }}>
|
|
58
|
+
{item.category}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
{item.endDate && (
|
|
62
|
+
<span className="gantt-board-card-date">{format(new Date(item.endDate), 'MMM d')}</span>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|