@furystack/shades-common-components 6.0.0 → 8.0.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/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
- package/esm/components/command-palette/command-palette-input.js +26 -23
- package/esm/components/command-palette/command-palette-input.js.map +1 -1
- package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
- package/esm/components/command-palette/command-palette-suggestion-list.js +33 -29
- package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
- package/esm/components/command-palette/index.d.ts.map +1 -1
- package/esm/components/command-palette/index.js +48 -44
- package/esm/components/command-palette/index.js.map +1 -1
- 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 +14 -7
- 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 +54 -42
- package/esm/components/data-grid/header.js.map +1 -1
- package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
- package/esm/components/data-grid/selection-cell.js +5 -3
- package/esm/components/data-grid/selection-cell.js.map +1 -1
- package/esm/components/inputs/input.js +1 -1
- package/esm/components/inputs/input.js.map +1 -1
- package/esm/components/suggest/suggest-input.d.ts.map +1 -1
- package/esm/components/suggest/suggest-input.js +12 -9
- package/esm/components/suggest/suggest-input.js.map +1 -1
- package/esm/components/suggest/suggest-manager.js +2 -2
- package/esm/components/suggest/suggest-manager.js.map +1 -1
- package/esm/components/suggest/suggestion-list.js +32 -27
- package/esm/components/suggest/suggestion-list.js.map +1 -1
- 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 -35
- 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 +5 -5
- package/src/components/command-palette/command-palette-input.tsx +26 -28
- package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -38
- package/src/components/command-palette/index.tsx +55 -51
- package/src/components/data-grid/body.tsx +0 -11
- package/src/components/data-grid/data-grid-row.tsx +43 -41
- 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 +73 -53
- package/src/components/data-grid/selection-cell.tsx +4 -2
- package/src/components/inputs/input.tsx +1 -1
- package/src/components/suggest/suggest-input.tsx +4 -6
- package/src/components/suggest/suggest-manager.ts +2 -2
- package/src/components/suggest/suggestion-list.tsx +32 -32
- package/src/services/collection-service.spec.ts +14 -20
- package/src/services/collection-service.ts +7 -61
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { Shade, createComponent } from '@furystack/shades'
|
|
2
2
|
import { ThemeProviderService } from '../../services/theme-provider-service.js'
|
|
3
3
|
import type { CollectionService } from '../../services/collection-service.js'
|
|
4
|
+
import type { FindOptions } from '@furystack/core'
|
|
5
|
+
import type { ObservableValue } from '@furystack/utils'
|
|
4
6
|
|
|
5
7
|
export const dataGridItemsPerPage = [10, 20, 25, 50, 100, Infinity]
|
|
6
8
|
|
|
7
|
-
export const DataGridFooter = Shade<{
|
|
9
|
+
export const DataGridFooter = Shade<{
|
|
10
|
+
service: CollectionService<any>
|
|
11
|
+
findOptions: ObservableValue<FindOptions<any, any[]>>
|
|
12
|
+
}>({
|
|
8
13
|
shadowDomName: 'shade-data-grid-footer',
|
|
9
14
|
render: ({ props, injector, useObservable }) => {
|
|
10
15
|
const { theme } = injector.getInstance(ThemeProviderService)
|
|
11
16
|
|
|
12
|
-
const
|
|
17
|
+
const { service, findOptions } = props
|
|
18
|
+
const [currentData] = useObservable('dataUpdater', service.data)
|
|
19
|
+
const [currentOptions, setCurrentOptions] = useObservable('optionsUpdater', findOptions, {
|
|
20
|
+
filter: (newValue, oldValue) => {
|
|
21
|
+
return newValue.top !== oldValue.top || newValue.skip !== oldValue.skip
|
|
22
|
+
},
|
|
23
|
+
})
|
|
13
24
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const top = currentQuerySettings.top || Infinity
|
|
17
|
-
const skip = currentQuerySettings.skip || 0
|
|
25
|
+
const top = currentOptions.top || Infinity
|
|
26
|
+
const skip = currentOptions.skip || 0
|
|
18
27
|
const currentPage = Math.ceil(skip) / (top || 1)
|
|
19
28
|
const currentEntriesPerPage = top
|
|
20
29
|
|
|
@@ -39,13 +48,10 @@ export const DataGridFooter = Shade<{ service: CollectionService<any> }>({
|
|
|
39
48
|
style={{ margin: '0 1em' }}
|
|
40
49
|
onchange={(ev) => {
|
|
41
50
|
const value = parseInt((ev.target as any).value, 10)
|
|
42
|
-
|
|
43
|
-
props.service.querySettings.setValue({ ...currentQuery, skip: (currentQuery.top || 0) * value })
|
|
51
|
+
setCurrentOptions({ ...currentOptions, skip: (currentOptions.top || 0) * value })
|
|
44
52
|
}}
|
|
45
53
|
>
|
|
46
|
-
{[
|
|
47
|
-
...new Array(Math.ceil(currentData.count / (props.service.querySettings.getValue().top || Infinity))),
|
|
48
|
-
].map((_val, index) => (
|
|
54
|
+
{[...new Array(Math.ceil(currentData.count / (currentOptions.top || Infinity)))].map((_val, index) => (
|
|
49
55
|
<option value={index.toString()} selected={currentPage === index}>
|
|
50
56
|
{(index + 1).toString()}
|
|
51
57
|
</option>
|
|
@@ -59,8 +65,8 @@ export const DataGridFooter = Shade<{ service: CollectionService<any> }>({
|
|
|
59
65
|
style={{ margin: '0 1em' }}
|
|
60
66
|
onchange={(ev) => {
|
|
61
67
|
const value = parseInt((ev.currentTarget as any).value as string, 10)
|
|
62
|
-
|
|
63
|
-
...
|
|
68
|
+
setCurrentOptions({
|
|
69
|
+
...currentOptions,
|
|
64
70
|
top: value,
|
|
65
71
|
skip: currentPage * value,
|
|
66
72
|
})
|
|
@@ -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,20 +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
|
-
button.innerHTML = currentValue ? '🔍' : '🔎'
|
|
69
|
-
button.style.textShadow = currentValue
|
|
70
|
-
? '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)'
|
|
71
|
-
: 'none'
|
|
67
|
+
render: ({ props, useObservable }) => {
|
|
68
|
+
const [findOptions] = useObservable('currentValue', props.findOptions, {
|
|
69
|
+
filter: (newValue) => {
|
|
70
|
+
return !!newValue.filter?.[props.fieldName]
|
|
71
|
+
},
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
const filterValue =
|
|
74
|
+
const filterValue =
|
|
75
|
+
(findOptions.filter?.[props.fieldName] as FilterType<{ [K in typeof props.fieldName]: string }>)?.$regex || ''
|
|
75
76
|
|
|
76
77
|
return (
|
|
77
78
|
<Button
|
|
@@ -93,18 +94,23 @@ const SearchButton = Shade<{ service: CollectionService<any>; fieldName: string;
|
|
|
93
94
|
const SearchForm = Shade<{
|
|
94
95
|
onSubmit: (newValue: string) => void
|
|
95
96
|
onClear: () => void
|
|
96
|
-
service: CollectionService<any>
|
|
97
97
|
fieldName: string
|
|
98
|
+
findOptions: ObservableValue<FindOptions<any, any[]>>
|
|
98
99
|
}>({
|
|
99
100
|
shadowDomName: 'data-grid-search-form',
|
|
100
|
-
render: ({ props, useObservable
|
|
101
|
+
render: ({ props, useObservable }) => {
|
|
101
102
|
type SearchSubmitType = { searchValue: string }
|
|
102
103
|
|
|
103
|
-
const [
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
},
|
|
106
110
|
})
|
|
107
111
|
|
|
112
|
+
const currentValue = (findOptions.filter?.[props.fieldName] as any)?.$regex || ''
|
|
113
|
+
|
|
108
114
|
return (
|
|
109
115
|
<Form<SearchSubmitType>
|
|
110
116
|
className="search-form"
|
|
@@ -127,7 +133,7 @@ const SearchForm = Shade<{
|
|
|
127
133
|
autofocus
|
|
128
134
|
labelTitle={`${props.fieldName}`}
|
|
129
135
|
name="searchValue"
|
|
130
|
-
value={
|
|
136
|
+
value={currentValue}
|
|
131
137
|
labelProps={{
|
|
132
138
|
style: { padding: '0px 2em' },
|
|
133
139
|
}}
|
|
@@ -136,7 +142,9 @@ const SearchForm = Shade<{
|
|
|
136
142
|
<Button
|
|
137
143
|
type="reset"
|
|
138
144
|
style={{ padding: '4px', margin: '0' }}
|
|
139
|
-
onclick={() => {
|
|
145
|
+
onclick={(ev) => {
|
|
146
|
+
ev.preventDefault()
|
|
147
|
+
ev.stopPropagation()
|
|
140
148
|
props.onClear()
|
|
141
149
|
}}
|
|
142
150
|
>
|
|
@@ -154,35 +162,47 @@ const SearchForm = Shade<{
|
|
|
154
162
|
export const DataGridHeader: <T, K extends keyof T>(
|
|
155
163
|
props: DataGridHeaderProps<T, K>,
|
|
156
164
|
children: ChildrenList,
|
|
165
|
+
findOptions: ObservableValue<FindOptions<T, Array<keyof T>>>,
|
|
157
166
|
) => JSX.Element<any> = Shade<DataGridHeaderProps<any, any>>({
|
|
158
167
|
shadowDomName: 'data-grid-header',
|
|
159
168
|
render: ({ props, element, useObservable }) => {
|
|
160
|
-
const [, setIsSearchOpened] = useObservable('isSearchOpened', new ObservableValue(false),
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const [, setIsSearchOpened] = useObservable('isSearchOpened', new ObservableValue(false), {
|
|
170
|
+
onChange: (newValue) => {
|
|
171
|
+
const searchForm = element.querySelector('.search-form') as HTMLElement
|
|
172
|
+
const headerContent = element.querySelector('.header-content') as HTMLElement
|
|
173
|
+
if (!newValue) {
|
|
174
|
+
collapse(searchForm)
|
|
175
|
+
expand(headerContent)
|
|
176
|
+
} else {
|
|
177
|
+
searchForm.style.display = 'flex'
|
|
178
|
+
expand(searchForm).then(async () => {
|
|
179
|
+
await sleepAsync(100)
|
|
180
|
+
searchForm.querySelector('input')?.focus()
|
|
181
|
+
})
|
|
182
|
+
collapse(headerContent)
|
|
183
|
+
}
|
|
184
|
+
},
|
|
171
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
|
+
|
|
172
193
|
const updateSearchValue = (value?: string) => {
|
|
173
|
-
const currentSettings = props.collectionService.querySettings.getValue()
|
|
174
194
|
if (value) {
|
|
175
195
|
const newSettings: FindOptions<unknown, any> = {
|
|
176
|
-
...
|
|
196
|
+
...findOptions,
|
|
177
197
|
filter: {
|
|
178
|
-
...
|
|
198
|
+
...findOptions.filter,
|
|
179
199
|
[props.field]: { $regex: value },
|
|
180
200
|
},
|
|
181
201
|
}
|
|
182
|
-
|
|
202
|
+
setFindOptions(newSettings)
|
|
183
203
|
} else {
|
|
184
|
-
const { [props.field]: _, ...newFilter } =
|
|
185
|
-
|
|
204
|
+
const { [props.field]: _, ...newFilter } = findOptions.filter || {}
|
|
205
|
+
setFindOptions({ ...findOptions, filter: newFilter })
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
setIsSearchOpened(false)
|
|
@@ -193,8 +213,8 @@ export const DataGridHeader: <T, K extends keyof T>(
|
|
|
193
213
|
<SearchForm
|
|
194
214
|
onSubmit={updateSearchValue}
|
|
195
215
|
onClear={updateSearchValue}
|
|
196
|
-
service={props.collectionService}
|
|
197
216
|
fieldName={props.field}
|
|
217
|
+
findOptions={props.findOptions}
|
|
198
218
|
/>
|
|
199
219
|
<div
|
|
200
220
|
className="header-content"
|
|
@@ -217,11 +237,11 @@ export const DataGridHeader: <T, K extends keyof T>(
|
|
|
217
237
|
onclick={() => {
|
|
218
238
|
setIsSearchOpened(true)
|
|
219
239
|
}}
|
|
220
|
-
|
|
240
|
+
findOptions={props.findOptions}
|
|
221
241
|
fieldName={props.field}
|
|
222
242
|
/>
|
|
223
243
|
|
|
224
|
-
<OrderButton
|
|
244
|
+
<OrderButton field={props.field} findOptions={props.findOptions} />
|
|
225
245
|
</div>
|
|
226
246
|
</div>
|
|
227
247
|
</>
|
|
@@ -4,8 +4,10 @@ import type { CollectionService } from '../../services/collection-service.js'
|
|
|
4
4
|
export const SelectionCell = Shade<{ entry: any; service: CollectionService<any> }>({
|
|
5
5
|
shadowDomName: 'shades-data-grid-selection-cell',
|
|
6
6
|
render: ({ props, useObservable, element }) => {
|
|
7
|
-
const [selection] = useObservable('selection', props.service.selection,
|
|
8
|
-
|
|
7
|
+
const [selection] = useObservable('selection', props.service.selection, {
|
|
8
|
+
onChange: (newSelection) => {
|
|
9
|
+
;(element.querySelector('input') as HTMLInputElement).checked = newSelection.includes(props.entry)
|
|
10
|
+
},
|
|
9
11
|
})
|
|
10
12
|
const isSelected = selection.includes(props.entry)
|
|
11
13
|
|
|
@@ -11,10 +11,9 @@ export const SuggestInput = Shade<{ manager: SuggestManager<any> }>({
|
|
|
11
11
|
render: ({ element, props, useObservable, injector }) => {
|
|
12
12
|
const { theme } = injector.getInstance(ThemeProviderService)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(isOpened) => {
|
|
14
|
+
// todo: getLast is eliminated, do we need it?
|
|
15
|
+
useObservable('isOpened', props.manager.isOpened, {
|
|
16
|
+
onChange: (isOpened) => {
|
|
18
17
|
const input = element.firstChild as HTMLInputElement
|
|
19
18
|
if (isOpened) {
|
|
20
19
|
input.focus()
|
|
@@ -22,8 +21,7 @@ export const SuggestInput = Shade<{ manager: SuggestManager<any> }>({
|
|
|
22
21
|
input.value = ''
|
|
23
22
|
}
|
|
24
23
|
},
|
|
25
|
-
|
|
26
|
-
)
|
|
24
|
+
})
|
|
27
25
|
|
|
28
26
|
return (
|
|
29
27
|
<input
|
|
@@ -30,8 +30,8 @@ export class SuggestManager<T> extends EventHub<'onSelectSuggestion', { onSelect
|
|
|
30
30
|
}).bind(this)
|
|
31
31
|
|
|
32
32
|
public dispose() {
|
|
33
|
-
window.removeEventListener('keyup', this.keyPressListener)
|
|
34
|
-
window.removeEventListener('click', this.clickOutsideListener)
|
|
33
|
+
window.removeEventListener('keyup', this.keyPressListener, true)
|
|
34
|
+
window.removeEventListener('click', this.clickOutsideListener, true)
|
|
35
35
|
this.isOpened.dispose()
|
|
36
36
|
this.isLoading.dispose()
|
|
37
37
|
this.term.dispose()
|
|
@@ -13,10 +13,9 @@ export const SuggestionList: <T>(props: { manager: SuggestManager<T> }, children
|
|
|
13
13
|
|
|
14
14
|
const [suggestions] = useObservable('suggestions', manager.currentSuggestions)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(idx) => {
|
|
16
|
+
// todo: GetLast is eliminated, do we need it?
|
|
17
|
+
const [selectedIndex] = useObservable('selectedIndex', manager.selectedIndex, {
|
|
18
|
+
onChange: (idx) => {
|
|
20
19
|
;([...element.querySelectorAll('.suggestion-item')] as HTMLDivElement[]).map((s, i) => {
|
|
21
20
|
if (i === idx) {
|
|
22
21
|
s.style.background = theme.background.paper
|
|
@@ -27,35 +26,36 @@ export const SuggestionList: <T>(props: { manager: SuggestManager<T> }, children
|
|
|
27
26
|
}
|
|
28
27
|
})
|
|
29
28
|
},
|
|
30
|
-
|
|
31
|
-
)
|
|
29
|
+
})
|
|
32
30
|
|
|
33
|
-
const [isListOpened] = useObservable('isOpened', manager.isOpened,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
31
|
+
const [isListOpened] = useObservable('isOpened', manager.isOpened, {
|
|
32
|
+
onChange: async (isOpened) => {
|
|
33
|
+
const container = element.firstElementChild as HTMLDivElement
|
|
34
|
+
if (isOpened) {
|
|
35
|
+
container.style.zIndex = '1'
|
|
36
|
+
container.style.width = `calc(${Math.round(
|
|
37
|
+
element.parentElement?.getBoundingClientRect().width || 200,
|
|
38
|
+
)}px - 3em)`
|
|
39
|
+
await promisifyAnimation(
|
|
40
|
+
container,
|
|
41
|
+
[
|
|
42
|
+
{ opacity: 0, transform: 'translate(0, -50px)' },
|
|
43
|
+
{ opacity: 1, transform: 'translate(0, 0)' },
|
|
44
|
+
],
|
|
45
|
+
{ fill: 'forwards', duration: 500 },
|
|
46
|
+
)
|
|
47
|
+
} else {
|
|
48
|
+
await promisifyAnimation(
|
|
49
|
+
container,
|
|
50
|
+
[
|
|
51
|
+
{ opacity: 1, transform: 'translate(0, 0)' },
|
|
52
|
+
{ opacity: 0, transform: 'translate(0, -50px)' },
|
|
53
|
+
],
|
|
54
|
+
{ fill: 'forwards', duration: 200 },
|
|
55
|
+
)
|
|
56
|
+
container.style.zIndex = '-1'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
return (
|
|
@@ -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,34 +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), true)
|
|
221
|
-
}
|
|
167
|
+
constructor(private options: CollectionServiceOptions<T> = {}) {}
|
|
222
168
|
|
|
223
|
-
public
|
|
169
|
+
public handleRowDoubleClick(entry: T) {
|
|
224
170
|
this.options.onRowDoubleClick?.(entry)
|
|
225
171
|
}
|
|
226
172
|
}
|