@proteos/ui 0.46.0 → 0.48.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 +154 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +152 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/channel-icon/channel-icon.tsx +2 -0
- package/src/components/checkbox/checkbox.tsx +8 -1
- package/src/components/data-table/data-table.test.tsx +112 -0
- package/src/components/data-table/data-table.tsx +135 -11
- package/src/components/data-table/index.ts +1 -1
- package/src/components/orb/index.ts +2 -0
- package/src/components/orb/orb.stories.tsx +25 -0
- package/src/components/orb/orb.tsx +57 -0
- package/src/components/record-filter/chip-value.tsx +17 -2
- package/src/components/record-filter/field-label.ts +29 -0
- package/src/components/record-filter/index.ts +1 -0
- package/src/components/record-filter/record-filter-chips.tsx +7 -1
- package/src/components/record-filter/record-filter.stories.tsx +3 -3
- package/src/components/record-filter/record-filter.test.tsx +86 -0
- package/src/components/record-filter/types.ts +2 -1
- package/src/index.ts +1 -0
- package/src/styles.css +1 -0
- package/src/test/setup.ts +30 -0
|
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
5
5
|
|
|
6
6
|
import { RecordFilterBuilderPanel } from './record-filter-builder-panel'
|
|
7
7
|
import { RecordFilterButton } from './record-filter-button'
|
|
8
|
+
import { formatValue } from './chip-value'
|
|
9
|
+
import { FIELD_LABEL_SEPARATOR, joinFieldLabel } from './field-label'
|
|
8
10
|
import { RecordFilterChips } from './record-filter-chips'
|
|
9
11
|
import { __resetShowDetailsStores } from './show-details-store'
|
|
10
12
|
import type { FilterField, FilterGroup } from './types'
|
|
@@ -435,3 +437,87 @@ describe('RecordFilterChips — nested groups', () => {
|
|
|
435
437
|
expect(screen.getByText(/· 2/)).toBeInTheDocument()
|
|
436
438
|
})
|
|
437
439
|
})
|
|
440
|
+
|
|
441
|
+
/* The chip rail composes its own breadcrumb from `parentLabel`, while the
|
|
442
|
+
consumer composes `label` when it builds the field list. Both must use
|
|
443
|
+
FIELD_LABEL_SEPARATOR, or one chip reads "Company · Name" beside another
|
|
444
|
+
reading "Company / Name". */
|
|
445
|
+
describe('RecordFilterChips — breadcrumb separator', () => {
|
|
446
|
+
const nestedFields: FilterField[] = [
|
|
447
|
+
{
|
|
448
|
+
path: 'billing_address.city',
|
|
449
|
+
label: `Billing Address${FIELD_LABEL_SEPARATOR}City`,
|
|
450
|
+
shortLabel: 'City',
|
|
451
|
+
parentLabel: 'Billing Address',
|
|
452
|
+
type: 'string',
|
|
453
|
+
},
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
it('separates the parent from the leaf with a middle dot', () => {
|
|
457
|
+
// Chips only render when expanded; the store defaults to collapsed.
|
|
458
|
+
window.localStorage.setItem('rf.showDetails', '1')
|
|
459
|
+
render(
|
|
460
|
+
<RecordFilterChips
|
|
461
|
+
fields={nestedFields}
|
|
462
|
+
filter={{
|
|
463
|
+
logicalOperator: 'and',
|
|
464
|
+
elements: [{ field: 'billing_address.city', operator: 'eq', value: 'Berlin' }],
|
|
465
|
+
}}
|
|
466
|
+
onChange={() => undefined}
|
|
467
|
+
/>,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
expect(screen.getByText('Billing Address', { exact: false }).textContent).toContain(
|
|
471
|
+
FIELD_LABEL_SEPARATOR,
|
|
472
|
+
)
|
|
473
|
+
expect(document.body.textContent).not.toContain('Billing Address / ')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('exposes the separator as the middle dot', () => {
|
|
477
|
+
expect(FIELD_LABEL_SEPARATOR).toBe(' · ')
|
|
478
|
+
expect(joinFieldLabel('Company', undefined, 'Name')).toBe('Company · Name')
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
/* An enum stores a system value and displays a declared label. Both the
|
|
483
|
+
multi-value branch and the chip badge already resolved it; a single value
|
|
484
|
+
did not, so the same filter read "is technical_decider" or "is any of
|
|
485
|
+
Technical Decider" depending only on the operator. */
|
|
486
|
+
describe('formatValue — enum labels', () => {
|
|
487
|
+
const roleField: FilterField = {
|
|
488
|
+
path: 'buying_role',
|
|
489
|
+
label: 'Buying Role',
|
|
490
|
+
shortLabel: 'Buying Role',
|
|
491
|
+
type: 'enum',
|
|
492
|
+
options: [
|
|
493
|
+
{ value: 'technical_decider', label: 'Technical Decider' },
|
|
494
|
+
{ value: 'economic_decider', label: 'Economic Decider' },
|
|
495
|
+
],
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
it('renders the label for a single value', () => {
|
|
499
|
+
expect(formatValue(roleField, 'eq', 'technical_decider')).toBe('Technical Decider')
|
|
500
|
+
expect(formatValue(roleField, 'ne', 'economic_decider')).toBe('Economic Decider')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('renders labels for a multi-value operator', () => {
|
|
504
|
+
expect(formatValue(roleField, 'in', ['technical_decider', 'economic_decider'])).toBe(
|
|
505
|
+
'Technical Decider, Economic Decider',
|
|
506
|
+
)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// A value the entity no longer declares still has to render as something.
|
|
510
|
+
it('falls back to the stored value when no option matches', () => {
|
|
511
|
+
expect(formatValue(roleField, 'eq', 'retired_role')).toBe('retired_role')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('leaves non-enum fields alone', () => {
|
|
515
|
+
const nameField: FilterField = {
|
|
516
|
+
path: 'name',
|
|
517
|
+
label: 'Name',
|
|
518
|
+
shortLabel: 'Name',
|
|
519
|
+
type: 'string',
|
|
520
|
+
}
|
|
521
|
+
expect(formatValue(nameField, 'eq', 'technical_decider')).toBe('technical_decider')
|
|
522
|
+
})
|
|
523
|
+
})
|
|
@@ -34,7 +34,8 @@ export type FilterFieldOption = {
|
|
|
34
34
|
export type FilterField = {
|
|
35
35
|
/** Dot-path to the attribute, e.g. `'owner.email'`. Stable identifier. */
|
|
36
36
|
path: string
|
|
37
|
-
/** Full nested label, e.g. `'Owner
|
|
37
|
+
/** Full nested label, e.g. `'Owner · Email'` — parts joined with
|
|
38
|
+
* `FIELD_LABEL_SEPARATOR`. Used in pickers. */
|
|
38
39
|
label: string
|
|
39
40
|
/** Leaf label only, e.g. `'Email'`. Used in chips. */
|
|
40
41
|
shortLabel: string
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from './components/message-composer'
|
|
|
33
33
|
export * from './components/modal'
|
|
34
34
|
export * from './components/multi-select'
|
|
35
35
|
export * from './components/nav'
|
|
36
|
+
export * from './components/orb'
|
|
36
37
|
export * from './components/popover'
|
|
37
38
|
export * from './components/progress-bar'
|
|
38
39
|
export * from './components/radio-group'
|
package/src/styles.css
CHANGED
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
/* Status pop — one-shot scale+fade entrance for the large success/error
|
|
123
123
|
status icons. Soft ease, no overshoot (nothing bounces). */
|
|
124
124
|
--animate-status-pop: status-pop var(--duration-slow) var(--ease-out-soft) both;
|
|
125
|
+
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
@keyframes indeterminate-progress {
|
package/src/test/setup.ts
CHANGED
|
@@ -4,6 +4,36 @@ import { afterEach, expect } from 'vitest'
|
|
|
4
4
|
|
|
5
5
|
expect.extend(matchers)
|
|
6
6
|
|
|
7
|
+
// jsdom implements neither Element.scrollTo nor IntersectionObserver, both of
|
|
8
|
+
// which the virtualized DataTable calls on mount. Stub them so scrolling
|
|
9
|
+
// components can be rendered in tests at all.
|
|
10
|
+
if (!Element.prototype.scrollTo) {
|
|
11
|
+
Element.prototype.scrollTo = () => {}
|
|
12
|
+
}
|
|
13
|
+
if (!('ResizeObserver' in globalThis)) {
|
|
14
|
+
class NoopResizeObserver implements ResizeObserver {
|
|
15
|
+
disconnect() {}
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
}
|
|
19
|
+
globalThis.ResizeObserver = NoopResizeObserver as unknown as typeof ResizeObserver
|
|
20
|
+
}
|
|
21
|
+
if (!('IntersectionObserver' in globalThis)) {
|
|
22
|
+
class NoopIntersectionObserver implements IntersectionObserver {
|
|
23
|
+
readonly root = null
|
|
24
|
+
readonly rootMargin = ''
|
|
25
|
+
readonly thresholds: ReadonlyArray<number> = []
|
|
26
|
+
disconnect() {}
|
|
27
|
+
observe() {}
|
|
28
|
+
unobserve() {}
|
|
29
|
+
takeRecords(): IntersectionObserverEntry[] {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
globalThis.IntersectionObserver =
|
|
34
|
+
NoopIntersectionObserver as unknown as typeof IntersectionObserver
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
afterEach(() => {
|
|
8
38
|
cleanup()
|
|
9
39
|
})
|