@stonecrop/aform 0.16.0 → 0.16.2
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/README.md +1 -1
- package/dist/aform.d.ts +37 -2
- package/dist/aform.js +2307 -2080
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -3
- package/dist/src/types/index.d.ts +30 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/AForm.vue +12 -4
- package/src/components/form/ADropdown.vue +1 -0
- package/src/components/form/AQuantityInput.vue +349 -0
- package/src/components/form/ATextboxInput.vue +66 -0
- package/src/index.ts +6 -3
- package/src/types/index.ts +32 -0
- package/src/components/form/ATextarea.vue +0 -31
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="aform_form-element aquantity">
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span class="aform_display-value">{{ displayText }}</span>
|
|
5
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
<template v-else>
|
|
8
|
+
<div class="aquantity__row">
|
|
9
|
+
<div class="aquantity__field aquantity__field--qty">
|
|
10
|
+
<div class="aquantity__group">
|
|
11
|
+
<input
|
|
12
|
+
:id="uuid"
|
|
13
|
+
v-model.number="qty"
|
|
14
|
+
class="aquantity__qty"
|
|
15
|
+
type="number"
|
|
16
|
+
:disabled="mode === 'read'"
|
|
17
|
+
:required="required"
|
|
18
|
+
@keydown="onQtyKeydown"
|
|
19
|
+
@paste="onQtyPaste" />
|
|
20
|
+
<div v-on-click-outside="closeDropdown" class="aquantity__uom">
|
|
21
|
+
<button
|
|
22
|
+
:id="`${uuid}-uom`"
|
|
23
|
+
type="button"
|
|
24
|
+
class="aquantity__uom-toggle"
|
|
25
|
+
:disabled="mode === 'read'"
|
|
26
|
+
aria-haspopup="listbox"
|
|
27
|
+
:aria-expanded="dropdown.open"
|
|
28
|
+
:aria-activedescendant="
|
|
29
|
+
dropdown.open && dropdown.activeIndex >= 0 ? `${uuid}-uom-opt-${dropdown.activeIndex}` : undefined
|
|
30
|
+
"
|
|
31
|
+
@click="toggleDropdown"
|
|
32
|
+
@keydown.down.prevent="moveActive(1)"
|
|
33
|
+
@keydown.up.prevent="moveActive(-1)"
|
|
34
|
+
@keydown.enter.prevent="selectActive"
|
|
35
|
+
@keydown.esc="closeDropdown">
|
|
36
|
+
<span class="aquantity__uom-value">{{ uom || uomLabel }}</span>
|
|
37
|
+
<span class="aquantity__caret" aria-hidden="true"></span>
|
|
38
|
+
</button>
|
|
39
|
+
<ul v-show="dropdown.open" class="aquantity__uom-menu" role="listbox" :aria-label="uomLabel">
|
|
40
|
+
<li
|
|
41
|
+
v-for="(option, i) in uoms"
|
|
42
|
+
:id="`${uuid}-uom-opt-${i}`"
|
|
43
|
+
:key="option"
|
|
44
|
+
role="option"
|
|
45
|
+
:aria-selected="option === uom"
|
|
46
|
+
class="aquantity__uom-option"
|
|
47
|
+
:class="{ 'is-active': i === dropdown.activeIndex }"
|
|
48
|
+
@mouseenter="dropdown.activeIndex = i"
|
|
49
|
+
@click="selectUom(option)">
|
|
50
|
+
{{ option }}
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="aquantity__row aquantity__row--stock">
|
|
59
|
+
<div class="aquantity__field aquantity__field--stock-uom">
|
|
60
|
+
<input :value="modelValue.stockUom" class="aform_input-field aquantity__stock-field" type="text" disabled />
|
|
61
|
+
<label class="aform_field-label">{{ stockUomLabel }}</label>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="aquantity__field aquantity__field--stock-qty">
|
|
64
|
+
<input :value="modelValue.stockQty" class="aform_input-field aquantity__stock-field" type="number" disabled />
|
|
65
|
+
<label class="aform_field-label">{{ stockQtyLabel }}</label>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="aquantity__field aquantity__field--conversion">
|
|
68
|
+
<input
|
|
69
|
+
:value="modelValue.conversionFactor"
|
|
70
|
+
class="aform_input-field aquantity__stock-field"
|
|
71
|
+
type="number"
|
|
72
|
+
disabled />
|
|
73
|
+
<label class="aform_field-label">{{ conversionFactorLabel }}</label>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
|
|
77
|
+
</template>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { vOnClickOutside } from '@vueuse/components'
|
|
83
|
+
import { computed, reactive } from 'vue'
|
|
84
|
+
|
|
85
|
+
import type { ComponentProps, QuantityOptions, QuantityValue } from '../../types'
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
label,
|
|
89
|
+
required,
|
|
90
|
+
mode,
|
|
91
|
+
uuid,
|
|
92
|
+
validation = { errorMessage: ' ' },
|
|
93
|
+
options = {},
|
|
94
|
+
uomLabel = 'UOM',
|
|
95
|
+
stockUomLabel = 'Stock UOM',
|
|
96
|
+
stockQtyLabel = 'Stock Qty',
|
|
97
|
+
conversionFactorLabel = 'Conversion Factor',
|
|
98
|
+
} = defineProps<
|
|
99
|
+
ComponentProps & {
|
|
100
|
+
options?: QuantityOptions
|
|
101
|
+
uomLabel?: string
|
|
102
|
+
stockUomLabel?: string
|
|
103
|
+
stockQtyLabel?: string
|
|
104
|
+
conversionFactorLabel?: string
|
|
105
|
+
}
|
|
106
|
+
>()
|
|
107
|
+
|
|
108
|
+
const modelValue = defineModel<QuantityValue>({
|
|
109
|
+
default: { qty: 0, uom: '', stockQty: 0, stockUom: '', conversionFactor: 1 },
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const uoms = computed(() => options.uoms ?? [])
|
|
113
|
+
|
|
114
|
+
// Round to shed binary floating-point noise (e.g. 0.1 * 3 → 0.30000000000000004) while
|
|
115
|
+
// preserving any legitimate decimal places.
|
|
116
|
+
const roundQty = (value: number): number => Number(value.toFixed(6))
|
|
117
|
+
|
|
118
|
+
const resolveConversionFactor = (uom: string): number => {
|
|
119
|
+
const stockUom = options.stockUom ?? modelValue.value.stockUom
|
|
120
|
+
if (!uom || uom === stockUom) return 1
|
|
121
|
+
const mapped = options.conversionFactors?.[uom]
|
|
122
|
+
if (mapped !== undefined) return mapped
|
|
123
|
+
// UOM absent from the conversion map: keep the stored factor only when the unit is
|
|
124
|
+
// unchanged (e.g. editing qty on a loaded value, so the factor round-trips). Switching
|
|
125
|
+
// to a new, unmapped unit resets to 1 rather than silently reusing the previous factor.
|
|
126
|
+
if (uom === modelValue.value.uom) return modelValue.value.conversionFactor ?? 1
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const recompute = (qty: number, uom: string) => {
|
|
131
|
+
const conversionFactor = resolveConversionFactor(uom)
|
|
132
|
+
modelValue.value = {
|
|
133
|
+
qty,
|
|
134
|
+
uom,
|
|
135
|
+
conversionFactor,
|
|
136
|
+
stockUom: options.stockUom ?? modelValue.value.stockUom,
|
|
137
|
+
stockQty: roundQty(qty * conversionFactor),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const qty = computed({
|
|
142
|
+
get: () => modelValue.value?.qty ?? 0,
|
|
143
|
+
set: (value: number) => recompute(value, modelValue.value?.uom ?? ''),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const uom = computed({
|
|
147
|
+
get: () => modelValue.value?.uom ?? '',
|
|
148
|
+
set: (value: string) => recompute(modelValue.value?.qty ?? 0, value),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const qtyNavigationKeys = [
|
|
152
|
+
'Backspace',
|
|
153
|
+
'Delete',
|
|
154
|
+
'Tab',
|
|
155
|
+
'Escape',
|
|
156
|
+
'Enter',
|
|
157
|
+
'ArrowLeft',
|
|
158
|
+
'ArrowRight',
|
|
159
|
+
'ArrowUp',
|
|
160
|
+
'ArrowDown',
|
|
161
|
+
'Home',
|
|
162
|
+
'End',
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
const onQtyKeydown = (event: KeyboardEvent) => {
|
|
166
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return
|
|
167
|
+
if (qtyNavigationKeys.includes(event.key)) return
|
|
168
|
+
if (/^[0-9]$/.test(event.key)) return
|
|
169
|
+
const input = event.target as HTMLInputElement
|
|
170
|
+
if (event.key === '.' && !input.value.includes('.')) return
|
|
171
|
+
event.preventDefault()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const onQtyPaste = (event: ClipboardEvent) => {
|
|
175
|
+
const pasted = event.clipboardData?.getData('text') ?? ''
|
|
176
|
+
if (!/^\d*\.?\d*$/.test(pasted)) event.preventDefault()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dropdown = reactive({ open: false, activeIndex: -1 })
|
|
180
|
+
|
|
181
|
+
const openDropdown = () => {
|
|
182
|
+
dropdown.activeIndex = Math.max(uoms.value.indexOf(uom.value), 0)
|
|
183
|
+
dropdown.open = true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const closeDropdown = () => {
|
|
187
|
+
dropdown.open = false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const toggleDropdown = () => {
|
|
191
|
+
if (dropdown.open) closeDropdown()
|
|
192
|
+
else openDropdown()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const selectUom = (value: string) => {
|
|
196
|
+
uom.value = value
|
|
197
|
+
closeDropdown()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const moveActive = (delta: number) => {
|
|
201
|
+
if (!dropdown.open) {
|
|
202
|
+
openDropdown()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const length = uoms.value.length
|
|
206
|
+
if (!length) return
|
|
207
|
+
dropdown.activeIndex = (dropdown.activeIndex + delta + length) % length
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const selectActive = () => {
|
|
211
|
+
if (!dropdown.open) {
|
|
212
|
+
openDropdown()
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
const option = uoms.value[dropdown.activeIndex]
|
|
216
|
+
if (option !== undefined) selectUom(option)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const showStock = computed(() => {
|
|
220
|
+
const v = modelValue.value
|
|
221
|
+
return !!v?.stockUom && (v.uom !== v.stockUom || v.qty !== v.stockQty)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const displayText = computed(() => {
|
|
225
|
+
const v = modelValue.value
|
|
226
|
+
if (!v || !v.uom) return '—'
|
|
227
|
+
const base = `${v.qty} ${v.uom}`
|
|
228
|
+
return showStock.value ? `${base} (${v.stockQty} ${v.stockUom})` : base
|
|
229
|
+
})
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<style scoped>
|
|
233
|
+
.aquantity__row {
|
|
234
|
+
display: flex;
|
|
235
|
+
gap: 1ch;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.aquantity__row--stock {
|
|
239
|
+
margin-top: 1.5rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.aquantity__field {
|
|
243
|
+
position: relative;
|
|
244
|
+
flex: 1;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.aquantity__group {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: stretch;
|
|
251
|
+
width: 100%;
|
|
252
|
+
border: 1px solid var(--sc-input-border-color);
|
|
253
|
+
border-radius: 0.25rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.aquantity__group:focus-within {
|
|
257
|
+
border-color: var(--sc-input-active-border-color);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.aquantity__qty {
|
|
261
|
+
flex: 1;
|
|
262
|
+
min-width: 0;
|
|
263
|
+
border: none;
|
|
264
|
+
outline: none;
|
|
265
|
+
padding: 0.5ch 1ch;
|
|
266
|
+
background: transparent;
|
|
267
|
+
border-radius: 0.25rem 0 0 0.25rem;
|
|
268
|
+
appearance: textfield;
|
|
269
|
+
-moz-appearance: textfield;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.aquantity__qty::-webkit-outer-spin-button,
|
|
273
|
+
.aquantity__qty::-webkit-inner-spin-button {
|
|
274
|
+
appearance: none;
|
|
275
|
+
-webkit-appearance: none;
|
|
276
|
+
margin: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.aquantity__uom {
|
|
280
|
+
position: relative;
|
|
281
|
+
flex: 0 0 auto;
|
|
282
|
+
border-left: 1px solid var(--sc-input-border-color);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.aquantity__uom-toggle {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 0.75ch;
|
|
289
|
+
height: 100%;
|
|
290
|
+
padding: 0.5ch 1ch;
|
|
291
|
+
background: var(--sc-gray-5);
|
|
292
|
+
border: none;
|
|
293
|
+
border-radius: 0 0.25rem 0.25rem 0;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.aquantity__uom-toggle:disabled {
|
|
299
|
+
cursor: not-allowed;
|
|
300
|
+
color: var(--sc-gray-50, #888);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.aquantity__caret {
|
|
304
|
+
display: inline-block;
|
|
305
|
+
width: 0;
|
|
306
|
+
height: 0;
|
|
307
|
+
border-left: 0.3em solid transparent;
|
|
308
|
+
border-right: 0.3em solid transparent;
|
|
309
|
+
border-top: 0.3em solid currentColor;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.aquantity__uom-menu {
|
|
313
|
+
position: absolute;
|
|
314
|
+
top: 100%;
|
|
315
|
+
right: 0;
|
|
316
|
+
z-index: 100;
|
|
317
|
+
min-width: 100%;
|
|
318
|
+
margin: 0.15rem 0 0 0;
|
|
319
|
+
padding: 0.25rem 0;
|
|
320
|
+
list-style: none;
|
|
321
|
+
background: var(--sc-input-field-background, #fff);
|
|
322
|
+
border: 1px solid var(--sc-input-active-border-color);
|
|
323
|
+
border-radius: 0.25rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.aquantity__uom-option {
|
|
327
|
+
padding: 0.4ch 1ch;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
cursor: pointer;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.aquantity__uom-option.is-active,
|
|
333
|
+
.aquantity__uom-option:hover {
|
|
334
|
+
background-color: var(--sc-row-color-zebra-light);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.aquantity__stock-field {
|
|
338
|
+
width: 100%;
|
|
339
|
+
font-size: 1rem;
|
|
340
|
+
padding: 0.5ch 1ch;
|
|
341
|
+
border: 1px solid var(--sc-input-border-color);
|
|
342
|
+
border-radius: 0.25rem;
|
|
343
|
+
outline: none;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.aquantity__stock-field:disabled {
|
|
347
|
+
color: var(--sc-gray-50, #888);
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="aform_form-element">
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span class="aform_display-value aform_textbox-display">{{ inputText ?? '' }}</span>
|
|
5
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
<template v-else>
|
|
8
|
+
<textarea
|
|
9
|
+
:id="uuid"
|
|
10
|
+
v-model="inputText"
|
|
11
|
+
class="aform_input-field aform_textbox"
|
|
12
|
+
:placeholder="placeholder"
|
|
13
|
+
:rows="rows"
|
|
14
|
+
:maxlength="maxlength"
|
|
15
|
+
:disabled="mode === 'read'"
|
|
16
|
+
:required="required"></textarea>
|
|
17
|
+
<label class="aform_field-label" :for="uuid">{{ label }}</label>
|
|
18
|
+
<p v-show="errorText" class="aform_error" v-html="errorText"></p>
|
|
19
|
+
</template>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { computed } from 'vue'
|
|
25
|
+
|
|
26
|
+
import { ComponentProps } from '../../types'
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
label,
|
|
30
|
+
required,
|
|
31
|
+
mode,
|
|
32
|
+
uuid,
|
|
33
|
+
errors,
|
|
34
|
+
placeholder = '',
|
|
35
|
+
rows = 4,
|
|
36
|
+
maxlength,
|
|
37
|
+
validation = { errorMessage: '' },
|
|
38
|
+
} = defineProps<
|
|
39
|
+
ComponentProps & {
|
|
40
|
+
/** Placeholder shown when the field is empty */
|
|
41
|
+
placeholder?: string
|
|
42
|
+
/** Visible number of text lines (maps to the textarea `rows` attribute) */
|
|
43
|
+
rows?: number
|
|
44
|
+
/** Maximum number of characters the field will accept */
|
|
45
|
+
maxlength?: number
|
|
46
|
+
}
|
|
47
|
+
>()
|
|
48
|
+
|
|
49
|
+
// Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
|
|
50
|
+
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
51
|
+
|
|
52
|
+
const inputText = defineModel<string | null>()
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.aform_textbox {
|
|
57
|
+
resize: vertical;
|
|
58
|
+
line-height: 1.5;
|
|
59
|
+
min-height: 4rem;
|
|
60
|
+
font-family: inherit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.aform_textbox-display {
|
|
64
|
+
white-space: pre-wrap;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,9 @@ import AFileAttach from './components/form/AFileAttach.vue'
|
|
|
16
16
|
import AForm from './components/AForm.vue'
|
|
17
17
|
import AFormLink from './components/form/AFormLink.vue'
|
|
18
18
|
import ANumericInput from './components/form/ANumericInput.vue'
|
|
19
|
+
import AQuantityInput from './components/form/AQuantityInput.vue'
|
|
19
20
|
import ATextInput from './components/form/ATextInput.vue'
|
|
20
|
-
import
|
|
21
|
+
import ATextboxInput from './components/form/ATextboxInput.vue'
|
|
21
22
|
import Login from './components/utilities/Login.vue'
|
|
22
23
|
import AFormLoading from './components/AFormLoading.vue'
|
|
23
24
|
|
|
@@ -43,8 +44,9 @@ function install(app: App /* options */) {
|
|
|
43
44
|
app.component('AForm', AForm)
|
|
44
45
|
app.component('AFormLink', AFormLink)
|
|
45
46
|
app.component('ANumericInput', ANumericInput)
|
|
47
|
+
app.component('AQuantityInput', AQuantityInput)
|
|
46
48
|
app.component('ATextInput', ATextInput)
|
|
47
|
-
app.component('
|
|
49
|
+
app.component('ATextboxInput', ATextboxInput)
|
|
48
50
|
app.component('ADuration', ADuration)
|
|
49
51
|
app.component('AFormLoading', AFormLoading)
|
|
50
52
|
}
|
|
@@ -63,8 +65,9 @@ export {
|
|
|
63
65
|
AForm,
|
|
64
66
|
AFormLink,
|
|
65
67
|
ANumericInput,
|
|
68
|
+
AQuantityInput,
|
|
66
69
|
ATextInput,
|
|
67
|
-
|
|
70
|
+
ATextboxInput,
|
|
68
71
|
Login,
|
|
69
72
|
AFormLoading,
|
|
70
73
|
install,
|
package/src/types/index.ts
CHANGED
|
@@ -236,3 +236,35 @@ export interface AFormLinkNavigator {
|
|
|
236
236
|
/** Navigate to the linked document. Implementation is app-defined. */
|
|
237
237
|
navigate(doctype: string, id: string | number): void
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The value shape for AQuantityInput — a quantity paired with its unit of measure, plus the
|
|
242
|
+
* derived stock-equivalent quantity/UOM. `conversionFactor` is carried on the value so it
|
|
243
|
+
* round-trips with the record even though it is never shown in the UI.
|
|
244
|
+
* @public
|
|
245
|
+
*/
|
|
246
|
+
export interface QuantityValue {
|
|
247
|
+
/** The entered quantity, in `uom` units */
|
|
248
|
+
qty: number
|
|
249
|
+
/** Unit of measure the user entered `qty` in */
|
|
250
|
+
uom: string
|
|
251
|
+
/** `qty` converted into `stockUom` units — `qty * conversionFactor` */
|
|
252
|
+
stockQty: number
|
|
253
|
+
/** The item's base/stock unit of measure — fixed, not user-editable */
|
|
254
|
+
stockUom: string
|
|
255
|
+
/** Multiplier from `uom` to `stockUom` — hidden from the UI, drives `stockQty` */
|
|
256
|
+
conversionFactor: number
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Type-specific configuration for AQuantityInput, passed via the field's `options` property.
|
|
261
|
+
* @public
|
|
262
|
+
*/
|
|
263
|
+
export interface QuantityOptions {
|
|
264
|
+
/** Dropdown choices for the `uom` field */
|
|
265
|
+
uoms?: string[]
|
|
266
|
+
/** The item's base/stock unit of measure — fixed, not user-editable */
|
|
267
|
+
stockUom?: string
|
|
268
|
+
/** Conversion factor lookup for each non-stock UOM, relative to `stockUom` (which is implicitly `1`) */
|
|
269
|
+
conversionFactors?: Record<string, number>
|
|
270
|
+
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="aform_form-element">
|
|
3
|
-
<template v-if="mode === 'display'">
|
|
4
|
-
<span class="aform_display-value">{{ inputText ?? '' }}</span>
|
|
5
|
-
<label class="aform_field-label">{{ label }}</label>
|
|
6
|
-
</template>
|
|
7
|
-
<template v-else>
|
|
8
|
-
<textarea
|
|
9
|
-
:id="uuid"
|
|
10
|
-
v-model="inputText"
|
|
11
|
-
class="aform_input-field aform_textarea"
|
|
12
|
-
:disabled="mode === 'read'"
|
|
13
|
-
:required="required"></textarea>
|
|
14
|
-
<label class="aform_field-label" :for="uuid">{{ label }} </label>
|
|
15
|
-
<p v-show="errorText" class="aform_error" v-html="errorText"></p>
|
|
16
|
-
</template>
|
|
17
|
-
</div>
|
|
18
|
-
</template>
|
|
19
|
-
|
|
20
|
-
<script setup lang="ts">
|
|
21
|
-
import { computed } from 'vue'
|
|
22
|
-
|
|
23
|
-
import { ComponentProps } from '../../types'
|
|
24
|
-
|
|
25
|
-
const { label, required, mode, uuid, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
|
|
26
|
-
|
|
27
|
-
// Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
|
|
28
|
-
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
29
|
-
|
|
30
|
-
const inputText = defineModel<number | string>()
|
|
31
|
-
</script>
|