@d-mok/quasar-app-extension-quasar-axe 3.1.64 → 3.1.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d-mok/quasar-app-extension-quasar-axe",
3
- "version": "3.1.64",
3
+ "version": "3.1.67",
4
4
  "description": "A Quasar App Extension",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -19,8 +19,11 @@ type PropType<C extends VueComponent> = Omit<
19
19
  >
20
20
  type OkType<C extends VueComponent> = Parameters<InstanceType<C>['$emit']>[1]
21
21
 
22
- export async function base(component: Component, props: any) {
23
- return new Promise<any>((resolve, reject) =>
22
+ export async function custom<C extends VueComponent>(
23
+ component: C,
24
+ props?: PropType<C>
25
+ ): Promise<OkType<C>> {
26
+ return new Promise<OkType<C>>((resolve, reject) =>
24
27
  Dialog.create({
25
28
  component,
26
29
  componentProps: {
@@ -28,18 +31,11 @@ export async function base(component: Component, props: any) {
28
31
  ...props,
29
32
  },
30
33
  })
31
- .onOk((data: any) => resolve(data))
34
+ .onOk(resolve)
32
35
  .onCancel(() => reject('dialog cancelled by user.'))
33
36
  )
34
37
  }
35
38
 
36
- export async function custom<C extends VueComponent>(
37
- component: C,
38
- props?: PropType<C>
39
- ): Promise<OkType<C>> {
40
- return await base(component, props ?? {})
41
- }
42
-
43
39
  /**
44
40
  * Ask for a string by a text area. Allow empty.
45
41
  */
@@ -49,7 +45,7 @@ export async function askTextarea(
49
45
  prefill: string = '',
50
46
  isValid: Predicate<string> = $ => true
51
47
  ): Promise<string> {
52
- return await base(dialogTextarea, {
48
+ return await custom(dialogTextarea, {
53
49
  title,
54
50
  message,
55
51
  prefill,
@@ -67,12 +63,11 @@ export async function askBtn<T>(
67
63
  items: T[],
68
64
  label?: (string & keyof T) | Mapper<T, string>
69
65
  ): Promise<T> {
70
- return await base(dialogBtn, {
66
+ return await custom(dialogBtn, {
71
67
  title,
72
68
  message,
73
69
  items,
74
70
  labelFunc: ($: T) => Object.print($, label),
75
- cancel: true,
76
71
  })
77
72
  }
78
73
 
@@ -94,7 +89,9 @@ export async function askFn(
94
89
  * The first non-empty prefill will be used.
95
90
  * The last non-empty prefill will be the spec.
96
91
  */
97
- export async function askTable<T extends object>(
92
+ export async function askTable<
93
+ T extends Record<string, string | number | boolean>
94
+ >(
98
95
  title: string,
99
96
  message: string = '',
100
97
  prefills: T[][],
@@ -102,43 +99,17 @@ export async function askTable<T extends object>(
102
99
  arrayValidators: [(_: T[]) => boolean, string][] = [[$ => true, '']],
103
100
  headersMap: Record<string, string> = {}
104
101
  ): Promise<T[]> {
105
- if (prefills.flat().length === 0)
106
- throwError('Error', 'All prefills are empty array.')
107
- return await base(dialogTable, {
102
+ const ps = prefills.unmatch($ => $.length === 0)
103
+ if (ps.length === 0) throwError('Error', 'All prefills are empty array.')
104
+ return await custom(dialogTable, {
108
105
  title,
109
106
  message,
110
- content: prefills.find($ => $.length > 0),
111
- sample: [...prefills].reverse().find($ => $.length > 0)![0],
112
- validators,
113
- arrayValidators,
107
+ content: ps[0]!,
108
+ sample: ps.at(-1)![0]!,
109
+ validators: validators as any,
110
+ arrayValidators: arrayValidators as any,
114
111
  headersMap,
115
112
  cancel: true,
116
- allowAddRow: true,
117
- })
118
- }
119
-
120
- /**
121
- * Edit an array of objects by table.
122
- */
123
- export async function editTable<T extends object>(
124
- title: string,
125
- message: string = '',
126
- prefill: T[],
127
- validators: [(_: T) => boolean, string][] = [[$ => true, '']],
128
- arrayValidators: [(_: T[]) => boolean, string][] = [[$ => true, '']],
129
- headersMap: Record<string, string> = {}
130
- ): Promise<T[]> {
131
- if (prefill.length === 0) throwError('Error', 'prefill is empty array.')
132
- return await base(dialogTable, {
133
- title,
134
- message,
135
- content: prefill,
136
- sample: prefill[0],
137
- validators,
138
- arrayValidators,
139
- headersMap,
140
- cancel: true,
141
- allowAddRow: false,
142
113
  })
143
114
  }
144
115
 
@@ -170,7 +141,7 @@ export async function showTextarea(
170
141
  message: string = '',
171
142
  content: string = ''
172
143
  ): Promise<void> {
173
- await base(dialogTextarea, {
144
+ await custom(dialogTextarea, {
174
145
  title,
175
146
  message,
176
147
  prefill: content,
@@ -185,21 +156,20 @@ export async function showTextarea(
185
156
  export async function showTable(
186
157
  title: string,
187
158
  message: string,
188
- content: object[]
159
+ content: Record<string, string | number | boolean>[]
189
160
  ): Promise<void> {
190
161
  if (content.length === 0) {
191
162
  content = [{ empty: '' }]
192
163
  }
193
- await base(dialogTable, {
164
+ await custom(dialogTable, {
194
165
  title,
195
166
  message,
196
167
  content,
197
- sample: content[0],
168
+ sample: content[0]!,
198
169
  validators: [[($: any) => true, '']],
199
170
  arrayValidators: [[($: any) => true, '']],
200
171
  headersMap: {},
201
172
  cancel: false,
202
- allowAddRow: false,
203
173
  })
204
174
  }
205
175
 
@@ -209,7 +179,7 @@ export async function showTable(
209
179
  export async function showArray(
210
180
  title: string,
211
181
  message: string = '',
212
- content: unknown[] = []
182
+ content: (string | number)[] = []
213
183
  ): Promise<void> {
214
184
  await showTable(
215
185
  title,
@@ -225,7 +195,7 @@ export async function askFile(
225
195
  title: string,
226
196
  message: string = ''
227
197
  ): Promise<File> {
228
- return await base(dialogFile, {
198
+ return await custom(dialogFile, {
229
199
  title,
230
200
  message,
231
201
  multiple: false,
@@ -240,7 +210,7 @@ export async function askFiles(
240
210
  title: string,
241
211
  message: string = ''
242
212
  ): Promise<File[]> {
243
- return await base(dialogFile, {
213
+ return await custom(dialogFile, {
244
214
  title,
245
215
  message,
246
216
  multiple: true,
@@ -259,14 +229,13 @@ export async function askRadioText(
259
229
  newPlaceholder: string = '',
260
230
  isValid: Predicate<string> = $ => true
261
231
  ): Promise<string> {
262
- return await base(dialogRadioText, {
232
+ return await custom(dialogRadioText, {
263
233
  title,
264
234
  message,
265
235
  items,
266
236
  prefill,
267
237
  newPlaceholder,
268
238
  isValid,
269
- cancel: true,
270
239
  })
271
240
  }
272
241
 
@@ -281,14 +250,13 @@ export async function askCheckboxText(
281
250
  newPlaceholder: string = '',
282
251
  isValid: Predicate<string[]> = $ => true
283
252
  ): Promise<string[]> {
284
- return await base(dialogCheckboxText, {
253
+ return await custom(dialogCheckboxText, {
285
254
  title,
286
255
  message,
287
256
  items,
288
257
  prefill,
289
258
  newPlaceholder,
290
259
  isValid,
291
- cancel: true,
292
260
  })
293
261
  }
294
262
 
@@ -360,11 +328,15 @@ export async function askForm<T extends FormPrefill>(
360
328
  return $
361
329
  })
362
330
 
363
- return await base(dialogForm, {
331
+ const output = await custom(dialogForm, {
364
332
  title,
365
333
  message,
366
- prefill: standardPrefill,
367
- validators,
368
- onChange,
334
+ prefill: standardPrefill as any,
335
+ validators: validators as any,
336
+ onChange: onChange as any,
369
337
  })
338
+
339
+ return Object.mapValues(output, v =>
340
+ typeof v === 'string' ? v.trim() : v
341
+ ) as any
370
342
  }
@@ -24,16 +24,14 @@
24
24
  <handson
25
25
  v-model="dummy"
26
26
  :sample="sample"
27
- :allowAddRow="allowAddRow"
28
27
  :headersMap="headersMap"
29
28
  v-if="exist"
30
29
  ></handson>
31
30
  </q-card>
32
31
  <div
33
- v-show="err()"
32
+ v-if="err"
34
33
  class="text-red"
35
- style="text-align: right"
36
- v-html="err()"
34
+ v-html="err.join('<br/>')"
37
35
  />
38
36
  </q-card-section>
39
37
 
@@ -53,7 +51,7 @@
53
51
  label="OK"
54
52
  flat
55
53
  @click="onDialogOK(filterBlank(dummy))"
56
- :disable="cancel && err() !== false"
54
+ :disable="cancel && err !== false"
57
55
  />
58
56
  </q-card-actions>
59
57
  </q-card>
@@ -63,7 +61,7 @@
63
61
  <script lang="ts" setup>
64
62
  import handson from './handson.vue'
65
63
  import { useDialogPluginComponent } from 'quasar'
66
- import { ref } from 'vue'
64
+ import { computed, ref } from 'vue'
67
65
  import { clone } from './schema'
68
66
  import { copyToClipboard } from 'quasar'
69
67
  import * as v from 'valibot'
@@ -83,7 +81,6 @@ const {
83
81
  validators,
84
82
  arrayValidators,
85
83
  cancel,
86
- allowAddRow,
87
84
  headersMap,
88
85
  } = defineProps<{
89
86
  title: string
@@ -93,7 +90,6 @@ const {
93
90
  validators: [(_: row) => boolean, string][]
94
91
  arrayValidators: [(_: row[]) => boolean, string][]
95
92
  cancel: boolean
96
- allowAddRow: boolean
97
93
  headersMap: Record<string, string>
98
94
  }>()
99
95
 
@@ -125,40 +121,39 @@ const EmptySchema = v.strictObject(
125
121
  })
126
122
  )
127
123
 
128
- function err(): string | false {
124
+ const err = computed(() => {
129
125
  let rows = dummy.value
130
126
 
131
- for (let i = 0; i < rows.length; i++) {
132
- let row = rows[i]!
127
+ for (let i = 1; i <= rows.length; i++) {
128
+ let row = rows[i - 1]!
133
129
  if (v.is(EmptySchema, row)) continue
134
130
 
135
131
  if (!v.is(StandardSchema, row)) {
136
132
  if (Object.keys(row).length > Object.keys(sample).length) {
137
- return `row ${i + 1} has extra column`
133
+ return [`#${i} has extra column`]
138
134
  }
139
- return (
140
- `row ${i + 1} required format:<br/>` +
141
- Object.entries(sample)
142
- .map(([k, v]) => `${k}: ${v}`)
143
- .join('<br/>')
144
- )
135
+ return [
136
+ `#${i} require format:`,
137
+ ...Object.entries(sample).map(
138
+ ([k, v]) => `${headersMap[k] ?? k}: ${v}`
139
+ ),
140
+ ]
145
141
  }
146
142
 
147
143
  for (let [f, msg] of validators) {
148
- if (!f(row)) return `row ${i + 1}:<br/>` + msg
144
+ if (!f(row)) return [`#${i}:`, msg]
149
145
  }
150
146
  }
151
147
 
152
148
  for (let [f, msg] of arrayValidators) {
153
149
  if (!f(filterBlank(rows))) {
154
- return msg
150
+ return [msg]
155
151
  }
156
152
  }
157
153
  return false
158
- }
154
+ })
159
155
 
160
156
  function filterBlank(rows: row[]): row[] {
161
- if (!allowAddRow) return rows
162
157
  return rows.unmatch(r => v.is(EmptySchema, r))
163
158
  }
164
159
 
@@ -172,7 +167,7 @@ function copy() {
172
167
  const TOTAL_WIDTH =
173
168
  200 +
174
169
  Object.values(
175
- [...content, Object.mapValues(sample, (v, k) => k)]
170
+ [...content, Object.mapValues(sample, (v, k) => headersMap[k] ?? k)]
176
171
  .map(r => Object.mapValues(r, v => v.toString().length))
177
172
  .reduce((acc, obj) => {
178
173
  for (const key in acc) {
@@ -7,16 +7,24 @@
7
7
  width="100%"
8
8
  stretchH="all"
9
9
  :data="content"
10
+ :dataSchema="
11
+ Object.mapValues(sample, val =>
12
+ typeof val === 'boolean'
13
+ ? false
14
+ : typeof val === 'number'
15
+ ? NaN
16
+ : ''
17
+ )
18
+ "
10
19
  :colHeaders="Object.keys(sample).map($ => headersMap[$] ?? $)"
11
20
  :columns="columns()"
12
21
  :rowHeaders="true"
22
+ :rowHeaderWidth="50"
13
23
  :afterChange="shake"
14
- :afterRemoveRow="afterRemoveRow"
15
24
  :afterCreateRow="shake"
25
+ :afterRemoveRow="shake"
16
26
  :afterCopy="afterCopy"
17
- :contextMenu="
18
- allowAddRow ? ['row_above', 'row_below', 'remove_row'] : []
19
- "
27
+ :contextMenu="['row_above', 'row_below', 'remove_row']"
20
28
  />
21
29
  </template>
22
30
 
@@ -24,7 +32,9 @@
24
32
  import { ref } from 'vue'
25
33
  import { HotTable } from '@handsontable/vue3'
26
34
  import { registerAllModules } from 'handsontable/registry'
27
- import type Handsontable from 'handsontable'
35
+ import Handsontable from 'handsontable'
36
+
37
+ type row = Record<string, string | number | boolean>
28
38
 
29
39
  registerAllModules()
30
40
 
@@ -32,20 +42,45 @@ registerAllModules()
32
42
 
33
43
  let hotTableComponent = ref<InstanceType<typeof HotTable> | null>(null)
34
44
 
35
- function getInstance(): Handsontable {
45
+ function hot(): Handsontable {
36
46
  return hotTableComponent.value.hotInstance
37
47
  }
38
48
 
39
- const content = defineModel<Record<string, string | number | boolean>[]>({
40
- required: true,
41
- })
49
+ const content = defineModel<row[]>({ required: true })
42
50
 
43
- const { sample, allowAddRow, headersMap } = defineProps<{
44
- sample: Record<string, string | number | boolean>
45
- allowAddRow: boolean
51
+ const { sample, headersMap } = defineProps<{
52
+ sample: row
46
53
  headersMap: Record<string, string>
47
54
  }>()
48
55
 
56
+ function numericRenderer(
57
+ instance: Handsontable.Core,
58
+ td: HTMLTableCellElement,
59
+ row: number,
60
+ col: number,
61
+ prop: string | number,
62
+ value: any,
63
+ cellProperties: Handsontable.CellProperties
64
+ ) {
65
+ Handsontable.renderers.NumericRenderer(
66
+ instance,
67
+ td,
68
+ row,
69
+ col,
70
+ prop,
71
+ value,
72
+ cellProperties
73
+ )
74
+
75
+ const isInvalid = value === null || value === undefined || isNaN(value)
76
+
77
+ if (isInvalid) {
78
+ td.innerText = ''
79
+ const isBlankRow = instance.getDataAtRow(row).belongs(['', false, NaN])
80
+ if (!isBlankRow) td.style.backgroundColor = '#ffbeba'
81
+ }
82
+ }
83
+
49
84
  function columns() {
50
85
  return Object.entries(sample).map(([field, val]) => ({
51
86
  data: field,
@@ -56,30 +91,29 @@ function columns() {
56
91
  ? 'numeric'
57
92
  : 'text',
58
93
  allowEmpty: false,
94
+ renderer: typeof val === 'number' ? numericRenderer : undefined,
59
95
  }))
60
96
  }
61
97
 
62
- function coerceString(x: any): string {
63
- if (x === null) return ''
64
- if (x === undefined) return ''
65
- return String(x).replaceAll(String.fromCodePoint(0x00a0), ' ')
66
- }
98
+ function shake(): void {
99
+ function coerceString(x: any): string {
100
+ if (x === null) return ''
101
+ if (x === undefined) return ''
102
+ return String(x).replaceAll(String.fromCodePoint(0x00a0), ' ').trim()
103
+ }
67
104
 
68
- function coerceNumber(x: any): number {
69
- if (typeof x !== 'number') return NaN
70
- return Number(x)
71
- }
105
+ function coerceNumber(x: any): number {
106
+ if (typeof x !== 'number') return NaN
107
+ return Number(x)
108
+ }
72
109
 
73
- function coerceBoolean(x: any): boolean {
74
- if (typeof x === 'boolean') return x
75
- if (x === 'true') return true
76
- if (x === 'false') return false
77
- if (x === 'TRUE') return true
78
- if (x === 'FALSE') return false
79
- return false
80
- }
110
+ function coerceBoolean(x: any): boolean {
111
+ if (typeof x === 'boolean') return x
112
+ if (x === 'true') return true
113
+ if (x === 'TRUE') return true
114
+ return false
115
+ }
81
116
 
82
- function shake(): void {
83
117
  const keys = Object.keys(sample)
84
118
  for (let row of content.value) {
85
119
  for (let [k, v] of Object.entries(row)) {
@@ -90,18 +124,15 @@ function shake(): void {
90
124
  if (!keys.includes(k)) delete row[k]
91
125
  }
92
126
  }
93
- }
94
127
 
95
- function afterRemoveRow() {
96
- // avoid no row
97
- let hot = getInstance()
98
- if (hot.countRows() === 0) {
99
- hot.alter('insert_row_above', 1)
128
+ const lastRow = content.value.at(-1)
129
+
130
+ if (!lastRow || !Object.values(lastRow).belongs(['', false, NaN])) {
131
+ const h = hot()
132
+ h.alter('insert_row_below', null as any, 1)
100
133
  }
101
134
  }
102
135
 
103
- // aftercopy
104
-
105
136
  async function afterCopy(data: (string | number | boolean)[][], coords: any) {
106
137
  // fix pasting space as non break space problem
107
138
  let text = data.map(r => r.join('\t')).join('\n')