@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.
Files changed (61) hide show
  1. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  2. package/esm/components/command-palette/command-palette-input.js +26 -23
  3. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  4. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  5. package/esm/components/command-palette/command-palette-suggestion-list.js +33 -29
  6. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  7. package/esm/components/command-palette/index.d.ts.map +1 -1
  8. package/esm/components/command-palette/index.js +48 -44
  9. package/esm/components/command-palette/index.js.map +1 -1
  10. package/esm/components/data-grid/body.d.ts.map +1 -1
  11. package/esm/components/data-grid/body.js +0 -5
  12. package/esm/components/data-grid/body.js.map +1 -1
  13. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  14. package/esm/components/data-grid/data-grid-row.js +14 -7
  15. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  16. package/esm/components/data-grid/data-grid.d.ts +7 -1
  17. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  18. package/esm/components/data-grid/data-grid.js +7 -6
  19. package/esm/components/data-grid/data-grid.js.map +1 -1
  20. package/esm/components/data-grid/footer.d.ts +3 -0
  21. package/esm/components/data-grid/footer.d.ts.map +1 -1
  22. package/esm/components/data-grid/footer.js +13 -11
  23. package/esm/components/data-grid/footer.js.map +1 -1
  24. package/esm/components/data-grid/header.d.ts +6 -6
  25. package/esm/components/data-grid/header.d.ts.map +1 -1
  26. package/esm/components/data-grid/header.js +54 -42
  27. package/esm/components/data-grid/header.js.map +1 -1
  28. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  29. package/esm/components/data-grid/selection-cell.js +5 -3
  30. package/esm/components/data-grid/selection-cell.js.map +1 -1
  31. package/esm/components/inputs/input.js +1 -1
  32. package/esm/components/inputs/input.js.map +1 -1
  33. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  34. package/esm/components/suggest/suggest-input.js +12 -9
  35. package/esm/components/suggest/suggest-input.js.map +1 -1
  36. package/esm/components/suggest/suggest-manager.js +2 -2
  37. package/esm/components/suggest/suggest-manager.js.map +1 -1
  38. package/esm/components/suggest/suggestion-list.js +32 -27
  39. package/esm/components/suggest/suggestion-list.js.map +1 -1
  40. package/esm/services/collection-service.d.ts +2 -21
  41. package/esm/services/collection-service.d.ts.map +1 -1
  42. package/esm/services/collection-service.js +7 -35
  43. package/esm/services/collection-service.js.map +1 -1
  44. package/esm/services/collection-service.spec.js +2 -5
  45. package/esm/services/collection-service.spec.js.map +1 -1
  46. package/package.json +5 -5
  47. package/src/components/command-palette/command-palette-input.tsx +26 -28
  48. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -38
  49. package/src/components/command-palette/index.tsx +55 -51
  50. package/src/components/data-grid/body.tsx +0 -11
  51. package/src/components/data-grid/data-grid-row.tsx +43 -41
  52. package/src/components/data-grid/data-grid.tsx +25 -13
  53. package/src/components/data-grid/footer.tsx +19 -13
  54. package/src/components/data-grid/header.tsx +73 -53
  55. package/src/components/data-grid/selection-cell.tsx +4 -2
  56. package/src/components/inputs/input.tsx +1 -1
  57. package/src/components/suggest/suggest-input.tsx +4 -6
  58. package/src/components/suggest/suggest-manager.ts +2 -2
  59. package/src/components/suggest/suggestion-list.tsx +32 -32
  60. package/src/services/collection-service.spec.ts +14 -20
  61. 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<{ service: CollectionService<any> }>({
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 [currentData] = useObservable('dataUpdater', props.service.data)
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 [currentQuerySettings] = useObservable('querySettings', props.service.querySettings)
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
- const currentQuery = props.service.querySettings.getValue()
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
- props.service.querySettings.setValue({
63
- ...currentQuerySettings,
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
- querySettings: FindOptions<T, any>
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<{ collectionService: CollectionService<any>; field: string }>({
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 [currentQuerySettings, setQuerySettings] = useObservable(
26
- 'currentQuerySettings',
27
- props.collectionService.querySettings,
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
- setQuerySettings({
50
- ...currentQuerySettings,
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<{ service: CollectionService<any>; fieldName: string; onclick: () => void }>({
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, element }) => {
64
- const [queryState] = useObservable('currentFilterState', props.service.querySettings, (currentQueryState) => {
65
- const currentValue = (currentQueryState.filter?.[props.fieldName] as any)?.$regex || ''
66
-
67
- const button = element.querySelector('button') as HTMLInputElement
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 = (queryState.filter as any)?.[props.fieldName]?.$regex || ''
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, element }) => {
101
+ render: ({ props, useObservable }) => {
101
102
  type SearchSubmitType = { searchValue: string }
102
103
 
103
- const [queryState] = useObservable('currentFilterState', props.service.querySettings, (currentQueryState) => {
104
- const currentValue = (currentQueryState.filter?.[props.fieldName] as any)?.$regex || ''
105
- ;(element.querySelector('input') as HTMLInputElement).value = currentValue
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={(queryState.filter?.[props.fieldName] as any)?.$regex || ''}
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), (newValue) => {
161
- const searchForm = element.querySelector('.search-form') as HTMLElement
162
- const headerContent = element.querySelector('.header-content') as HTMLElement
163
- if (!newValue) {
164
- collapse(searchForm)
165
- expand(headerContent)
166
- } else {
167
- searchForm.style.display = 'flex'
168
- expand(searchForm).then(() => searchForm.querySelector('input')?.focus())
169
- collapse(headerContent)
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
- ...currentSettings,
196
+ ...findOptions,
177
197
  filter: {
178
- ...currentSettings.filter,
198
+ ...findOptions.filter,
179
199
  [props.field]: { $regex: value },
180
200
  },
181
201
  }
182
- props.collectionService.querySettings.setValue(newSettings)
202
+ setFindOptions(newSettings)
183
203
  } else {
184
- const { [props.field]: _, ...newFilter } = currentSettings.filter || {}
185
- props.collectionService.querySettings.setValue({ ...currentSettings, filter: newFilter })
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
- service={props.collectionService}
240
+ findOptions={props.findOptions}
221
241
  fieldName={props.field}
222
242
  />
223
243
 
224
- <OrderButton collectionService={props.collectionService} field={props.field} />
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, (newSelection) => {
8
- ;(element.querySelector('input') as HTMLInputElement).checked = newSelection.includes(props.entry)
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
 
@@ -226,7 +226,7 @@ export const Input = Shade<TextInputProps>({
226
226
  validity: element.querySelector('input')?.validity || ({} as ValidityState),
227
227
  element,
228
228
  }),
229
- updateState,
229
+ { onChange: updateState },
230
230
  )
231
231
 
232
232
  return (
@@ -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
- useObservable(
15
- 'isOpened',
16
- props.manager.isOpened,
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
- true,
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
- const [selectedIndex] = useObservable(
17
- 'selectedIndex',
18
- manager.selectedIndex,
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
- true,
31
- )
29
+ })
32
30
 
33
- const [isListOpened] = useObservable('isOpened', manager.isOpened, async (isOpened) => {
34
- const container = element.firstElementChild as HTMLDivElement
35
- if (isOpened) {
36
- container.style.zIndex = '1'
37
- container.style.width = `calc(${Math.round(
38
- element.parentElement?.getBoundingClientRect().width || 200,
39
- )}px - 3em)`
40
- await promisifyAnimation(
41
- container,
42
- [
43
- { opacity: 0, transform: 'translate(0, -50px)' },
44
- { opacity: 1, transform: 'translate(0, 0)' },
45
- ],
46
- { fill: 'forwards', duration: 500 },
47
- )
48
- } else {
49
- await promisifyAnimation(
50
- container,
51
- [
52
- { opacity: 1, transform: 'translate(0, 0)' },
53
- { opacity: 0, transform: 'translate(0, -50px)' },
54
- ],
55
- { fill: 'forwards', duration: 200 },
56
- )
57
- container.style.zIndex = '-1'
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
- new CollectionService({
12
- defaultSettings: {},
13
- loader: async () => ({ count: 3, entries: testEntries }),
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
- collectionService.addToSelection(testEntries[0])
16
+ collectionService.addToSelection(testEntries[0])
22
17
 
23
- expect(collectionService.isSelected(testEntries[0])).toBe(true)
24
- expect(collectionService.isSelected(testEntries[1])).toBe(false)
25
- expect(collectionService.isSelected(testEntries[2])).toBe(false)
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
- collectionService.removeFromSelection(testEntries[0])
22
+ collectionService.removeFromSelection(testEntries[0])
28
23
 
29
- expect(collectionService.isSelected(testEntries[0])).toBe(false)
24
+ expect(collectionService.isSelected(testEntries[0])).toBe(false)
30
25
 
31
- collectionService.toggleSelection(testEntries[1])
32
- expect(collectionService.isSelected(testEntries[1])).toBe(true)
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 { debounce, ObservableValue } from '@furystack/utils'
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.error.dispose()
53
- this.isLoading.dispose()
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 async handleRowDoubleClick(entry: T) {
169
+ public handleRowDoubleClick(entry: T) {
224
170
  this.options.onRowDoubleClick?.(entry)
225
171
  }
226
172
  }