@furystack/shades-common-components 12.4.0 → 12.5.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/CHANGELOG.md +56 -0
- package/esm/components/app-bar-link.spec.js +16 -19
- package/esm/components/app-bar-link.spec.js.map +1 -1
- package/esm/components/app-bar.spec.js +6 -4
- package/esm/components/app-bar.spec.js.map +1 -1
- package/esm/components/avatar.spec.js +9 -9
- package/esm/components/avatar.spec.js.map +1 -1
- package/esm/components/breadcrumb.spec.js +2 -2
- package/esm/components/breadcrumb.spec.js.map +1 -1
- package/esm/components/button-group.d.ts +32 -0
- package/esm/components/button-group.d.ts.map +1 -1
- package/esm/components/button-group.js +26 -2
- package/esm/components/button-group.js.map +1 -1
- package/esm/components/button-group.spec.js +127 -11
- package/esm/components/button-group.spec.js.map +1 -1
- package/esm/components/button.spec.js +4 -4
- package/esm/components/button.spec.js.map +1 -1
- package/esm/components/cache-view.spec.js +2 -3
- package/esm/components/cache-view.spec.js.map +1 -1
- package/esm/components/carousel.spec.js +47 -47
- package/esm/components/carousel.spec.js.map +1 -1
- package/esm/components/circular-progress.spec.js +2 -2
- package/esm/components/command-palette/command-palette-input.spec.js +23 -19
- package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
- package/esm/components/command-palette/command-palette-suggestion-list.spec.js +27 -27
- package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -1
- package/esm/components/command-palette/index.spec.js +64 -51
- package/esm/components/command-palette/index.spec.js.map +1 -1
- package/esm/components/context-menu/context-menu.spec.js +33 -33
- package/esm/components/context-menu/context-menu.spec.js.map +1 -1
- package/esm/components/data-grid/body.spec.js +13 -13
- package/esm/components/data-grid/body.spec.js.map +1 -1
- package/esm/components/data-grid/data-grid-row.spec.js +8 -8
- package/esm/components/data-grid/data-grid-row.spec.js.map +1 -1
- package/esm/components/data-grid/data-grid.d.ts +40 -2
- package/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid.js +7 -10
- package/esm/components/data-grid/data-grid.js.map +1 -1
- package/esm/components/data-grid/data-grid.spec.js +71 -28
- package/esm/components/data-grid/data-grid.spec.js.map +1 -1
- package/esm/components/data-grid/filters/boolean-filter.d.ts +12 -0
- package/esm/components/data-grid/filters/boolean-filter.d.ts.map +1 -0
- package/esm/components/data-grid/filters/boolean-filter.js +27 -0
- package/esm/components/data-grid/filters/boolean-filter.js.map +1 -0
- package/esm/components/data-grid/filters/boolean-filter.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/boolean-filter.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/boolean-filter.spec.js +114 -0
- package/esm/components/data-grid/filters/boolean-filter.spec.js.map +1 -0
- package/esm/components/data-grid/filters/date-filter.d.ts +12 -0
- package/esm/components/data-grid/filters/date-filter.d.ts.map +1 -0
- package/esm/components/data-grid/filters/date-filter.js +109 -0
- package/esm/components/data-grid/filters/date-filter.js.map +1 -0
- package/esm/components/data-grid/filters/date-filter.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/date-filter.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/date-filter.spec.js +145 -0
- package/esm/components/data-grid/filters/date-filter.spec.js.map +1 -0
- package/esm/components/data-grid/filters/enum-filter.d.ts +16 -0
- package/esm/components/data-grid/filters/enum-filter.d.ts.map +1 -0
- package/esm/components/data-grid/filters/enum-filter.js +72 -0
- package/esm/components/data-grid/filters/enum-filter.js.map +1 -0
- package/esm/components/data-grid/filters/enum-filter.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/enum-filter.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/enum-filter.spec.js +136 -0
- package/esm/components/data-grid/filters/enum-filter.spec.js.map +1 -0
- package/esm/components/data-grid/filters/filter-dropdown.d.ts +6 -0
- package/esm/components/data-grid/filters/filter-dropdown.d.ts.map +1 -0
- package/esm/components/data-grid/filters/filter-dropdown.js +41 -0
- package/esm/components/data-grid/filters/filter-dropdown.js.map +1 -0
- package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/filter-dropdown.spec.js +69 -0
- package/esm/components/data-grid/filters/filter-dropdown.spec.js.map +1 -0
- package/esm/components/data-grid/filters/filter-styles.d.ts +24 -0
- package/esm/components/data-grid/filters/filter-styles.d.ts.map +1 -0
- package/esm/components/data-grid/filters/filter-styles.js +25 -0
- package/esm/components/data-grid/filters/filter-styles.js.map +1 -0
- package/esm/components/data-grid/filters/index.d.ts +7 -0
- package/esm/components/data-grid/filters/index.d.ts.map +1 -0
- package/esm/components/data-grid/filters/index.js +7 -0
- package/esm/components/data-grid/filters/index.js.map +1 -0
- package/esm/components/data-grid/filters/number-filter.d.ts +12 -0
- package/esm/components/data-grid/filters/number-filter.d.ts.map +1 -0
- package/esm/components/data-grid/filters/number-filter.js +65 -0
- package/esm/components/data-grid/filters/number-filter.js.map +1 -0
- package/esm/components/data-grid/filters/number-filter.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/number-filter.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/number-filter.spec.js +142 -0
- package/esm/components/data-grid/filters/number-filter.spec.js.map +1 -0
- package/esm/components/data-grid/filters/string-filter.d.ts +12 -0
- package/esm/components/data-grid/filters/string-filter.d.ts.map +1 -0
- package/esm/components/data-grid/filters/string-filter.js +63 -0
- package/esm/components/data-grid/filters/string-filter.js.map +1 -0
- package/esm/components/data-grid/filters/string-filter.spec.d.ts +2 -0
- package/esm/components/data-grid/filters/string-filter.spec.d.ts.map +1 -0
- package/esm/components/data-grid/filters/string-filter.spec.js +128 -0
- package/esm/components/data-grid/filters/string-filter.spec.js.map +1 -0
- package/esm/components/data-grid/footer.d.ts.map +1 -1
- package/esm/components/data-grid/footer.js +24 -9
- package/esm/components/data-grid/footer.js.map +1 -1
- package/esm/components/data-grid/footer.spec.js +38 -36
- package/esm/components/data-grid/footer.spec.js.map +1 -1
- package/esm/components/data-grid/header.d.ts +6 -9
- package/esm/components/data-grid/header.d.ts.map +1 -1
- package/esm/components/data-grid/header.js +51 -117
- package/esm/components/data-grid/header.js.map +1 -1
- package/esm/components/data-grid/header.spec.js +116 -187
- package/esm/components/data-grid/header.spec.js.map +1 -1
- package/esm/components/data-grid/index.d.ts +1 -0
- package/esm/components/data-grid/index.d.ts.map +1 -1
- package/esm/components/data-grid/index.js +1 -0
- package/esm/components/data-grid/index.js.map +1 -1
- package/esm/components/data-grid/selection-cell.spec.js +8 -8
- package/esm/components/data-grid/selection-cell.spec.js.map +1 -1
- package/esm/components/drawer/drawer-toggle-button.spec.js +22 -22
- package/esm/components/drawer/drawer-toggle-button.spec.js.map +1 -1
- package/esm/components/drawer/index.spec.js +36 -36
- package/esm/components/drawer/index.spec.js.map +1 -1
- package/esm/components/dropdown.spec.js +38 -30
- package/esm/components/dropdown.spec.js.map +1 -1
- package/esm/components/fab.spec.js +4 -4
- package/esm/components/fab.spec.js.map +1 -1
- package/esm/components/form.spec.js +37 -37
- package/esm/components/form.spec.js.map +1 -1
- package/esm/components/grid.spec.js +3 -3
- package/esm/components/grid.spec.js.map +1 -1
- package/esm/components/image.spec.js +55 -52
- package/esm/components/image.spec.js.map +1 -1
- package/esm/components/inputs/autocomplete.spec.js +7 -14
- package/esm/components/inputs/autocomplete.spec.js.map +1 -1
- package/esm/components/inputs/checkbox.spec.js +22 -22
- package/esm/components/inputs/checkbox.spec.js.map +1 -1
- package/esm/components/inputs/input-number.spec.js +47 -47
- package/esm/components/inputs/input-number.spec.js.map +1 -1
- package/esm/components/inputs/input.spec.js +53 -53
- package/esm/components/inputs/input.spec.js.map +1 -1
- package/esm/components/inputs/radio-group.spec.js +14 -14
- package/esm/components/inputs/radio-group.spec.js.map +1 -1
- package/esm/components/inputs/radio.spec.js +16 -16
- package/esm/components/inputs/radio.spec.js.map +1 -1
- package/esm/components/inputs/select.spec.js +74 -74
- package/esm/components/inputs/select.spec.js.map +1 -1
- package/esm/components/inputs/slider.spec.js +16 -16
- package/esm/components/inputs/slider.spec.js.map +1 -1
- package/esm/components/inputs/switch.spec.js +24 -24
- package/esm/components/inputs/switch.spec.js.map +1 -1
- package/esm/components/inputs/text-area.spec.js +17 -17
- package/esm/components/inputs/text-area.spec.js.map +1 -1
- package/esm/components/linear-progress.spec.js +2 -2
- package/esm/components/list/list.spec.js +36 -36
- package/esm/components/list/list.spec.js.map +1 -1
- package/esm/components/markdown/markdown-display.spec.js +15 -15
- package/esm/components/markdown/markdown-display.spec.js.map +1 -1
- package/esm/components/markdown/markdown-editor.spec.js +8 -8
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
- package/esm/components/markdown/markdown-input.spec.js +17 -17
- package/esm/components/markdown/markdown-input.spec.js.map +1 -1
- package/esm/components/menu/menu.spec.js +28 -28
- package/esm/components/menu/menu.spec.js.map +1 -1
- package/esm/components/modal.spec.js +15 -18
- package/esm/components/modal.spec.js.map +1 -1
- package/esm/components/noty-list.spec.js +25 -23
- package/esm/components/noty-list.spec.js.map +1 -1
- package/esm/components/page-container/index.spec.js +16 -16
- package/esm/components/page-container/index.spec.js.map +1 -1
- package/esm/components/page-container/page-header.spec.js +16 -16
- package/esm/components/page-container/page-header.spec.js.map +1 -1
- package/esm/components/page-layout/index.spec.js +29 -29
- package/esm/components/page-layout/index.spec.js.map +1 -1
- package/esm/components/paper.spec.js +3 -3
- package/esm/components/paper.spec.js.map +1 -1
- package/esm/components/rating.spec.js +61 -61
- package/esm/components/rating.spec.js.map +1 -1
- package/esm/components/skeleton.spec.js +10 -6
- package/esm/components/skeleton.spec.js.map +1 -1
- package/esm/components/suggest/suggest-input.spec.js +4 -10
- package/esm/components/suggest/suggest-input.spec.js.map +1 -1
- package/esm/components/tabs.spec.js +30 -30
- package/esm/components/tabs.spec.js.map +1 -1
- package/esm/components/tree/tree.spec.js +27 -27
- package/esm/components/tree/tree.spec.js.map +1 -1
- package/esm/components/typography.spec.js +3 -3
- package/esm/components/typography.spec.js.map +1 -1
- package/esm/components/wizard/index.spec.js +5 -5
- package/esm/components/wizard/index.spec.js.map +1 -1
- package/esm/utils/promisify-animation.d.ts.map +1 -1
- package/esm/utils/promisify-animation.js +3 -0
- package/esm/utils/promisify-animation.js.map +1 -1
- package/package.json +2 -2
- package/src/components/app-bar-link.spec.tsx +16 -19
- package/src/components/app-bar.spec.tsx +6 -4
- package/src/components/avatar.spec.tsx +9 -9
- package/src/components/breadcrumb.spec.tsx +2 -2
- package/src/components/button-group.spec.tsx +155 -11
- package/src/components/button-group.tsx +49 -2
- package/src/components/button.spec.tsx +4 -4
- package/src/components/cache-view.spec.tsx +3 -3
- package/src/components/carousel.spec.tsx +47 -47
- package/src/components/circular-progress.spec.tsx +2 -2
- package/src/components/command-palette/command-palette-input.spec.tsx +23 -19
- package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +27 -27
- package/src/components/command-palette/index.spec.tsx +64 -51
- package/src/components/context-menu/context-menu.spec.tsx +33 -33
- package/src/components/data-grid/body.spec.tsx +13 -13
- package/src/components/data-grid/data-grid-row.spec.tsx +8 -8
- package/src/components/data-grid/data-grid.spec.tsx +106 -28
- package/src/components/data-grid/data-grid.tsx +44 -11
- package/src/components/data-grid/filters/boolean-filter.spec.tsx +142 -0
- package/src/components/data-grid/filters/boolean-filter.tsx +45 -0
- package/src/components/data-grid/filters/date-filter.spec.tsx +181 -0
- package/src/components/data-grid/filters/date-filter.tsx +162 -0
- package/src/components/data-grid/filters/enum-filter.spec.tsx +168 -0
- package/src/components/data-grid/filters/enum-filter.tsx +119 -0
- package/src/components/data-grid/filters/filter-dropdown.spec.tsx +89 -0
- package/src/components/data-grid/filters/filter-dropdown.tsx +60 -0
- package/src/components/data-grid/filters/filter-styles.ts +26 -0
- package/src/components/data-grid/filters/index.ts +6 -0
- package/src/components/data-grid/filters/number-filter.spec.tsx +174 -0
- package/src/components/data-grid/filters/number-filter.tsx +115 -0
- package/src/components/data-grid/filters/string-filter.spec.tsx +157 -0
- package/src/components/data-grid/filters/string-filter.tsx +112 -0
- package/src/components/data-grid/footer.spec.tsx +38 -36
- package/src/components/data-grid/footer.tsx +21 -8
- package/src/components/data-grid/header.spec.tsx +128 -212
- package/src/components/data-grid/header.tsx +95 -183
- package/src/components/data-grid/index.tsx +1 -0
- package/src/components/data-grid/selection-cell.spec.tsx +8 -8
- package/src/components/drawer/drawer-toggle-button.spec.tsx +22 -22
- package/src/components/drawer/index.spec.tsx +36 -36
- package/src/components/dropdown.spec.tsx +38 -30
- package/src/components/fab.spec.tsx +4 -4
- package/src/components/form.spec.tsx +37 -37
- package/src/components/grid.spec.tsx +3 -3
- package/src/components/image.spec.tsx +55 -52
- package/src/components/inputs/autocomplete.spec.tsx +7 -14
- package/src/components/inputs/checkbox.spec.tsx +22 -22
- package/src/components/inputs/input-number.spec.tsx +47 -47
- package/src/components/inputs/input.spec.tsx +53 -53
- package/src/components/inputs/radio-group.spec.tsx +14 -14
- package/src/components/inputs/radio.spec.tsx +16 -16
- package/src/components/inputs/select.spec.tsx +74 -74
- package/src/components/inputs/slider.spec.tsx +16 -16
- package/src/components/inputs/switch.spec.tsx +24 -24
- package/src/components/inputs/text-area.spec.tsx +17 -17
- package/src/components/linear-progress.spec.tsx +2 -2
- package/src/components/list/list.spec.tsx +36 -36
- package/src/components/markdown/markdown-display.spec.tsx +15 -15
- package/src/components/markdown/markdown-editor.spec.tsx +8 -8
- package/src/components/markdown/markdown-input.spec.tsx +17 -17
- package/src/components/menu/menu.spec.tsx +28 -28
- package/src/components/modal.spec.tsx +15 -18
- package/src/components/noty-list.spec.tsx +25 -23
- package/src/components/page-container/index.spec.tsx +16 -16
- package/src/components/page-container/page-header.spec.tsx +16 -16
- package/src/components/page-layout/index.spec.tsx +29 -29
- package/src/components/paper.spec.tsx +3 -3
- package/src/components/rating.spec.tsx +61 -61
- package/src/components/skeleton.spec.tsx +10 -6
- package/src/components/suggest/suggest-input.spec.tsx +4 -10
- package/src/components/tabs.spec.tsx +30 -30
- package/src/components/tree/tree.spec.tsx +27 -27
- package/src/components/typography.spec.tsx +3 -3
- package/src/components/wizard/index.spec.tsx +5 -5
- package/src/utils/promisify-animation.ts +3 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
|
|
3
|
+
import { ObservableValue, usingAsync } from '@furystack/utils'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import type { FilterableFindOptions } from '../data-grid.js'
|
|
6
|
+
import { DateFilter } from './date-filter.js'
|
|
7
|
+
|
|
8
|
+
describe('DateFilter', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
document.body.innerHTML = ''
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const createFindOptions = (options: Partial<FilterableFindOptions> = {}): ObservableValue<FilterableFindOptions> => {
|
|
18
|
+
return new ObservableValue<FilterableFindOptions>(options)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const renderDateFilter = async (
|
|
22
|
+
findOptions: ObservableValue<FilterableFindOptions>,
|
|
23
|
+
field = 'createdAt',
|
|
24
|
+
onClose = vi.fn(),
|
|
25
|
+
) => {
|
|
26
|
+
const injector = new Injector()
|
|
27
|
+
const rootElement = document.getElementById('root')!
|
|
28
|
+
initializeShadeRoot({
|
|
29
|
+
injector,
|
|
30
|
+
rootElement,
|
|
31
|
+
jsxElement: <DateFilter field={field} findOptions={findOptions} onClose={onClose} />,
|
|
32
|
+
})
|
|
33
|
+
await flushUpdates()
|
|
34
|
+
return { injector, onClose }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('should render mode segmented control and date input', async () => {
|
|
38
|
+
const findOptions = createFindOptions()
|
|
39
|
+
await usingAsync((await renderDateFilter(findOptions)).injector, async () => {
|
|
40
|
+
const control = document.querySelector('shade-segmented-control')
|
|
41
|
+
expect(control).not.toBeNull()
|
|
42
|
+
|
|
43
|
+
const input = document.querySelector('[data-testid="date-filter-value"]')
|
|
44
|
+
expect(input).not.toBeNull()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should apply "before" filter on submit', async () => {
|
|
49
|
+
const findOptions = createFindOptions()
|
|
50
|
+
const { injector, onClose } = await renderDateFilter(findOptions)
|
|
51
|
+
await usingAsync(injector, async () => {
|
|
52
|
+
const input = document.querySelector('[data-testid="date-filter-value"]') as HTMLInputElement
|
|
53
|
+
input.value = '2025-06-15T10:30'
|
|
54
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
55
|
+
|
|
56
|
+
const form = document.querySelector('form') as HTMLFormElement
|
|
57
|
+
form.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
58
|
+
await flushUpdates()
|
|
59
|
+
|
|
60
|
+
const filter = findOptions.getValue().filter?.createdAt as Record<string, Date>
|
|
61
|
+
expect(filter.$lt).toBeInstanceOf(Date)
|
|
62
|
+
expect(filter.$lt.toISOString()).toBe(new Date('2025-06-15T10:30').toISOString())
|
|
63
|
+
expect(onClose).toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should apply "after" filter when mode is changed', async () => {
|
|
68
|
+
const findOptions = createFindOptions()
|
|
69
|
+
const { injector, onClose } = await renderDateFilter(findOptions)
|
|
70
|
+
await usingAsync(injector, async () => {
|
|
71
|
+
const afterButton = document.querySelector(
|
|
72
|
+
'shade-segmented-control button[data-value="after"]',
|
|
73
|
+
) as HTMLButtonElement
|
|
74
|
+
afterButton?.click()
|
|
75
|
+
await flushUpdates()
|
|
76
|
+
|
|
77
|
+
const input = document.querySelector('[data-testid="date-filter-value"]') as HTMLInputElement
|
|
78
|
+
input.value = '2025-01-01T00:00'
|
|
79
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
80
|
+
|
|
81
|
+
const form = document.querySelector('form') as HTMLFormElement
|
|
82
|
+
form.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
83
|
+
await flushUpdates()
|
|
84
|
+
|
|
85
|
+
const filter = findOptions.getValue().filter?.createdAt as Record<string, Date>
|
|
86
|
+
expect(filter.$gt).toBeInstanceOf(Date)
|
|
87
|
+
expect(onClose).toHaveBeenCalled()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should apply "between" filter with both dates', async () => {
|
|
92
|
+
const findOptions = createFindOptions()
|
|
93
|
+
const { injector, onClose } = await renderDateFilter(findOptions)
|
|
94
|
+
await usingAsync(injector, async () => {
|
|
95
|
+
const betweenButton = document.querySelector(
|
|
96
|
+
'shade-segmented-control button[data-value="between"]',
|
|
97
|
+
) as HTMLButtonElement
|
|
98
|
+
betweenButton?.click()
|
|
99
|
+
await flushUpdates()
|
|
100
|
+
|
|
101
|
+
const startInput = document.querySelector('[data-testid="date-filter-value"]') as HTMLInputElement
|
|
102
|
+
startInput.value = '2025-01-01T00:00'
|
|
103
|
+
startInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
104
|
+
|
|
105
|
+
const endInput = document.querySelector('[data-testid="date-filter-value-end"]') as HTMLInputElement
|
|
106
|
+
endInput.value = '2025-12-31T23:59'
|
|
107
|
+
endInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
108
|
+
|
|
109
|
+
const form = document.querySelector('form') as HTMLFormElement
|
|
110
|
+
form.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
111
|
+
await flushUpdates()
|
|
112
|
+
|
|
113
|
+
const filter = findOptions.getValue().filter?.createdAt as Record<string, Date>
|
|
114
|
+
expect(filter.$gte).toBeInstanceOf(Date)
|
|
115
|
+
expect(filter.$lte).toBeInstanceOf(Date)
|
|
116
|
+
expect(onClose).toHaveBeenCalled()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should clear filter when Clear button is clicked', async () => {
|
|
121
|
+
const findOptions = createFindOptions({ filter: { createdAt: { $lt: new Date() } } })
|
|
122
|
+
const { injector, onClose } = await renderDateFilter(findOptions)
|
|
123
|
+
await usingAsync(injector, async () => {
|
|
124
|
+
const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
|
|
125
|
+
clearButton?.click()
|
|
126
|
+
await flushUpdates()
|
|
127
|
+
|
|
128
|
+
expect(findOptions.getValue().filter?.createdAt).toBeUndefined()
|
|
129
|
+
expect(onClose).toHaveBeenCalled()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should remove filter when submitting empty date', async () => {
|
|
134
|
+
const findOptions = createFindOptions({ filter: { createdAt: { $lt: new Date() } } })
|
|
135
|
+
const { injector, onClose } = await renderDateFilter(findOptions)
|
|
136
|
+
await usingAsync(injector, async () => {
|
|
137
|
+
const input = document.querySelector('[data-testid="date-filter-value"]') as HTMLInputElement
|
|
138
|
+
input.value = ''
|
|
139
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
140
|
+
|
|
141
|
+
const form = document.querySelector('form') as HTMLFormElement
|
|
142
|
+
form.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
143
|
+
await flushUpdates()
|
|
144
|
+
|
|
145
|
+
expect(findOptions.getValue().filter?.createdAt).toBeUndefined()
|
|
146
|
+
expect(onClose).toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should preserve filters on other fields', async () => {
|
|
151
|
+
const findOptions = createFindOptions({
|
|
152
|
+
filter: { createdAt: { $lt: new Date() }, name: { $regex: 'keep' } },
|
|
153
|
+
})
|
|
154
|
+
const { injector } = await renderDateFilter(findOptions)
|
|
155
|
+
await usingAsync(injector, async () => {
|
|
156
|
+
const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
|
|
157
|
+
clearButton?.click()
|
|
158
|
+
await flushUpdates()
|
|
159
|
+
|
|
160
|
+
const updatedFilter = findOptions.getValue().filter
|
|
161
|
+
expect(updatedFilter?.createdAt).toBeUndefined()
|
|
162
|
+
expect(updatedFilter?.name).toEqual({ $regex: 'keep' })
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should reset skip to 0 when applying filter', async () => {
|
|
167
|
+
const findOptions = createFindOptions({ skip: 20 })
|
|
168
|
+
const { injector } = await renderDateFilter(findOptions)
|
|
169
|
+
await usingAsync(injector, async () => {
|
|
170
|
+
const input = document.querySelector('[data-testid="date-filter-value"]') as HTMLInputElement
|
|
171
|
+
input.value = '2025-06-15T10:30'
|
|
172
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
173
|
+
|
|
174
|
+
const form = document.querySelector('form') as HTMLFormElement
|
|
175
|
+
form.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
176
|
+
await flushUpdates()
|
|
177
|
+
|
|
178
|
+
expect(findOptions.getValue().skip).toBe(0)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createComponent, Shade } from '@furystack/shades'
|
|
2
|
+
import type { ObservableValue } from '@furystack/utils'
|
|
3
|
+
import { SegmentedControl } from '../../button-group.js'
|
|
4
|
+
import { Button } from '../../button.js'
|
|
5
|
+
import { close as closeIcon, search as searchIcon } from '../../icons/icon-definitions.js'
|
|
6
|
+
import { Icon } from '../../icons/icon.js'
|
|
7
|
+
import type { FilterableFindOptions } from '../data-grid.js'
|
|
8
|
+
import { filterBaseCss, filterInputCss } from './filter-styles.js'
|
|
9
|
+
|
|
10
|
+
type DateMode = 'before' | 'after' | 'between'
|
|
11
|
+
|
|
12
|
+
export const DateFilter = Shade<{
|
|
13
|
+
field: string
|
|
14
|
+
findOptions: ObservableValue<FilterableFindOptions>
|
|
15
|
+
onClose: () => void
|
|
16
|
+
}>({
|
|
17
|
+
shadowDomName: 'data-grid-date-filter',
|
|
18
|
+
css: {
|
|
19
|
+
...filterBaseCss,
|
|
20
|
+
'& input[type="datetime-local"]': filterInputCss,
|
|
21
|
+
},
|
|
22
|
+
render: ({ props, useObservable, useState }) => {
|
|
23
|
+
const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions)
|
|
24
|
+
|
|
25
|
+
const currentFilter = findOptions.filter?.[props.field] as
|
|
26
|
+
| { $lt?: Date; $gt?: Date; $gte?: Date; $lte?: Date }
|
|
27
|
+
| undefined
|
|
28
|
+
|
|
29
|
+
const detectMode = (): DateMode => {
|
|
30
|
+
if (currentFilter?.$gte && currentFilter?.$lte) return 'between'
|
|
31
|
+
if (currentFilter?.$lt) return 'before'
|
|
32
|
+
if (currentFilter?.$gt) return 'after'
|
|
33
|
+
return 'before'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const toLocalDateTimeString = (date?: Date) => {
|
|
37
|
+
if (!date) return ''
|
|
38
|
+
const d = date instanceof Date ? date : new Date(date)
|
|
39
|
+
if (isNaN(d.getTime())) return ''
|
|
40
|
+
const pad = (n: number) => n.toString().padStart(2, '0')
|
|
41
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getInitialValue = (): string => {
|
|
45
|
+
if (currentFilter?.$gte) return toLocalDateTimeString(currentFilter.$gte)
|
|
46
|
+
if (currentFilter?.$lt) return toLocalDateTimeString(currentFilter.$lt)
|
|
47
|
+
if (currentFilter?.$gt) return toLocalDateTimeString(currentFilter.$gt)
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getInitialSecondValue = (): string => {
|
|
52
|
+
if (currentFilter?.$lte) return toLocalDateTimeString(currentFilter.$lte)
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [selectedMode, setSelectedMode] = useState<DateMode>('selectedMode', detectMode())
|
|
57
|
+
let dateValue = getInitialValue()
|
|
58
|
+
let secondDateValue = getInitialSecondValue()
|
|
59
|
+
|
|
60
|
+
const applyFilter = () => {
|
|
61
|
+
const filter = { ...findOptions.filter }
|
|
62
|
+
if (!dateValue) {
|
|
63
|
+
delete filter[props.field]
|
|
64
|
+
setFindOptions({ ...findOptions, filter, skip: 0 })
|
|
65
|
+
props.onClose()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let filterValue: Record<string, Date>
|
|
70
|
+
switch (selectedMode) {
|
|
71
|
+
case 'before':
|
|
72
|
+
filterValue = { $lt: new Date(dateValue) }
|
|
73
|
+
break
|
|
74
|
+
case 'after':
|
|
75
|
+
filterValue = { $gt: new Date(dateValue) }
|
|
76
|
+
break
|
|
77
|
+
case 'between':
|
|
78
|
+
filterValue = {
|
|
79
|
+
$gte: new Date(dateValue),
|
|
80
|
+
...(secondDateValue ? { $lte: new Date(secondDateValue) } : {}),
|
|
81
|
+
}
|
|
82
|
+
break
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Invalid date mode: ${selectedMode as unknown as string}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
filter[props.field] = filterValue
|
|
88
|
+
setFindOptions({ ...findOptions, filter, skip: 0 })
|
|
89
|
+
props.onClose()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const clearFilter = () => {
|
|
93
|
+
const filter = { ...findOptions.filter }
|
|
94
|
+
delete filter[props.field]
|
|
95
|
+
setFindOptions({ ...findOptions, filter, skip: 0 })
|
|
96
|
+
props.onClose()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<form
|
|
101
|
+
onsubmit={(ev: Event) => {
|
|
102
|
+
ev.preventDefault()
|
|
103
|
+
applyFilter()
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<div className="filter-row">
|
|
107
|
+
<SegmentedControl
|
|
108
|
+
size="small"
|
|
109
|
+
value={selectedMode}
|
|
110
|
+
onValueChange={(v) => setSelectedMode(v as DateMode)}
|
|
111
|
+
options={[
|
|
112
|
+
{ value: 'before', label: 'Before' },
|
|
113
|
+
{ value: 'after', label: 'After' },
|
|
114
|
+
{ value: 'between', label: 'Between' },
|
|
115
|
+
]}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="filter-row">
|
|
119
|
+
<input
|
|
120
|
+
data-testid="date-filter-value"
|
|
121
|
+
type="datetime-local"
|
|
122
|
+
value={dateValue}
|
|
123
|
+
autofocus
|
|
124
|
+
oninput={(ev: Event) => {
|
|
125
|
+
dateValue = (ev.target as HTMLInputElement).value
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="filter-row" style={{ display: selectedMode === 'between' ? 'flex' : 'none' }}>
|
|
130
|
+
<input
|
|
131
|
+
data-testid="date-filter-value-end"
|
|
132
|
+
type="datetime-local"
|
|
133
|
+
value={secondDateValue}
|
|
134
|
+
oninput={(ev: Event) => {
|
|
135
|
+
secondDateValue = (ev.target as HTMLInputElement).value
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="filter-actions">
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="outlined"
|
|
143
|
+
size="small"
|
|
144
|
+
onclick={clearFilter}
|
|
145
|
+
startIcon={<Icon icon={closeIcon} size={14} />}
|
|
146
|
+
>
|
|
147
|
+
Clear
|
|
148
|
+
</Button>
|
|
149
|
+
<Button
|
|
150
|
+
type="submit"
|
|
151
|
+
variant="outlined"
|
|
152
|
+
size="small"
|
|
153
|
+
color="primary"
|
|
154
|
+
startIcon={<Icon icon={searchIcon} size={14} />}
|
|
155
|
+
>
|
|
156
|
+
Apply
|
|
157
|
+
</Button>
|
|
158
|
+
</div>
|
|
159
|
+
</form>
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
|
|
3
|
+
import { ObservableValue, usingAsync } from '@furystack/utils'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import type { FilterableFindOptions } from '../data-grid.js'
|
|
6
|
+
import { EnumFilter } from './enum-filter.js'
|
|
7
|
+
|
|
8
|
+
const enumValues = [
|
|
9
|
+
{ label: 'Admin', value: 'admin' },
|
|
10
|
+
{ label: 'User', value: 'user' },
|
|
11
|
+
{ label: 'Guest', value: 'guest' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
describe('EnumFilter', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.innerHTML = ''
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const createFindOptions = (options: Partial<FilterableFindOptions> = {}): ObservableValue<FilterableFindOptions> => {
|
|
24
|
+
return new ObservableValue<FilterableFindOptions>(options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const renderEnumFilter = async (
|
|
28
|
+
findOptions: ObservableValue<FilterableFindOptions>,
|
|
29
|
+
field = 'role',
|
|
30
|
+
values = enumValues,
|
|
31
|
+
onClose = vi.fn(),
|
|
32
|
+
) => {
|
|
33
|
+
const injector = new Injector()
|
|
34
|
+
const rootElement = document.getElementById('root')!
|
|
35
|
+
initializeShadeRoot({
|
|
36
|
+
injector,
|
|
37
|
+
rootElement,
|
|
38
|
+
jsxElement: <EnumFilter field={field} values={values} findOptions={findOptions} onClose={onClose} />,
|
|
39
|
+
})
|
|
40
|
+
await flushUpdates()
|
|
41
|
+
return { injector, onClose }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
it('should render mode control and checkboxes for each value', async () => {
|
|
45
|
+
const findOptions = createFindOptions()
|
|
46
|
+
await usingAsync((await renderEnumFilter(findOptions)).injector, async () => {
|
|
47
|
+
const control = document.querySelector('shade-segmented-control')
|
|
48
|
+
expect(control).not.toBeNull()
|
|
49
|
+
|
|
50
|
+
const checkboxes = document.querySelectorAll('shade-checkbox')
|
|
51
|
+
expect(checkboxes.length).toBe(3)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should apply $in filter when values are selected and Apply is clicked', async () => {
|
|
56
|
+
const findOptions = createFindOptions()
|
|
57
|
+
const { injector, onClose } = await renderEnumFilter(findOptions)
|
|
58
|
+
await usingAsync(injector, async () => {
|
|
59
|
+
const checkboxes = document.querySelectorAll('shade-checkbox input[type="checkbox"]')
|
|
60
|
+
const adminCheckbox = checkboxes[0] as HTMLInputElement
|
|
61
|
+
adminCheckbox.checked = true
|
|
62
|
+
adminCheckbox.dispatchEvent(new Event('change', { bubbles: true }))
|
|
63
|
+
|
|
64
|
+
const applyButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Apply')
|
|
65
|
+
applyButton?.click()
|
|
66
|
+
await flushUpdates()
|
|
67
|
+
|
|
68
|
+
expect(findOptions.getValue().filter).toEqual({ role: { $in: ['admin'] } })
|
|
69
|
+
expect(onClose).toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should apply $nin filter when exclude mode is selected', async () => {
|
|
74
|
+
const findOptions = createFindOptions()
|
|
75
|
+
const { injector, onClose } = await renderEnumFilter(findOptions)
|
|
76
|
+
await usingAsync(injector, async () => {
|
|
77
|
+
const excludeButton = document.querySelector(
|
|
78
|
+
'shade-segmented-control button[data-value="exclude"]',
|
|
79
|
+
) as HTMLButtonElement
|
|
80
|
+
excludeButton?.click()
|
|
81
|
+
await flushUpdates()
|
|
82
|
+
|
|
83
|
+
const checkboxes = document.querySelectorAll('shade-checkbox input[type="checkbox"]')
|
|
84
|
+
const guestCheckbox = checkboxes[2] as HTMLInputElement
|
|
85
|
+
guestCheckbox.checked = true
|
|
86
|
+
guestCheckbox.dispatchEvent(new Event('change', { bubbles: true }))
|
|
87
|
+
|
|
88
|
+
const applyButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Apply')
|
|
89
|
+
applyButton?.click()
|
|
90
|
+
await flushUpdates()
|
|
91
|
+
|
|
92
|
+
expect(findOptions.getValue().filter).toEqual({ role: { $nin: ['guest'] } })
|
|
93
|
+
expect(onClose).toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should remove filter when no values are selected', async () => {
|
|
98
|
+
const findOptions = createFindOptions({ filter: { role: { $in: ['admin'] } } })
|
|
99
|
+
const { injector, onClose } = await renderEnumFilter(findOptions)
|
|
100
|
+
await usingAsync(injector, async () => {
|
|
101
|
+
const checkboxes = document.querySelectorAll('shade-checkbox input[type="checkbox"]')
|
|
102
|
+
const adminCheckbox = checkboxes[0] as HTMLInputElement
|
|
103
|
+
adminCheckbox.checked = false
|
|
104
|
+
adminCheckbox.dispatchEvent(new Event('change', { bubbles: true }))
|
|
105
|
+
|
|
106
|
+
const applyButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Apply')
|
|
107
|
+
applyButton?.click()
|
|
108
|
+
await flushUpdates()
|
|
109
|
+
|
|
110
|
+
expect(findOptions.getValue().filter?.role).toBeUndefined()
|
|
111
|
+
expect(onClose).toHaveBeenCalled()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should clear filter when Clear button is clicked', async () => {
|
|
116
|
+
const findOptions = createFindOptions({ filter: { role: { $in: ['admin', 'user'] } } })
|
|
117
|
+
const { injector, onClose } = await renderEnumFilter(findOptions)
|
|
118
|
+
await usingAsync(injector, async () => {
|
|
119
|
+
const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
|
|
120
|
+
clearButton?.click()
|
|
121
|
+
await flushUpdates()
|
|
122
|
+
|
|
123
|
+
expect(findOptions.getValue().filter?.role).toBeUndefined()
|
|
124
|
+
expect(onClose).toHaveBeenCalled()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should preserve filters on other fields', async () => {
|
|
129
|
+
const findOptions = createFindOptions({ filter: { role: { $in: ['admin'] }, name: { $regex: 'keep' } } })
|
|
130
|
+
const { injector } = await renderEnumFilter(findOptions)
|
|
131
|
+
await usingAsync(injector, async () => {
|
|
132
|
+
const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
|
|
133
|
+
clearButton?.click()
|
|
134
|
+
await flushUpdates()
|
|
135
|
+
|
|
136
|
+
const updatedFilter = findOptions.getValue().filter
|
|
137
|
+
expect(updatedFilter?.role).toBeUndefined()
|
|
138
|
+
expect(updatedFilter?.name).toEqual({ $regex: 'keep' })
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should reset skip to 0 when applying filter', async () => {
|
|
143
|
+
const findOptions = createFindOptions({ skip: 20 })
|
|
144
|
+
const { injector } = await renderEnumFilter(findOptions)
|
|
145
|
+
await usingAsync(injector, async () => {
|
|
146
|
+
const checkboxes = document.querySelectorAll('shade-checkbox input[type="checkbox"]')
|
|
147
|
+
const checkbox = checkboxes[0] as HTMLInputElement
|
|
148
|
+
checkbox.checked = true
|
|
149
|
+
checkbox.dispatchEvent(new Event('change', { bubbles: true }))
|
|
150
|
+
|
|
151
|
+
const applyButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Apply')
|
|
152
|
+
applyButton?.click()
|
|
153
|
+
await flushUpdates()
|
|
154
|
+
|
|
155
|
+
expect(findOptions.getValue().skip).toBe(0)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should pre-check existing $in filter values', async () => {
|
|
160
|
+
const findOptions = createFindOptions({ filter: { role: { $in: ['admin', 'guest'] } } })
|
|
161
|
+
await usingAsync((await renderEnumFilter(findOptions)).injector, async () => {
|
|
162
|
+
const checkboxes = document.querySelectorAll('shade-checkbox input[type="checkbox"]')
|
|
163
|
+
expect((checkboxes[0] as HTMLInputElement).checked).toBe(true)
|
|
164
|
+
expect((checkboxes[1] as HTMLInputElement).checked).toBe(false)
|
|
165
|
+
expect((checkboxes[2] as HTMLInputElement).checked).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createComponent, Shade } from '@furystack/shades'
|
|
2
|
+
import type { ObservableValue } from '@furystack/utils'
|
|
3
|
+
import { SegmentedControl } from '../../button-group.js'
|
|
4
|
+
import { Button } from '../../button.js'
|
|
5
|
+
import { close as closeIcon, search as searchIcon } from '../../icons/icon-definitions.js'
|
|
6
|
+
import { Icon } from '../../icons/icon.js'
|
|
7
|
+
import { Checkbox } from '../../inputs/checkbox.js'
|
|
8
|
+
import type { FilterableFindOptions } from '../data-grid.js'
|
|
9
|
+
import { filterBaseCss } from './filter-styles.js'
|
|
10
|
+
|
|
11
|
+
type EnumMode = 'include' | 'exclude'
|
|
12
|
+
|
|
13
|
+
export const EnumFilter = Shade<{
|
|
14
|
+
field: string
|
|
15
|
+
values: Array<{ label: string; value: string }>
|
|
16
|
+
findOptions: ObservableValue<FilterableFindOptions>
|
|
17
|
+
onClose: () => void
|
|
18
|
+
}>({
|
|
19
|
+
shadowDomName: 'data-grid-enum-filter',
|
|
20
|
+
css: {
|
|
21
|
+
...filterBaseCss,
|
|
22
|
+
'& .filter-mode': {
|
|
23
|
+
marginBottom: '8px',
|
|
24
|
+
},
|
|
25
|
+
'& .filter-checkboxes': {
|
|
26
|
+
maxHeight: '200px',
|
|
27
|
+
overflowY: 'auto',
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
gap: '2px',
|
|
31
|
+
marginBottom: '8px',
|
|
32
|
+
},
|
|
33
|
+
'& shade-checkbox': {
|
|
34
|
+
marginBottom: '0',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
render: ({ props, useObservable, useState }) => {
|
|
38
|
+
const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions)
|
|
39
|
+
|
|
40
|
+
const currentFilter = findOptions.filter?.[props.field] as { $in?: string[]; $nin?: string[] } | undefined
|
|
41
|
+
const isExcludeMode = !!currentFilter?.$nin
|
|
42
|
+
const currentSelected = currentFilter?.$in ?? currentFilter?.$nin ?? []
|
|
43
|
+
|
|
44
|
+
const [mode, setMode] = useState<EnumMode>('mode', isExcludeMode ? 'exclude' : 'include')
|
|
45
|
+
const selected = new Set<string>(currentSelected)
|
|
46
|
+
|
|
47
|
+
const applyFilter = () => {
|
|
48
|
+
const filter = { ...findOptions.filter }
|
|
49
|
+
if (selected.size === 0) {
|
|
50
|
+
delete filter[props.field]
|
|
51
|
+
} else {
|
|
52
|
+
const operator = mode === 'include' ? '$in' : '$nin'
|
|
53
|
+
filter[props.field] = { [operator]: Array.from(selected) }
|
|
54
|
+
}
|
|
55
|
+
setFindOptions({ ...findOptions, filter, skip: 0 })
|
|
56
|
+
props.onClose()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const clearFilter = () => {
|
|
60
|
+
const filter = { ...findOptions.filter }
|
|
61
|
+
delete filter[props.field]
|
|
62
|
+
setFindOptions({ ...findOptions, filter, skip: 0 })
|
|
63
|
+
props.onClose()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<div className="filter-mode">
|
|
69
|
+
<SegmentedControl
|
|
70
|
+
size="small"
|
|
71
|
+
value={mode}
|
|
72
|
+
onValueChange={(v) => setMode(v as EnumMode)}
|
|
73
|
+
options={[
|
|
74
|
+
{ value: 'include', label: 'Include' },
|
|
75
|
+
{ value: 'exclude', label: 'Exclude' },
|
|
76
|
+
]}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="filter-checkboxes">
|
|
80
|
+
{props.values.map(({ label, value }) => (
|
|
81
|
+
<Checkbox
|
|
82
|
+
checked={selected.has(value)}
|
|
83
|
+
labelTitle={label}
|
|
84
|
+
onchange={(ev: Event) => {
|
|
85
|
+
const isChecked = (ev.target as HTMLInputElement).checked
|
|
86
|
+
if (isChecked) {
|
|
87
|
+
selected.add(value)
|
|
88
|
+
} else {
|
|
89
|
+
selected.delete(value)
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="filter-actions">
|
|
96
|
+
<Button
|
|
97
|
+
type="button"
|
|
98
|
+
variant="outlined"
|
|
99
|
+
size="small"
|
|
100
|
+
onclick={clearFilter}
|
|
101
|
+
startIcon={<Icon icon={closeIcon} size={14} />}
|
|
102
|
+
>
|
|
103
|
+
Clear
|
|
104
|
+
</Button>
|
|
105
|
+
<Button
|
|
106
|
+
type="button"
|
|
107
|
+
variant="outlined"
|
|
108
|
+
size="small"
|
|
109
|
+
color="primary"
|
|
110
|
+
onclick={applyFilter}
|
|
111
|
+
startIcon={<Icon icon={searchIcon} size={14} />}
|
|
112
|
+
>
|
|
113
|
+
Apply
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
})
|