@operato/data-grist 10.13.0 → 10.15.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 (60) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/coverage/lcov-report/index.html +215 -200
  3. package/coverage/lcov-report/webpack/bootstrap.html +1 -1
  4. package/coverage/lcov-report/webpack/index.html +1 -1
  5. package/coverage/lcov-report/webpack/runtime/define property getters.html +1 -1
  6. package/coverage/lcov-report/webpack/runtime/global.html +1 -1
  7. package/coverage/lcov-report/webpack/runtime/hasOwnProperty shorthand.html +1 -1
  8. package/coverage/lcov-report/webpack/runtime/index.html +1 -1
  9. package/coverage/lcov-report/webpack/runtime/make namespace object.html +1 -1
  10. package/coverage/lcov.info +31372 -29234
  11. package/dist/src/data-card/data-card.d.ts +4 -6
  12. package/dist/src/data-card/data-card.js +5 -10
  13. package/dist/src/data-card/data-card.js.map +1 -1
  14. package/dist/src/data-consumer.d.ts +4 -0
  15. package/dist/src/data-consumer.js.map +1 -1
  16. package/dist/src/data-grid/data-grid-field.js +5 -4
  17. package/dist/src/data-grid/data-grid-field.js.map +1 -1
  18. package/dist/src/data-grid/data-grid.d.ts +4 -6
  19. package/dist/src/data-grid/data-grid.js +6 -10
  20. package/dist/src/data-grid/data-grid.js.map +1 -1
  21. package/dist/src/data-grist.d.ts +9 -0
  22. package/dist/src/data-grist.js +38 -6
  23. package/dist/src/data-grist.js.map +1 -1
  24. package/dist/src/data-list/data-list.d.ts +4 -5
  25. package/dist/src/data-list/data-list.js +5 -9
  26. package/dist/src/data-list/data-list.js.map +1 -1
  27. package/dist/src/data-provider.js +25 -8
  28. package/dist/src/data-provider.js.map +1 -1
  29. package/dist/src/data-report.d.ts +1 -0
  30. package/dist/src/data-report.js +2 -1
  31. package/dist/src/data-report.js.map +1 -1
  32. package/dist/src/empty-note-render.d.ts +13 -0
  33. package/dist/src/empty-note-render.js +14 -0
  34. package/dist/src/empty-note-render.js.map +1 -0
  35. package/dist/src/empty-note.d.ts +1 -9
  36. package/dist/src/empty-note.js +5 -52
  37. package/dist/src/empty-note.js.map +1 -1
  38. package/dist/src/filters/filter-styles.js +10 -0
  39. package/dist/src/filters/filter-styles.js.map +1 -1
  40. package/dist/src/filters/filters-form.js +2 -1
  41. package/dist/src/filters/filters-form.js.map +1 -1
  42. package/dist/test/accumulator-labels.test.d.ts +1 -0
  43. package/dist/test/accumulator-labels.test.js +32 -0
  44. package/dist/test/accumulator-labels.test.js.map +1 -0
  45. package/dist/test/control-heights.test.d.ts +1 -0
  46. package/dist/test/control-heights.test.js +25 -0
  47. package/dist/test/control-heights.test.js.map +1 -0
  48. package/dist/test/fetch-failure-state.test.d.ts +1 -0
  49. package/dist/test/fetch-failure-state.test.js +111 -0
  50. package/dist/test/fetch-failure-state.test.js.map +1 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +8 -7
  53. package/test/accumulator-labels.test.ts +36 -0
  54. package/test/control-heights.test.ts +31 -0
  55. package/test/fetch-failure-state.test.ts +135 -0
  56. package/translations/en.json +3 -6
  57. package/translations/ja.json +3 -6
  58. package/translations/ko.json +4 -7
  59. package/translations/ms.json +3 -6
  60. package/translations/zh.json +3 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/data-grist",
3
- "version": "10.13.0",
3
+ "version": "10.15.0",
4
4
  "description": "User interface for grid (desktop) and list (mobile)",
5
5
  "author": "heartyoh",
6
6
  "type": "module",
@@ -70,13 +70,14 @@
70
70
  "@material/web": "^2.0.0",
71
71
  "@operato/headroom": "^10.0.0",
72
72
  "@operato/i18n": "^10.11.0",
73
- "@operato/input": "^10.12.0",
74
- "@operato/p13n": "^10.13.0",
75
- "@operato/popup": "^10.11.0",
73
+ "@operato/input": "^10.15.0",
74
+ "@operato/list-state": "^10.15.0",
75
+ "@operato/p13n": "^10.15.0",
76
+ "@operato/popup": "^10.15.0",
76
77
  "@operato/pull-to-refresh": "^10.0.0",
77
- "@operato/styles": "^10.10.0",
78
+ "@operato/styles": "^10.15.0",
78
79
  "@operato/time-calculator": "^10.0.0",
79
- "@operato/utils": "^10.11.0",
80
+ "@operato/utils": "^10.15.0",
80
81
  "i18next": "^24.2.1",
81
82
  "json5": "^2.2.0",
82
83
  "lit": "^3.1.2",
@@ -115,5 +116,5 @@
115
116
  "prettier --write"
116
117
  ]
117
118
  },
118
- "gitHead": "74b991d2b27696070fe8d54b24bd4c30ea0a2ef7"
119
+ "gitHead": "62ac2a23866e7ea579786c4433ef356888dfded8"
119
120
  }
@@ -0,0 +1,36 @@
1
+ import { expect } from '@open-wc/testing'
2
+
3
+ /**
4
+ * The summary row tags a column with its accumulator's name - `label.accumulator_${type}`.
5
+ *
6
+ * max and min were swapped in all five locales: a column summed with `max` (Math.max) was tagged
7
+ * "min" / 「최소」. The words are written out here per locale, so a swap in any one of them fails.
8
+ */
9
+
10
+ const EXPECTED: Record<string, { max: string; min: string }> = {
11
+ en: { max: 'max', min: 'min' },
12
+ ko: { max: '최대', min: '최소' },
13
+ ja: { max: '最大', min: '最小' },
14
+ zh: { max: '最大', min: '最小' },
15
+ ms: { max: 'Maksimum', min: 'Minimum' }
16
+ }
17
+
18
+ async function translations(locale: string): Promise<Record<string, string>> {
19
+ const response = await fetch(new URL(`../../translations/${locale}.json`, import.meta.url))
20
+ expect(response.ok, `${locale}.json is served`).to.equal(true)
21
+ return response.json()
22
+ }
23
+
24
+ describe('accumulator labels', () => {
25
+ for (const [locale, words] of Object.entries(EXPECTED)) {
26
+ it(`${locale}: max is the largest, min the smallest`, async () => {
27
+ const t = await translations(locale)
28
+ expect(t['label.accumulator_max']).to.equal(words.max)
29
+ expect(t['label.accumulator_min']).to.equal(words.min)
30
+ })
31
+ }
32
+
33
+ it('ko: count is 개수', async () => {
34
+ expect((await translations('ko'))['label.accumulator_count']).to.equal('개수')
35
+ })
36
+ })
@@ -0,0 +1,31 @@
1
+ import { expect } from '@open-wc/testing'
2
+
3
+ import { DataGridField } from '../src/data-grid/data-grid-field.js'
4
+ import { FilterStyles } from '../src/filters/filter-styles.js'
5
+
6
+ /**
7
+ * A button inside a grid cell is --cell-action-height (26px by default, ADR-0060 decision 6), for its
8
+ * width as well: a second token for the width could say a different number. It still takes no more
9
+ * than a line of text in the row, so one cell with a button does not make every row taller.
10
+ */
11
+
12
+ const text = (styles: unknown) =>
13
+ (Array.isArray(styles) ? styles : [styles]).map(s => String((s as { cssText?: string }).cssText ?? s)).join('\n')
14
+
15
+ describe('cell action height', () => {
16
+ it('the icon button in a cell reads the cell token for height and width', () => {
17
+ const css = text(DataGridField.styles)
18
+ const rule = css.slice(css.indexOf(':host > button.icon {'))
19
+
20
+ expect(rule).to.match(/^:host > button\.icon \{[^}]*width: var\(--cell-action-height, 26px\);/)
21
+ expect(rule).to.match(/^:host > button\.icon \{[^}]*height: var\(--cell-action-height, 26px\);/)
22
+ expect(rule).to.contain('margin-top: calc((20px - var(--cell-action-height, 26px)) / 2)')
23
+ })
24
+ })
25
+
26
+ describe('filter bar input height', () => {
27
+ it('a grid filter bar is a dense row, so its inputs take the small input height', () => {
28
+ const css = text(FilterStyles)
29
+ expect(css).to.match(/box-sizing: border-box;\s*height: var\(--form-element-height-small, 26px\);/)
30
+ })
31
+ })
@@ -0,0 +1,135 @@
1
+ /* Elements are compared by presence, not by value: a failing element comparison makes chai render the DOM and hang the runner. */
2
+ import { expect, fixture } from '@open-wc/testing'
3
+ import { html } from 'lit'
4
+
5
+ import '../src/data-grist.js'
6
+
7
+ /**
8
+ * A failed fetch is not an empty list.
9
+ *
10
+ * The provider wrapped the fetch in try/finally with no catch, so a thrown fetch hid the spinner and
11
+ * nothing else: with no rows the grid said "nothing to show", with rows from before it kept them as if
12
+ * they were the answer. Both are a confident wrong statement about data the grid never saw.
13
+ */
14
+
15
+ type Rec = { id: string; name: string }
16
+
17
+ const config = {
18
+ columns: [
19
+ { type: 'string', name: 'id', hidden: true },
20
+ { type: 'string', name: 'name', header: 'name' }
21
+ ],
22
+ pagination: { pages: [20] }
23
+ }
24
+
25
+ const settle = (ms = 150) => new Promise(resolve => setTimeout(resolve, ms))
26
+
27
+ async function grist(mode: 'GRID' | 'LIST' | 'CARD', handler: () => Promise<unknown>) {
28
+ const el = (await fixture(html`
29
+ <ox-grist style="height: 300px" mode=${mode} .config=${config} .fetchHandler=${handler}></ox-grist>
30
+ `)) as HTMLElement & { fetch(): Promise<void>; updateComplete: Promise<unknown>; renderRoot: ShadowRoot }
31
+ await el.fetch().catch(() => {})
32
+ await settle()
33
+ await el.updateComplete
34
+ return el
35
+ }
36
+
37
+ function emptyNote(el: HTMLElement & { renderRoot: ShadowRoot }): HTMLElement | null {
38
+ const body = el.renderRoot.querySelector('#grist') as (HTMLElement & { renderRoot: ShadowRoot }) | null
39
+ const inBody = body?.renderRoot.querySelector('ox-empty-note') as HTMLElement | null
40
+ if (inBody) return inBody
41
+ /* GRID draws the note inside its body element. */
42
+ const gridBody = body?.renderRoot.querySelector('ox-grid-body') as HTMLElement | null
43
+ return (gridBody?.querySelector('ox-empty-note') as HTMLElement | null) ?? null
44
+ }
45
+
46
+ describe('ox-grist when the fetch fails', () => {
47
+ /*
48
+ * Resizing the grid body inside a fixture trips Chrome's benign "ResizeObserver loop completed"
49
+ * error, which mocha reports as a failure. It says nothing about fetch failures; keep it out.
50
+ */
51
+ const ignoreResizeLoop = (e: ErrorEvent) => {
52
+ if (String(e.message).includes('ResizeObserver loop')) {
53
+ e.stopImmediatePropagation()
54
+ e.preventDefault()
55
+ }
56
+ }
57
+ let previousOnError: OnErrorEventHandler = null
58
+ before(() => {
59
+ window.addEventListener('error', ignoreResizeLoop, true)
60
+ previousOnError = window.onerror
61
+ window.onerror = (message, ...rest) =>
62
+ String(message).includes('ResizeObserver loop') ? true : previousOnError?.call(window, message, ...rest)
63
+ })
64
+ after(() => {
65
+ window.removeEventListener('error', ignoreResizeLoop, true)
66
+ window.onerror = previousOnError
67
+ })
68
+
69
+ for (const mode of ['GRID', 'LIST', 'CARD'] as const) {
70
+ it(`${mode}: says it could not load, not that there is nothing, and retries`, async () => {
71
+ let calls = 0
72
+ const el = await grist(mode, async () => {
73
+ calls++
74
+ throw new Error('server down')
75
+ })
76
+
77
+ const note = emptyNote(el)
78
+ expect(note, 'a note is drawn').to.exist
79
+ expect(note!.getAttribute('role')).to.equal('alert')
80
+ expect(note!.getAttribute('title')).to.equal('could not load the list')
81
+
82
+ const retry = note!.querySelector('md-text-button') as HTMLElement
83
+ expect(retry, 'retry').to.exist
84
+ const before = calls
85
+ retry.click()
86
+ await settle()
87
+ expect(calls).to.equal(before + 1)
88
+ })
89
+ }
90
+
91
+ it('an empty answer still says empty, without an alert', async () => {
92
+ const el = await grist('GRID', async () => ({ total: 0, records: [] as Rec[] }))
93
+
94
+ const note = emptyNote(el)
95
+ expect(note, 'a note is drawn').to.exist
96
+ expect(note!.hasAttribute('role')).to.equal(false)
97
+ expect(note!.getAttribute('title')).to.equal('nothing here yet.')
98
+ })
99
+
100
+ it('rows from before stay, with a band saying they are not a fresh result; a good fetch clears it', async () => {
101
+ let fail = false
102
+ const el = await grist('GRID', async () => {
103
+ if (fail) throw new Error('server down')
104
+ return { total: 1, records: [{ id: '1', name: 'first' }] }
105
+ })
106
+
107
+ expect(el.renderRoot.querySelector('ox-list-state') === null, 'absent').to.equal(true)
108
+
109
+ fail = true
110
+ await el.fetch().catch(() => {})
111
+ await settle()
112
+ await el.updateComplete
113
+
114
+ const state = el.renderRoot.querySelector('ox-list-state')!
115
+ expect(state, 'failure band').to.exist
116
+ await state.updateComplete
117
+ expect(state.renderRoot.querySelector('[role="alert"]')?.textContent).to.contain('could not refresh')
118
+
119
+ fail = false
120
+ await el.fetch()
121
+ await settle()
122
+ await el.updateComplete
123
+ expect(el.renderRoot.querySelector('ox-list-state') === null, 'absent').to.equal(true)
124
+ })
125
+
126
+ it('the error still reaches whoever awaited the fetch', async () => {
127
+ const el = (await fixture(html`
128
+ <ox-grist mode="GRID" .config=${config} .fetchHandler=${async () => { throw new Error('server down') }}></ox-grist>
129
+ `)) as HTMLElement & { fetch(): Promise<void> }
130
+
131
+ let caught: unknown
132
+ await el.fetch().catch(e => (caught = e))
133
+ expect((caught as Error)?.message).to.equal('server down')
134
+ })
135
+ })
@@ -3,19 +3,16 @@
3
3
  "button.save": "save",
4
4
  "label.accumulator_avg": "avg",
5
5
  "label.accumulator_count": "cnt",
6
- "label.accumulator_max": "min",
7
- "label.accumulator_min": "max",
6
+ "label.accumulator_max": "max",
7
+ "label.accumulator_min": "min",
8
8
  "label.accumulator_sum": "sum",
9
9
  "label.filter": "filter",
10
10
  "text.grid-cell-input": "{name} input",
11
11
  "text.grid-cell-input-plain": "cell input",
12
- "text.grid-clear-search": "clear the search or filters to see everything",
13
12
  "text.grid-filter-from": "{name} filter, from",
14
13
  "text.grid-filter-of": "{name} filter",
15
14
  "text.grid-filter-to": "{name} filter, to",
16
- "text.grid-no-records": "nothing to show",
17
- "text.grid-nothing-found": "nothing found",
18
- "text.grid-open-editor": "press to edit",
15
+ "text.grid-open-editor": "select to edit",
19
16
  "text.grid-select-all-records": "select all rows",
20
17
  "text.grid-select-record": "select row {row}",
21
18
  "text.grid-total-records": "{total} in total",
@@ -3,19 +3,16 @@
3
3
  "button.save": "保存",
4
4
  "label.accumulator_avg": "平均",
5
5
  "label.accumulator_count": "カウント",
6
- "label.accumulator_max": "最小",
7
- "label.accumulator_min": "最大",
6
+ "label.accumulator_max": "最大",
7
+ "label.accumulator_min": "最小",
8
8
  "label.accumulator_sum": "合計",
9
9
  "label.filter": "フィルター",
10
10
  "text.grid-cell-input": "{name} 入力",
11
11
  "text.grid-cell-input-plain": "セル入力",
12
- "text.grid-clear-search": "検索語やフィルターを消すと全体が表示されます",
13
12
  "text.grid-filter-from": "{name} フィルター 開始値",
14
13
  "text.grid-filter-of": "{name} フィルター",
15
14
  "text.grid-filter-to": "{name} フィルター 終了値",
16
- "text.grid-no-records": "まだ何もありません",
17
- "text.grid-nothing-found": "見つかりませんでした",
18
- "text.grid-open-editor": "押して編集します",
15
+ "text.grid-open-editor": "選択して編集",
19
16
  "text.grid-select-all-records": "すべての行を選択",
20
17
  "text.grid-select-record": "{row} 行目を選択",
21
18
  "text.grid-total-records": "全 {total} 件",
@@ -2,20 +2,17 @@
2
2
  "button.delete": "삭제",
3
3
  "button.save": "저장",
4
4
  "label.accumulator_avg": "평균",
5
- "label.accumulator_count": "계수",
6
- "label.accumulator_max": "최소",
7
- "label.accumulator_min": "최대",
5
+ "label.accumulator_count": "개수",
6
+ "label.accumulator_max": "최대",
7
+ "label.accumulator_min": "최소",
8
8
  "label.accumulator_sum": "합계",
9
9
  "label.filter": "필터",
10
10
  "text.grid-cell-input": "{name} 입력",
11
11
  "text.grid-cell-input-plain": "칸 입력",
12
- "text.grid-clear-search": "검색어나 필터를 지우면 전체가 보입니다",
13
12
  "text.grid-filter-from": "{name} 필터 시작값",
14
13
  "text.grid-filter-of": "{name} 필터",
15
14
  "text.grid-filter-to": "{name} 필터 끝값",
16
- "text.grid-no-records": "아직 아무것도 없습니다",
17
- "text.grid-nothing-found": "찾는 것이 없습니다",
18
- "text.grid-open-editor": "눌러서 고칩니다",
15
+ "text.grid-open-editor": "선택해 편집",
19
16
  "text.grid-select-all-records": "모든 행 선택",
20
17
  "text.grid-select-record": "{row}번째 행 선택",
21
18
  "text.grid-total-records": "모두 {total}건",
@@ -3,19 +3,16 @@
3
3
  "button.save": "simpan",
4
4
  "label.accumulator_avg": "Purata",
5
5
  "label.accumulator_count": "Kiraan",
6
- "label.accumulator_max": "Minimum",
7
- "label.accumulator_min": "Maksimum",
6
+ "label.accumulator_max": "Maksimum",
7
+ "label.accumulator_min": "Minimum",
8
8
  "label.accumulator_sum": "Jumlah",
9
9
  "label.filter": "penapis",
10
10
  "text.grid-cell-input": "input {name}",
11
11
  "text.grid-cell-input-plain": "input sel",
12
- "text.grid-clear-search": "kosongkan carian atau penapis untuk melihat semua",
13
12
  "text.grid-filter-from": "penapis {name}, dari",
14
13
  "text.grid-filter-of": "penapis {name}",
15
14
  "text.grid-filter-to": "penapis {name}, hingga",
16
- "text.grid-no-records": "tiada apa-apa untuk dipaparkan",
17
- "text.grid-nothing-found": "tiada apa-apa dijumpai",
18
- "text.grid-open-editor": "tekan untuk mengedit",
15
+ "text.grid-open-editor": "pilih untuk menyunting",
19
16
  "text.grid-select-all-records": "pilih semua baris",
20
17
  "text.grid-select-record": "pilih baris {row}",
21
18
  "text.grid-total-records": "{total} kesemuanya",
@@ -3,19 +3,16 @@
3
3
  "button.save": "保存",
4
4
  "label.accumulator_avg": "平均",
5
5
  "label.accumulator_count": "计数",
6
- "label.accumulator_max": "最小",
7
- "label.accumulator_min": "最大",
6
+ "label.accumulator_max": "最大",
7
+ "label.accumulator_min": "最小",
8
8
  "label.accumulator_sum": "总和",
9
9
  "label.filter": "筛选",
10
10
  "text.grid-cell-input": "{name} 输入",
11
11
  "text.grid-cell-input-plain": "单元格输入",
12
- "text.grid-clear-search": "清除搜索或筛选即可查看全部",
13
12
  "text.grid-filter-from": "{name} 筛选起始值",
14
13
  "text.grid-filter-of": "{name} 筛选",
15
14
  "text.grid-filter-to": "{name} 筛选结束值",
16
- "text.grid-no-records": "还没有任何记录",
17
- "text.grid-nothing-found": "没有找到",
18
- "text.grid-open-editor": "点击进行编辑",
15
+ "text.grid-open-editor": "选择以编辑",
19
16
  "text.grid-select-all-records": "选择所有行",
20
17
  "text.grid-select-record": "选择第 {row} 行",
21
18
  "text.grid-total-records": "共 {total} 条",