@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.
Files changed (37) hide show
  1. package/esm/components/data-grid/body.d.ts.map +1 -1
  2. package/esm/components/data-grid/body.js +0 -5
  3. package/esm/components/data-grid/body.js.map +1 -1
  4. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  5. package/esm/components/data-grid/data-grid-row.js +40 -37
  6. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  7. package/esm/components/data-grid/data-grid.d.ts +7 -1
  8. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  9. package/esm/components/data-grid/data-grid.js +7 -6
  10. package/esm/components/data-grid/data-grid.js.map +1 -1
  11. package/esm/components/data-grid/footer.d.ts +3 -0
  12. package/esm/components/data-grid/footer.d.ts.map +1 -1
  13. package/esm/components/data-grid/footer.js +13 -11
  14. package/esm/components/data-grid/footer.js.map +1 -1
  15. package/esm/components/data-grid/header.d.ts +6 -6
  16. package/esm/components/data-grid/header.d.ts.map +1 -1
  17. package/esm/components/data-grid/header.js +39 -33
  18. package/esm/components/data-grid/header.js.map +1 -1
  19. package/esm/services/click-away-service.spec.d.ts +2 -0
  20. package/esm/services/click-away-service.spec.d.ts.map +1 -0
  21. package/esm/services/click-away-service.spec.js +27 -0
  22. package/esm/services/click-away-service.spec.js.map +1 -0
  23. package/esm/services/collection-service.d.ts +2 -21
  24. package/esm/services/collection-service.d.ts.map +1 -1
  25. package/esm/services/collection-service.js +7 -36
  26. package/esm/services/collection-service.js.map +1 -1
  27. package/esm/services/collection-service.spec.js +2 -5
  28. package/esm/services/collection-service.spec.js.map +1 -1
  29. package/package.json +6 -6
  30. package/src/components/data-grid/body.tsx +0 -11
  31. package/src/components/data-grid/data-grid-row.tsx +42 -39
  32. package/src/components/data-grid/data-grid.tsx +25 -13
  33. package/src/components/data-grid/footer.tsx +19 -13
  34. package/src/components/data-grid/header.tsx +59 -45
  35. package/src/services/click-away-service.spec.ts +35 -0
  36. package/src/services/collection-service.spec.ts +14 -20
  37. 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
- 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,22 +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, {
65
- onChange: (currentQueryState) => {
66
- const currentValue = (currentQueryState.filter?.[props.fieldName] as any)?.$regex || ''
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 = (queryState.filter as any)?.[props.fieldName]?.$regex || ''
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, element }) => {
101
+ render: ({ props, useObservable }) => {
103
102
  type SearchSubmitType = { searchValue: string }
104
103
 
105
- const [queryState] = useObservable('currentFilterState', props.service.querySettings, {
106
- onChange: (currentQueryState) => {
107
- const currentValue = (currentQueryState.filter?.[props.fieldName] as any)?.$regex || ''
108
- ;(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
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={(queryState.filter?.[props.fieldName] as any)?.$regex || ''}
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(() => searchForm.querySelector('input')?.focus())
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
- ...currentSettings,
196
+ ...findOptions,
183
197
  filter: {
184
- ...currentSettings.filter,
198
+ ...findOptions.filter,
185
199
  [props.field]: { $regex: value },
186
200
  },
187
201
  }
188
- props.collectionService.querySettings.setValue(newSettings)
202
+ setFindOptions(newSettings)
189
203
  } else {
190
- const { [props.field]: _, ...newFilter } = currentSettings.filter || {}
191
- props.collectionService.querySettings.setValue({ ...currentSettings, filter: newFilter })
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
- service={props.collectionService}
240
+ findOptions={props.findOptions}
227
241
  fieldName={props.field}
228
242
  />
229
243
 
230
- <OrderButton collectionService={props.collectionService} field={props.field} />
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
- 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,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 async handleRowDoubleClick(entry: T) {
169
+ public handleRowDoubleClick(entry: T) {
225
170
  this.options.onRowDoubleClick?.(entry)
226
171
  }
227
172
  }