@furystack/shades-common-components 7.0.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/components/data-grid/body.d.ts.map +1 -1
- package/esm/components/data-grid/body.js +0 -5
- package/esm/components/data-grid/body.js.map +1 -1
- package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid-row.js +40 -37
- package/esm/components/data-grid/data-grid-row.js.map +1 -1
- package/esm/components/data-grid/data-grid.d.ts +7 -1
- package/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/esm/components/data-grid/data-grid.js +7 -6
- package/esm/components/data-grid/data-grid.js.map +1 -1
- package/esm/components/data-grid/footer.d.ts +3 -0
- package/esm/components/data-grid/footer.d.ts.map +1 -1
- package/esm/components/data-grid/footer.js +13 -11
- package/esm/components/data-grid/footer.js.map +1 -1
- package/esm/components/data-grid/header.d.ts +6 -6
- package/esm/components/data-grid/header.d.ts.map +1 -1
- package/esm/components/data-grid/header.js +39 -33
- package/esm/components/data-grid/header.js.map +1 -1
- package/esm/services/click-away-service.spec.d.ts +2 -0
- package/esm/services/click-away-service.spec.d.ts.map +1 -0
- package/esm/services/click-away-service.spec.js +27 -0
- package/esm/services/click-away-service.spec.js.map +1 -0
- package/esm/services/collection-service.d.ts +2 -21
- package/esm/services/collection-service.d.ts.map +1 -1
- package/esm/services/collection-service.js +7 -36
- package/esm/services/collection-service.js.map +1 -1
- package/esm/services/collection-service.spec.js +2 -5
- package/esm/services/collection-service.spec.js.map +1 -1
- package/package.json +6 -6
- package/src/components/data-grid/body.tsx +0 -11
- package/src/components/data-grid/data-grid-row.tsx +42 -39
- package/src/components/data-grid/data-grid.tsx +25 -13
- package/src/components/data-grid/footer.tsx +19 -13
- package/src/components/data-grid/header.tsx +59 -45
- package/src/services/click-away-service.spec.ts +35 -0
- package/src/services/collection-service.spec.ts +14 -20
- package/src/services/collection-service.ts +7 -62
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import type { FindOptions } from '@furystack/core'
|
|
1
|
+
import type { FilterType, FindOptions } from '@furystack/core'
|
|
2
2
|
import type { ChildrenList } from '@furystack/shades'
|
|
3
3
|
import { Shade, createComponent } from '@furystack/shades'
|
|
4
|
-
import type { CollectionService } from '../../services/collection-service.js'
|
|
5
4
|
import { Input } from '../inputs/input.js'
|
|
6
5
|
import { Form } from '../form.js'
|
|
7
6
|
import { Button } from '../button.js'
|
|
8
|
-
import { ObservableValue } from '@furystack/utils'
|
|
7
|
+
import { ObservableValue, sleepAsync } from '@furystack/utils'
|
|
9
8
|
import { collapse, expand } from '../animations.js'
|
|
10
9
|
|
|
11
10
|
export interface DataGridHeaderProps<T, K extends keyof T> {
|
|
12
|
-
collectionService: CollectionService<T>
|
|
13
11
|
field: K
|
|
12
|
+
findOptions: ObservableValue<FindOptions<T, K[]>>
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export interface DataGridHeaderState<T> {
|
|
17
|
-
|
|
15
|
+
export interface DataGridHeaderState<T, K extends keyof T> {
|
|
16
|
+
findOptions: FindOptions<T, K[]>
|
|
18
17
|
isSearchOpened: boolean
|
|
19
18
|
updateSearchValue: (value: string) => void
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
export const OrderButton = Shade<{
|
|
21
|
+
export const OrderButton = Shade<{
|
|
22
|
+
field: string
|
|
23
|
+
findOptions: ObservableValue<FindOptions<any, any[]>>
|
|
24
|
+
}>({
|
|
23
25
|
shadowDomName: 'data-grid-order-button',
|
|
24
26
|
render: ({ props, useObservable }) => {
|
|
25
|
-
const [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
const currentOrder = Object.keys(currentQuerySettings.order || {})[0]
|
|
30
|
-
const currentOrderDirection = Object.values(currentQuerySettings.order || {})[0]
|
|
27
|
+
const [findOptions, onFindOptionsChange] = useObservable('findOptions', props.findOptions, {})
|
|
28
|
+
|
|
29
|
+
const currentOrder = Object.keys(findOptions.order || {})[0]
|
|
30
|
+
const currentOrderDirection = Object.values(findOptions.order || {})[0]
|
|
31
31
|
return (
|
|
32
32
|
<Button
|
|
33
33
|
title="Change order"
|
|
@@ -46,8 +46,8 @@ export const OrderButton = Shade<{ collectionService: CollectionService<any>; fi
|
|
|
46
46
|
newDirection = currentOrderDirection === 'ASC' ? 'DESC' : 'ASC'
|
|
47
47
|
}
|
|
48
48
|
newOrder[props.field] = newDirection
|
|
49
|
-
|
|
50
|
-
...
|
|
49
|
+
onFindOptionsChange({
|
|
50
|
+
...findOptions,
|
|
51
51
|
order: newOrder,
|
|
52
52
|
})
|
|
53
53
|
}}
|
|
@@ -58,22 +58,21 @@ export const OrderButton = Shade<{ collectionService: CollectionService<any>; fi
|
|
|
58
58
|
},
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
const SearchButton = Shade<{
|
|
61
|
+
const SearchButton = Shade<{
|
|
62
|
+
fieldName: string
|
|
63
|
+
onclick: () => void
|
|
64
|
+
findOptions: ObservableValue<FindOptions<any, any[]>>
|
|
65
|
+
}>({
|
|
62
66
|
shadowDomName: 'data-grid-search-button',
|
|
63
|
-
render: ({ props, useObservable
|
|
64
|
-
const [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const button = element.querySelector('button') as HTMLInputElement
|
|
69
|
-
button.innerHTML = currentValue ? '🔍' : '🔎'
|
|
70
|
-
button.style.textShadow = currentValue
|
|
71
|
-
? '1px 1px 20px rgba(235,225,45,0.9), 1px 1px 12px rgba(235,225,45,0.9), 0px 0px 3px rgba(255,200,145,0.6)'
|
|
72
|
-
: 'none'
|
|
67
|
+
render: ({ props, useObservable }) => {
|
|
68
|
+
const [findOptions] = useObservable('currentValue', props.findOptions, {
|
|
69
|
+
filter: (newValue) => {
|
|
70
|
+
return !!newValue.filter?.[props.fieldName]
|
|
73
71
|
},
|
|
74
72
|
})
|
|
75
73
|
|
|
76
|
-
const filterValue =
|
|
74
|
+
const filterValue =
|
|
75
|
+
(findOptions.filter?.[props.fieldName] as FilterType<{ [K in typeof props.fieldName]: string }>)?.$regex || ''
|
|
77
76
|
|
|
78
77
|
return (
|
|
79
78
|
<Button
|
|
@@ -95,20 +94,23 @@ const SearchButton = Shade<{ service: CollectionService<any>; fieldName: string;
|
|
|
95
94
|
const SearchForm = Shade<{
|
|
96
95
|
onSubmit: (newValue: string) => void
|
|
97
96
|
onClear: () => void
|
|
98
|
-
service: CollectionService<any>
|
|
99
97
|
fieldName: string
|
|
98
|
+
findOptions: ObservableValue<FindOptions<any, any[]>>
|
|
100
99
|
}>({
|
|
101
100
|
shadowDomName: 'data-grid-search-form',
|
|
102
|
-
render: ({ props, useObservable
|
|
101
|
+
render: ({ props, useObservable }) => {
|
|
103
102
|
type SearchSubmitType = { searchValue: string }
|
|
104
103
|
|
|
105
|
-
const [
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
104
|
+
const [findOptions] = useObservable('currentValue', props.findOptions, {
|
|
105
|
+
filter: (newValue, lastValue) => {
|
|
106
|
+
const newFilter = newValue.filter?.[props.fieldName] as FilterType<{ [K in typeof props.fieldName]: string }>
|
|
107
|
+
const lastFilter = lastValue.filter?.[props.fieldName] as FilterType<{ [K in typeof props.fieldName]: string }>
|
|
108
|
+
return newFilter?.$regex !== lastFilter?.$regex
|
|
109
109
|
},
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
+
const currentValue = (findOptions.filter?.[props.fieldName] as any)?.$regex || ''
|
|
113
|
+
|
|
112
114
|
return (
|
|
113
115
|
<Form<SearchSubmitType>
|
|
114
116
|
className="search-form"
|
|
@@ -131,7 +133,7 @@ const SearchForm = Shade<{
|
|
|
131
133
|
autofocus
|
|
132
134
|
labelTitle={`${props.fieldName}`}
|
|
133
135
|
name="searchValue"
|
|
134
|
-
value={
|
|
136
|
+
value={currentValue}
|
|
135
137
|
labelProps={{
|
|
136
138
|
style: { padding: '0px 2em' },
|
|
137
139
|
}}
|
|
@@ -140,7 +142,9 @@ const SearchForm = Shade<{
|
|
|
140
142
|
<Button
|
|
141
143
|
type="reset"
|
|
142
144
|
style={{ padding: '4px', margin: '0' }}
|
|
143
|
-
onclick={() => {
|
|
145
|
+
onclick={(ev) => {
|
|
146
|
+
ev.preventDefault()
|
|
147
|
+
ev.stopPropagation()
|
|
144
148
|
props.onClear()
|
|
145
149
|
}}
|
|
146
150
|
>
|
|
@@ -158,6 +162,7 @@ const SearchForm = Shade<{
|
|
|
158
162
|
export const DataGridHeader: <T, K extends keyof T>(
|
|
159
163
|
props: DataGridHeaderProps<T, K>,
|
|
160
164
|
children: ChildrenList,
|
|
165
|
+
findOptions: ObservableValue<FindOptions<T, Array<keyof T>>>,
|
|
161
166
|
) => JSX.Element<any> = Shade<DataGridHeaderProps<any, any>>({
|
|
162
167
|
shadowDomName: 'data-grid-header',
|
|
163
168
|
render: ({ props, element, useObservable }) => {
|
|
@@ -170,25 +175,34 @@ export const DataGridHeader: <T, K extends keyof T>(
|
|
|
170
175
|
expand(headerContent)
|
|
171
176
|
} else {
|
|
172
177
|
searchForm.style.display = 'flex'
|
|
173
|
-
expand(searchForm).then(() =>
|
|
178
|
+
expand(searchForm).then(async () => {
|
|
179
|
+
await sleepAsync(100)
|
|
180
|
+
searchForm.querySelector('input')?.focus()
|
|
181
|
+
})
|
|
174
182
|
collapse(headerContent)
|
|
175
183
|
}
|
|
176
184
|
},
|
|
177
185
|
})
|
|
186
|
+
|
|
187
|
+
const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions, {
|
|
188
|
+
filter: (newValue, oldValue) => {
|
|
189
|
+
return newValue.filter?.[props.field] !== oldValue.filter?.[props.field]
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
178
193
|
const updateSearchValue = (value?: string) => {
|
|
179
|
-
const currentSettings = props.collectionService.querySettings.getValue()
|
|
180
194
|
if (value) {
|
|
181
195
|
const newSettings: FindOptions<unknown, any> = {
|
|
182
|
-
...
|
|
196
|
+
...findOptions,
|
|
183
197
|
filter: {
|
|
184
|
-
...
|
|
198
|
+
...findOptions.filter,
|
|
185
199
|
[props.field]: { $regex: value },
|
|
186
200
|
},
|
|
187
201
|
}
|
|
188
|
-
|
|
202
|
+
setFindOptions(newSettings)
|
|
189
203
|
} else {
|
|
190
|
-
const { [props.field]: _, ...newFilter } =
|
|
191
|
-
|
|
204
|
+
const { [props.field]: _, ...newFilter } = findOptions.filter || {}
|
|
205
|
+
setFindOptions({ ...findOptions, filter: newFilter })
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
setIsSearchOpened(false)
|
|
@@ -199,8 +213,8 @@ export const DataGridHeader: <T, K extends keyof T>(
|
|
|
199
213
|
<SearchForm
|
|
200
214
|
onSubmit={updateSearchValue}
|
|
201
215
|
onClear={updateSearchValue}
|
|
202
|
-
service={props.collectionService}
|
|
203
216
|
fieldName={props.field}
|
|
217
|
+
findOptions={props.findOptions}
|
|
204
218
|
/>
|
|
205
219
|
<div
|
|
206
220
|
className="header-content"
|
|
@@ -223,11 +237,11 @@ export const DataGridHeader: <T, K extends keyof T>(
|
|
|
223
237
|
onclick={() => {
|
|
224
238
|
setIsSearchOpened(true)
|
|
225
239
|
}}
|
|
226
|
-
|
|
240
|
+
findOptions={props.findOptions}
|
|
227
241
|
fieldName={props.field}
|
|
228
242
|
/>
|
|
229
243
|
|
|
230
|
-
<OrderButton
|
|
244
|
+
<OrderButton field={props.field} findOptions={props.findOptions} />
|
|
231
245
|
</div>
|
|
232
246
|
</div>
|
|
233
247
|
</>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ClickAwayService } from './click-away-service.js'
|
|
3
|
+
|
|
4
|
+
describe('ClickAwayService', () => {
|
|
5
|
+
it('Should be constructed and disposed', () => {
|
|
6
|
+
const service = new ClickAwayService(document.createElement('div'), () => {})
|
|
7
|
+
service.dispose()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('Should call onClickAway when clicking outside the element', () => {
|
|
11
|
+
const onClickAway = vi.fn()
|
|
12
|
+
const div = document.createElement('div')
|
|
13
|
+
const service = new ClickAwayService(div, onClickAway)
|
|
14
|
+
|
|
15
|
+
document.body.appendChild(div)
|
|
16
|
+
document.body.click()
|
|
17
|
+
|
|
18
|
+
expect(onClickAway).toBeCalled()
|
|
19
|
+
|
|
20
|
+
service.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('Should not call onClickAway when clicking inside the element', () => {
|
|
24
|
+
const onClickAway = vi.fn()
|
|
25
|
+
const div = document.createElement('div')
|
|
26
|
+
const service = new ClickAwayService(div, onClickAway)
|
|
27
|
+
|
|
28
|
+
document.body.appendChild(div)
|
|
29
|
+
div.click()
|
|
30
|
+
|
|
31
|
+
expect(onClickAway).not.toBeCalled()
|
|
32
|
+
|
|
33
|
+
service.dispose()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -7,31 +7,25 @@ const testEntries = [{ foo: 1 }, { foo: 2 }, { foo: 3 }]
|
|
|
7
7
|
describe('CollectionService', () => {
|
|
8
8
|
describe('Selection', () => {
|
|
9
9
|
it('Should add and remove selection', async () => {
|
|
10
|
-
await usingAsync(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
})
|
|
15
|
-
async (collectionService) => {
|
|
16
|
-
await collectionService.getEntries({})
|
|
17
|
-
testEntries.forEach((entry) => {
|
|
18
|
-
expect(collectionService.isSelected(entry)).toBe(false)
|
|
19
|
-
})
|
|
10
|
+
await usingAsync(new CollectionService({}), async (collectionService) => {
|
|
11
|
+
collectionService.data.setValue({ count: 3, entries: testEntries })
|
|
12
|
+
testEntries.forEach((entry) => {
|
|
13
|
+
expect(collectionService.isSelected(entry)).toBe(false)
|
|
14
|
+
})
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
collectionService.addToSelection(testEntries[0])
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
expect(collectionService.isSelected(testEntries[0])).toBe(true)
|
|
19
|
+
expect(collectionService.isSelected(testEntries[1])).toBe(false)
|
|
20
|
+
expect(collectionService.isSelected(testEntries[2])).toBe(false)
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
collectionService.removeFromSelection(testEntries[0])
|
|
28
23
|
|
|
29
|
-
|
|
24
|
+
expect(collectionService.isSelected(testEntries[0])).toBe(false)
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
26
|
+
collectionService.toggleSelection(testEntries[1])
|
|
27
|
+
expect(collectionService.isSelected(testEntries[1])).toBe(true)
|
|
28
|
+
})
|
|
35
29
|
})
|
|
36
30
|
})
|
|
37
31
|
})
|
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
import type { PartialResult, FindOptions } from '@furystack/core'
|
|
2
|
-
import { Lock } from 'semaphore-async-await'
|
|
3
1
|
import type { Disposable } from '@furystack/utils'
|
|
4
|
-
import {
|
|
2
|
+
import { ObservableValue } from '@furystack/utils'
|
|
5
3
|
|
|
6
4
|
export interface CollectionData<T> {
|
|
7
5
|
entries: T[]
|
|
8
6
|
count: number
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
export type EntryLoader<T> = <TFields extends Array<keyof T>>(
|
|
12
|
-
searchOptions: FindOptions<T, TFields>,
|
|
13
|
-
) => Promise<CollectionData<PartialResult<T, TFields>>>
|
|
14
|
-
|
|
15
9
|
export interface CollectionServiceOptions<T> {
|
|
16
|
-
/**
|
|
17
|
-
* A method used to retrieve the entries from the data source
|
|
18
|
-
*/
|
|
19
|
-
loader: EntryLoader<T>
|
|
20
|
-
/**
|
|
21
|
-
* The default filter / top / skip / etc... options
|
|
22
|
-
*/
|
|
23
|
-
defaultSettings: FindOptions<T, Array<keyof T>>
|
|
24
10
|
/**
|
|
25
11
|
* An optional field that can be used for quick search
|
|
26
12
|
*/
|
|
27
13
|
searchField?: keyof T
|
|
28
|
-
|
|
29
14
|
/**
|
|
30
15
|
* @param entry The clicked entry
|
|
31
16
|
* optional callback for row clicks
|
|
@@ -38,19 +23,15 @@ export interface CollectionServiceOptions<T> {
|
|
|
38
23
|
*/
|
|
39
24
|
|
|
40
25
|
onRowDoubleClick?: (entry: T) => void
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* An optional debounce interval in milliseconds
|
|
44
|
-
*/
|
|
45
|
-
debounceMs?: number
|
|
46
26
|
}
|
|
47
27
|
|
|
48
28
|
export class CollectionService<T> implements Disposable {
|
|
49
29
|
public dispose() {
|
|
50
|
-
this.querySettings.dispose()
|
|
51
30
|
this.data.dispose()
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
31
|
+
this.selection.dispose()
|
|
32
|
+
this.searchTerm.dispose()
|
|
33
|
+
this.hasFocus.dispose()
|
|
34
|
+
this.focusedEntry.dispose()
|
|
54
35
|
}
|
|
55
36
|
|
|
56
37
|
public isSelected = (entry: T) => this.selection.getValue().includes(entry)
|
|
@@ -67,18 +48,8 @@ export class CollectionService<T> implements Disposable {
|
|
|
67
48
|
this.isSelected(entry) ? this.removeFromSelection(entry) : this.addToSelection(entry)
|
|
68
49
|
}
|
|
69
50
|
|
|
70
|
-
private readonly loadLock = new Lock()
|
|
71
|
-
|
|
72
|
-
public getEntries: EntryLoader<T>
|
|
73
|
-
|
|
74
51
|
public data = new ObservableValue<CollectionData<T>>({ count: 0, entries: [] })
|
|
75
52
|
|
|
76
|
-
public error = new ObservableValue<unknown | undefined>(undefined)
|
|
77
|
-
|
|
78
|
-
public isLoading = new ObservableValue<boolean>(false)
|
|
79
|
-
|
|
80
|
-
public querySettings: ObservableValue<FindOptions<T, Array<keyof T>>>
|
|
81
|
-
|
|
82
53
|
public focusedEntry = new ObservableValue<T | undefined>(undefined)
|
|
83
54
|
|
|
84
55
|
public selection = new ObservableValue<T[]>([])
|
|
@@ -193,35 +164,9 @@ export class CollectionService<T> implements Disposable {
|
|
|
193
164
|
this.focusedEntry.setValue(entry)
|
|
194
165
|
}
|
|
195
166
|
|
|
196
|
-
constructor(private options: CollectionServiceOptions<T>) {
|
|
197
|
-
this.querySettings = new ObservableValue<FindOptions<T, Array<keyof T>>>(this.options.defaultSettings)
|
|
198
|
-
|
|
199
|
-
const loader = this.options.debounceMs
|
|
200
|
-
? debounce(this.options.loader, this.options.debounceMs)
|
|
201
|
-
: this.options.loader
|
|
202
|
-
|
|
203
|
-
this.getEntries = async (opt) => {
|
|
204
|
-
await this.loadLock.acquire()
|
|
205
|
-
try {
|
|
206
|
-
this.error.setValue(undefined)
|
|
207
|
-
this.isLoading.setValue(true)
|
|
208
|
-
const result = await loader(opt)
|
|
209
|
-
this.data.setValue(result)
|
|
210
|
-
return result
|
|
211
|
-
} catch (error) {
|
|
212
|
-
this.error.setValue(error)
|
|
213
|
-
throw error
|
|
214
|
-
} finally {
|
|
215
|
-
this.loadLock.release()
|
|
216
|
-
this.isLoading.setValue(false)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
this.querySettings.subscribe((val) => this.getEntries(val))
|
|
221
|
-
this.getEntries(this.querySettings.getValue())
|
|
222
|
-
}
|
|
167
|
+
constructor(private options: CollectionServiceOptions<T> = {}) {}
|
|
223
168
|
|
|
224
|
-
public
|
|
169
|
+
public handleRowDoubleClick(entry: T) {
|
|
225
170
|
this.options.onRowDoubleClick?.(entry)
|
|
226
171
|
}
|
|
227
172
|
}
|