@proteos/ui 0.79.0 → 0.80.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/index.cjs +316 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +197 -23
- package/dist/index.d.ts +197 -23
- package/dist/index.js +303 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ava-action-review/ava-action-review.stories.tsx +313 -0
- package/src/components/{record-update/record-update.test.tsx → ava-action-review/ava-action-review.test.tsx} +52 -23
- package/src/components/ava-action-review/ava-action-review.tsx +612 -0
- package/src/components/ava-action-review/index.ts +30 -0
- package/src/components/checkbox/checkbox.tsx +12 -6
- package/src/components/radio-group/radio-group.tsx +11 -6
- package/src/components/switch/switch.tsx +15 -8
- package/src/index.ts +1 -1
- package/src/components/record-update/index.ts +0 -17
- package/src/components/record-update/record-update.stories.tsx +0 -141
- package/src/components/record-update/record-update.tsx +0 -259
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteos/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"license": "FSL-1.1-Apache-2.0",
|
|
5
5
|
"description": "Proteos React component library (React Aria Components + Tailwind v4).",
|
|
6
6
|
"repository": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"tailwindcss": "^4.2.4",
|
|
58
58
|
"thinking-orbs": "^0.3.1",
|
|
59
59
|
"wavesurfer.js": "8.0.5",
|
|
60
|
-
"@proteos/sdk": "0.
|
|
60
|
+
"@proteos/sdk": "0.80.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@biomejs/biome": "^2.4.13",
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { type ReactNode, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Badge } from '../badge/badge'
|
|
5
|
+
import { Button } from '../button/button'
|
|
6
|
+
import { GlowFrame } from '../glow-frame/glow-frame'
|
|
7
|
+
import { Input } from '../input/input'
|
|
8
|
+
import { Modal, ModalBody, ModalContent, ModalHeader, ModalTitle } from '../modal/modal'
|
|
9
|
+
import { Select } from '../select/select'
|
|
10
|
+
import { Textarea } from '../textarea/textarea'
|
|
11
|
+
import {
|
|
12
|
+
AvaActionReview,
|
|
13
|
+
AvaActionReviewActions,
|
|
14
|
+
AvaActionReviewAddField,
|
|
15
|
+
AvaActionReviewBody,
|
|
16
|
+
AvaActionReviewErrors,
|
|
17
|
+
AvaActionReviewFooter,
|
|
18
|
+
AvaActionReviewHead,
|
|
19
|
+
AvaActionReviewProblem,
|
|
20
|
+
AvaActionReviewReason,
|
|
21
|
+
AvaActionReviewRow,
|
|
22
|
+
type AvaActionReviewRowState,
|
|
23
|
+
AvaActionReviewRows,
|
|
24
|
+
AvaActionReviewSummary,
|
|
25
|
+
AvaActionReviewTitle,
|
|
26
|
+
} from './ava-action-review'
|
|
27
|
+
|
|
28
|
+
const meta: Meta<typeof AvaActionReviewRow> = {
|
|
29
|
+
title: 'Components/AvaActionReview',
|
|
30
|
+
component: AvaActionReviewRow,
|
|
31
|
+
tags: ['autodocs'],
|
|
32
|
+
argTypes: {
|
|
33
|
+
state: { control: 'radio', options: ['changed', 'added', 'unresolved', 'rejected', 'removed'] },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
type Story = StoryObj<typeof AvaActionReviewRow>
|
|
39
|
+
|
|
40
|
+
const STATUS_OPTIONS = [
|
|
41
|
+
{ id: 'open', label: 'Open' },
|
|
42
|
+
{ id: 'won', label: 'Won' },
|
|
43
|
+
{ id: 'churned', label: 'Churned' },
|
|
44
|
+
] as const
|
|
45
|
+
|
|
46
|
+
function ReadOnly({ children }: { children: ReactNode }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex min-h-8 items-center rounded-md bg-surface-2 px-2.5 py-1.5 text-[13px] text-ink">
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Row: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
label: 'Status',
|
|
57
|
+
from: <ReadOnly>Open</ReadOnly>,
|
|
58
|
+
to: <ReadOnly>Churned</ReadOnly>,
|
|
59
|
+
state: 'changed',
|
|
60
|
+
onRemove: () => {},
|
|
61
|
+
onRestore: () => {},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const LONG_FROM =
|
|
66
|
+
'Waiting on legal to confirm the updated data-processing terms before the renewal goes to procurement.'
|
|
67
|
+
const LONG_TO =
|
|
68
|
+
'Send the countersigned order form to procurement on Monday, then book the handover call with the customer success lead.'
|
|
69
|
+
const SUMMARY =
|
|
70
|
+
'Marks the renewal as won and raises the amount by 10% to match the signed order form.'
|
|
71
|
+
const ADD_GROUPS = [
|
|
72
|
+
{
|
|
73
|
+
label: 'On this form',
|
|
74
|
+
options: [
|
|
75
|
+
{ id: 'probability', label: 'Probability' },
|
|
76
|
+
{ id: 'renewal', label: 'Renewal date' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{ label: 'More fields', options: [{ id: 'competitor', label: 'Competitor' }] },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
function ReviewContent() {
|
|
83
|
+
const [status, setStatus] = useState('churned')
|
|
84
|
+
const [amount, setAmount] = useState('13200')
|
|
85
|
+
const [removed, setRemoved] = useState<Record<string, boolean>>({})
|
|
86
|
+
const stateFor = (key: string, fallback: AvaActionReviewRowState = 'changed') =>
|
|
87
|
+
removed[key] ? 'removed' : fallback
|
|
88
|
+
const toggle = (key: string, value: boolean) => setRemoved((prev) => ({ ...prev, [key]: value }))
|
|
89
|
+
return (
|
|
90
|
+
<AvaActionReview>
|
|
91
|
+
<AvaActionReviewHead>
|
|
92
|
+
<AvaActionReviewTitle eyebrow="Update">Renewal — Acme GmbH</AvaActionReviewTitle>
|
|
93
|
+
<AvaActionReviewSummary>{SUMMARY}</AvaActionReviewSummary>
|
|
94
|
+
</AvaActionReviewHead>
|
|
95
|
+
<AvaActionReviewBody>
|
|
96
|
+
<AvaActionReviewErrors
|
|
97
|
+
errors={['“Created at” is managed by the platform and cannot be changed.']}
|
|
98
|
+
/>
|
|
99
|
+
<AvaActionReviewRows>
|
|
100
|
+
<AvaActionReviewRow
|
|
101
|
+
label="Status"
|
|
102
|
+
state={stateFor('status')}
|
|
103
|
+
from={
|
|
104
|
+
<ReadOnly>
|
|
105
|
+
<Badge>Open</Badge>
|
|
106
|
+
</ReadOnly>
|
|
107
|
+
}
|
|
108
|
+
to={
|
|
109
|
+
<Select
|
|
110
|
+
size="sm"
|
|
111
|
+
aria-label="Status"
|
|
112
|
+
options={STATUS_OPTIONS}
|
|
113
|
+
selectedKey={status}
|
|
114
|
+
onSelectionChange={(key) => setStatus(String(key))}
|
|
115
|
+
/>
|
|
116
|
+
}
|
|
117
|
+
onRemove={() => toggle('status', true)}
|
|
118
|
+
onRestore={() => toggle('status', false)}
|
|
119
|
+
/>
|
|
120
|
+
<AvaActionReviewRow
|
|
121
|
+
label="Amount"
|
|
122
|
+
state={stateFor('amount')}
|
|
123
|
+
from={<ReadOnly>12,000</ReadOnly>}
|
|
124
|
+
to={<Input size="sm" aria-label="Amount" value={amount} onChange={setAmount} />}
|
|
125
|
+
onRemove={() => toggle('amount', true)}
|
|
126
|
+
onRestore={() => toggle('amount', false)}
|
|
127
|
+
/>
|
|
128
|
+
<AvaActionReviewRow
|
|
129
|
+
label="Owner"
|
|
130
|
+
state={stateFor('owner', 'unresolved')}
|
|
131
|
+
from={<ReadOnly>Lena Hartmann</ReadOnly>}
|
|
132
|
+
to={<Input size="sm" aria-label="Owner" placeholder="Pick a user…" />}
|
|
133
|
+
hint="“Julia” matches two people. Pick the user."
|
|
134
|
+
onRemove={() => toggle('owner', true)}
|
|
135
|
+
onRestore={() => toggle('owner', false)}
|
|
136
|
+
/>
|
|
137
|
+
<AvaActionReviewRow
|
|
138
|
+
label="Tier"
|
|
139
|
+
state="rejected"
|
|
140
|
+
to={
|
|
141
|
+
<span className="text-[12.5px] text-danger">
|
|
142
|
+
“Platinum” is not an option. Choose Silver or Gold.
|
|
143
|
+
</span>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
<AvaActionReviewRow
|
|
147
|
+
label="Next step"
|
|
148
|
+
isLong
|
|
149
|
+
state={stateFor('next')}
|
|
150
|
+
from={<ReadOnly>{LONG_FROM}</ReadOnly>}
|
|
151
|
+
to={<Textarea size="sm" aria-label="Next step" rows={3} defaultValue={LONG_TO} />}
|
|
152
|
+
onRemove={() => toggle('next', true)}
|
|
153
|
+
onRestore={() => toggle('next', false)}
|
|
154
|
+
/>
|
|
155
|
+
<AvaActionReviewRow
|
|
156
|
+
label="Probability"
|
|
157
|
+
state="added"
|
|
158
|
+
from={<ReadOnly>60</ReadOnly>}
|
|
159
|
+
to={<Input size="sm" aria-label="Probability" defaultValue="60" />}
|
|
160
|
+
onRemove={() => {}}
|
|
161
|
+
/>
|
|
162
|
+
</AvaActionReviewRows>
|
|
163
|
+
</AvaActionReviewBody>
|
|
164
|
+
<AvaActionReviewFooter>
|
|
165
|
+
<AvaActionReviewAddField groups={ADD_GROUPS} onPick={() => {}} />
|
|
166
|
+
<AvaActionReviewActions>
|
|
167
|
+
<Button variant="ghost" size="sm">
|
|
168
|
+
Cancel
|
|
169
|
+
</Button>
|
|
170
|
+
<Button variant="cta" size="sm">
|
|
171
|
+
Apply 3 changes
|
|
172
|
+
</Button>
|
|
173
|
+
</AvaActionReviewActions>
|
|
174
|
+
</AvaActionReviewFooter>
|
|
175
|
+
</AvaActionReview>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Drag the corner: below 448px of width the cells stack. */
|
|
180
|
+
export const Review: Story = {
|
|
181
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
182
|
+
render: () => (
|
|
183
|
+
<div className="h-[560px] w-[320px] min-w-[240px] max-w-[768px] resize-x overflow-hidden rounded-md border border-rule bg-surface">
|
|
184
|
+
<ReviewContent />
|
|
185
|
+
</div>
|
|
186
|
+
),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const Wide: Story = {
|
|
190
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
191
|
+
render: () => (
|
|
192
|
+
<div className="h-[640px] w-[672px] overflow-hidden rounded-md border border-rule bg-surface">
|
|
193
|
+
<ReviewContent />
|
|
194
|
+
</div>
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The foot stays at the bottom right when the rows are few. */
|
|
199
|
+
export const ShortInTallHost: Story = {
|
|
200
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
201
|
+
render: () => (
|
|
202
|
+
<div className="h-[560px] w-[320px] overflow-hidden rounded-md border border-rule bg-surface">
|
|
203
|
+
<AvaActionReview>
|
|
204
|
+
<AvaActionReviewHead>
|
|
205
|
+
<AvaActionReviewTitle
|
|
206
|
+
eyebrow="Update"
|
|
207
|
+
action={
|
|
208
|
+
<Button variant="link" size="sm" className="h-auto py-0 text-[12.5px]">
|
|
209
|
+
Change
|
|
210
|
+
</Button>
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
Renewal — Acme GmbH
|
|
214
|
+
</AvaActionReviewTitle>
|
|
215
|
+
<AvaActionReviewReason>
|
|
216
|
+
Same customer and an open stage. The only match.
|
|
217
|
+
</AvaActionReviewReason>
|
|
218
|
+
<AvaActionReviewSummary meta="1 suggested">{SUMMARY}</AvaActionReviewSummary>
|
|
219
|
+
<AvaActionReviewProblem>
|
|
220
|
+
No price book exists for 2027. The amount uses the current list price.
|
|
221
|
+
</AvaActionReviewProblem>
|
|
222
|
+
</AvaActionReviewHead>
|
|
223
|
+
<AvaActionReviewBody>
|
|
224
|
+
<AvaActionReviewRows>
|
|
225
|
+
<AvaActionReviewRow
|
|
226
|
+
label="Amount"
|
|
227
|
+
from={<ReadOnly>12,000</ReadOnly>}
|
|
228
|
+
to={<Input size="sm" aria-label="Amount" defaultValue="13200" />}
|
|
229
|
+
onRemove={() => {}}
|
|
230
|
+
/>
|
|
231
|
+
</AvaActionReviewRows>
|
|
232
|
+
</AvaActionReviewBody>
|
|
233
|
+
<AvaActionReviewFooter>
|
|
234
|
+
<AvaActionReviewAddField groups={ADD_GROUPS} onPick={() => {}} />
|
|
235
|
+
<AvaActionReviewActions>
|
|
236
|
+
<Button variant="ghost" size="sm">
|
|
237
|
+
Cancel
|
|
238
|
+
</Button>
|
|
239
|
+
<Button variant="cta" size="sm">
|
|
240
|
+
Apply 1 change
|
|
241
|
+
</Button>
|
|
242
|
+
</AvaActionReviewActions>
|
|
243
|
+
</AvaActionReviewFooter>
|
|
244
|
+
</AvaActionReview>
|
|
245
|
+
</div>
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The page hosts' modal: a long proposal scrolls in the body, the foot stays. */
|
|
250
|
+
export const InModal: Story = {
|
|
251
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
252
|
+
render: () => (
|
|
253
|
+
<Modal isOpen>
|
|
254
|
+
<ModalContent modalClassName="max-w-2xl">
|
|
255
|
+
<ModalHeader className="px-4 pt-4 pb-2">
|
|
256
|
+
<ModalTitle>Log renewal</ModalTitle>
|
|
257
|
+
</ModalHeader>
|
|
258
|
+
<ModalBody className="overflow-hidden p-0">
|
|
259
|
+
<div className="flex min-h-0 flex-col gap-2">
|
|
260
|
+
<GlowFrame status="ready" className="min-h-0 flex-1 rounded-md">
|
|
261
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
262
|
+
<ReviewContent />
|
|
263
|
+
</div>
|
|
264
|
+
</GlowFrame>
|
|
265
|
+
</div>
|
|
266
|
+
</ModalBody>
|
|
267
|
+
</ModalContent>
|
|
268
|
+
</Modal>
|
|
269
|
+
),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Press More: the head scrolls, the body keeps its room, the foot stays in the box. */
|
|
273
|
+
export const LongHeadInShortHost: Story = {
|
|
274
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
275
|
+
render: () => (
|
|
276
|
+
<div className="h-[300px] w-[320px] overflow-hidden rounded-md border border-rule bg-surface">
|
|
277
|
+
<AvaActionReview>
|
|
278
|
+
<AvaActionReviewHead>
|
|
279
|
+
<AvaActionReviewTitle eyebrow="Action">Create renewal quote</AvaActionReviewTitle>
|
|
280
|
+
<AvaActionReviewSummary meta="5 suggested">
|
|
281
|
+
{SUMMARY} {SUMMARY} {SUMMARY} {SUMMARY} {SUMMARY}
|
|
282
|
+
</AvaActionReviewSummary>
|
|
283
|
+
<AvaActionReviewProblem>
|
|
284
|
+
No price book exists for 2027. The amount uses the current list price.
|
|
285
|
+
</AvaActionReviewProblem>
|
|
286
|
+
<AvaActionReviewProblem>
|
|
287
|
+
The customer has no billing contact. Add one before you start.
|
|
288
|
+
</AvaActionReviewProblem>
|
|
289
|
+
</AvaActionReviewHead>
|
|
290
|
+
<AvaActionReviewBody>
|
|
291
|
+
<AvaActionReviewRows>
|
|
292
|
+
<AvaActionReviewRow
|
|
293
|
+
label="Amount"
|
|
294
|
+
from={<ReadOnly>12,000</ReadOnly>}
|
|
295
|
+
to={<Input size="sm" aria-label="Amount" defaultValue="13200" />}
|
|
296
|
+
onRemove={() => {}}
|
|
297
|
+
/>
|
|
298
|
+
</AvaActionReviewRows>
|
|
299
|
+
</AvaActionReviewBody>
|
|
300
|
+
<AvaActionReviewFooter>
|
|
301
|
+
<AvaActionReviewActions>
|
|
302
|
+
<Button variant="ghost" size="sm">
|
|
303
|
+
Cancel
|
|
304
|
+
</Button>
|
|
305
|
+
<Button variant="cta" size="sm">
|
|
306
|
+
Apply 1 change
|
|
307
|
+
</Button>
|
|
308
|
+
</AvaActionReviewActions>
|
|
309
|
+
</AvaActionReviewFooter>
|
|
310
|
+
</AvaActionReview>
|
|
311
|
+
</div>
|
|
312
|
+
),
|
|
313
|
+
}
|
|
@@ -3,16 +3,17 @@ import userEvent from '@testing-library/user-event'
|
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
AvaActionReview,
|
|
7
|
+
AvaActionReviewAddField,
|
|
8
|
+
AvaActionReviewErrors,
|
|
9
|
+
AvaActionReviewRow,
|
|
10
|
+
AvaActionReviewRows,
|
|
11
|
+
AvaActionReviewSummary,
|
|
12
|
+
} from './ava-action-review'
|
|
12
13
|
|
|
13
|
-
describe('
|
|
14
|
+
describe('AvaActionReviewRow', () => {
|
|
14
15
|
it('names the group after the label and shows both cells', () => {
|
|
15
|
-
render(<
|
|
16
|
+
render(<AvaActionReviewRow label="Status" from={<span>Open</span>} to={<span>Won</span>} />)
|
|
16
17
|
expect(screen.getByRole('group')).toHaveAttribute('data-state', 'changed')
|
|
17
18
|
expect(screen.getByText('Status')).toBeInTheDocument()
|
|
18
19
|
expect(screen.getByText('Open')).toBeInTheDocument()
|
|
@@ -24,7 +25,7 @@ describe('RecordUpdateRow', () => {
|
|
|
24
25
|
const onRemove = vi.fn()
|
|
25
26
|
const onRestore = vi.fn()
|
|
26
27
|
const { rerender } = render(
|
|
27
|
-
<
|
|
28
|
+
<AvaActionReviewRow
|
|
28
29
|
label="Status"
|
|
29
30
|
from="Open"
|
|
30
31
|
to="Won"
|
|
@@ -37,7 +38,7 @@ describe('RecordUpdateRow', () => {
|
|
|
37
38
|
expect(screen.queryByRole('button', { name: 'Restore change' })).not.toBeInTheDocument()
|
|
38
39
|
|
|
39
40
|
rerender(
|
|
40
|
-
<
|
|
41
|
+
<AvaActionReviewRow
|
|
41
42
|
label="Status"
|
|
42
43
|
from="Open"
|
|
43
44
|
to="Won"
|
|
@@ -53,14 +54,14 @@ describe('RecordUpdateRow', () => {
|
|
|
53
54
|
})
|
|
54
55
|
|
|
55
56
|
it('renders the proposed value alone when there is no current value', () => {
|
|
56
|
-
const { container } = render(<
|
|
57
|
+
const { container } = render(<AvaActionReviewRow label="Name" to={<span>Julia</span>} />)
|
|
57
58
|
expect(screen.getByText('Julia')).toBeInTheDocument()
|
|
58
59
|
expect(container.querySelector('svg')).toBeNull()
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
it('tags a rejected row and
|
|
62
|
+
it('tags a rejected row and shows only the explanation', () => {
|
|
62
63
|
render(
|
|
63
|
-
<
|
|
64
|
+
<AvaActionReviewRow
|
|
64
65
|
label="Created at"
|
|
65
66
|
from="2026-09-01"
|
|
66
67
|
to={<span>Read-only field</span>}
|
|
@@ -70,12 +71,13 @@ describe('RecordUpdateRow', () => {
|
|
|
70
71
|
expect(screen.getByRole('group')).toHaveAttribute('data-state', 'rejected')
|
|
71
72
|
expect(screen.getByText('not possible')).toBeInTheDocument()
|
|
72
73
|
expect(screen.getByText('Read-only field')).toBeInTheDocument()
|
|
74
|
+
expect(screen.queryByText('2026-09-01')).not.toBeInTheDocument()
|
|
73
75
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
74
76
|
})
|
|
75
77
|
|
|
76
78
|
it('tags an unresolved row and shows the hint; an error is an alert', () => {
|
|
77
79
|
render(
|
|
78
|
-
<
|
|
80
|
+
<AvaActionReviewRow
|
|
79
81
|
label="Owner"
|
|
80
82
|
from="Tonio"
|
|
81
83
|
to={<input aria-label="Owner" />}
|
|
@@ -91,16 +93,16 @@ describe('RecordUpdateRow', () => {
|
|
|
91
93
|
})
|
|
92
94
|
})
|
|
93
95
|
|
|
94
|
-
describe('
|
|
96
|
+
describe('AvaActionReview', () => {
|
|
95
97
|
it('composes summary, rows and the general errors alert', () => {
|
|
96
98
|
render(
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
</
|
|
102
|
-
<
|
|
103
|
-
</
|
|
99
|
+
<AvaActionReview>
|
|
100
|
+
<AvaActionReviewSummary>Two changes.</AvaActionReviewSummary>
|
|
101
|
+
<AvaActionReviewRows>
|
|
102
|
+
<AvaActionReviewRow label="Status" from="Open" to="Won" />
|
|
103
|
+
</AvaActionReviewRows>
|
|
104
|
+
<AvaActionReviewErrors errors={['Created at cannot be changed.']} />
|
|
105
|
+
</AvaActionReview>,
|
|
104
106
|
)
|
|
105
107
|
expect(screen.getByText('Two changes.')).toBeInTheDocument()
|
|
106
108
|
expect(screen.getByRole('group', { name: '' })).toBeInTheDocument()
|
|
@@ -108,7 +110,34 @@ describe('RecordUpdate', () => {
|
|
|
108
110
|
})
|
|
109
111
|
|
|
110
112
|
it('renders no errors block when the list is empty', () => {
|
|
111
|
-
render(<
|
|
113
|
+
render(<AvaActionReviewErrors errors={[]} />)
|
|
112
114
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
|
113
115
|
})
|
|
114
116
|
})
|
|
117
|
+
|
|
118
|
+
describe('AvaActionReviewRow labels', () => {
|
|
119
|
+
it('names the two cells for assistive tech and tags an added row', () => {
|
|
120
|
+
render(<AvaActionReviewRow label="Status" from="Open" to="Won" state="added" />)
|
|
121
|
+
expect(screen.getByText('Current')).toBeInTheDocument()
|
|
122
|
+
expect(screen.getByText('New')).toBeInTheDocument()
|
|
123
|
+
expect(screen.getByText('added')).toBeInTheDocument()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('AvaActionReviewAddField', () => {
|
|
128
|
+
const groups = [
|
|
129
|
+
{ label: 'On this form', options: [{ id: 'probability', label: 'Probability' }] },
|
|
130
|
+
{ label: 'More fields', options: [{ id: 'competitor', label: 'Competitor' }] },
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
it('filters as you type and reports the pick', async () => {
|
|
134
|
+
const user = userEvent.setup()
|
|
135
|
+
const onPick = vi.fn()
|
|
136
|
+
render(<AvaActionReviewAddField groups={groups} onPick={onPick} />)
|
|
137
|
+
await user.click(screen.getByRole('button', { name: 'Add field' }))
|
|
138
|
+
await user.type(await screen.findByRole('searchbox', { name: 'Find a field' }), 'comp')
|
|
139
|
+
expect(screen.queryByRole('menuitem', { name: 'Probability' })).not.toBeInTheDocument()
|
|
140
|
+
await user.click(screen.getByRole('menuitem', { name: 'Competitor' }))
|
|
141
|
+
expect(onPick).toHaveBeenCalledWith('competitor')
|
|
142
|
+
})
|
|
143
|
+
})
|