mgv-backoffice 1.29.0 → 1.30.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.
- package/README.md +84 -0
- package/dist/components/BaseCredentialsForm.vue.d.ts +80 -0
- package/dist/components/BaseFilterChip.vue.d.ts +31 -0
- package/dist/composables/useQueryParamSync.d.ts +11 -0
- package/dist/index.d.ts +6 -1
- package/dist/types/specField.d.ts +5 -0
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +837 -578
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/specForm.d.ts +20 -0
- package/package.json +1 -1
- package/src/components/BaseCredentialsForm.test.ts +108 -0
- package/src/components/BaseCredentialsForm.vue +241 -0
- package/src/components/BaseFilterChip.test.ts +44 -0
- package/src/components/BaseFilterChip.vue +67 -0
- package/src/composables/useQueryParamSync.test.ts +56 -0
- package/src/composables/useQueryParamSync.ts +42 -0
- package/src/index.ts +6 -0
- package/src/types/specField.ts +5 -0
- package/src/utils/format.test.ts +16 -0
- package/src/utils/format.ts +23 -0
- package/src/utils/specForm.test.ts +53 -0
- package/src/utils/specForm.ts +48 -0
package/src/utils/format.ts
CHANGED
|
@@ -55,6 +55,29 @@ export function fmtDateShort(ms: number): string {
|
|
|
55
55
|
return new Date(ms).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Format an ISO string / epoch-millis as a full 24-hour locale date-time WITH
|
|
60
|
+
* the millisecond fraction — for dense feeds where several rows can land in
|
|
61
|
+
* the same second and must stay distinguishable. Falls back to the raw value
|
|
62
|
+
* when the input can't be parsed.
|
|
63
|
+
*/
|
|
64
|
+
export function fmtDateTimeMs(s: string | number): string {
|
|
65
|
+
try {
|
|
66
|
+
return new Date(s).toLocaleString(undefined, {
|
|
67
|
+
year: 'numeric',
|
|
68
|
+
month: '2-digit',
|
|
69
|
+
day: '2-digit',
|
|
70
|
+
hour: '2-digit',
|
|
71
|
+
minute: '2-digit',
|
|
72
|
+
second: '2-digit',
|
|
73
|
+
fractionalSecondDigits: 3,
|
|
74
|
+
hour12: false,
|
|
75
|
+
})
|
|
76
|
+
} catch {
|
|
77
|
+
return String(s)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
/**
|
|
59
82
|
* Format a price with precision that scales to magnitude: penny stocks and
|
|
60
83
|
* sub-cent crypto get more decimals so the value never collapses to "0.00".
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { SpecField } from '../types/specField'
|
|
3
|
+
import { buildSpecParams, firstInvalidNumericSpec } from './specForm'
|
|
4
|
+
|
|
5
|
+
// Mirrors a typical backend catalogue shape: required numerics carry a
|
|
6
|
+
// default, the optional field carries `default: null` (the optionality
|
|
7
|
+
// convention — blank means "knob disabled").
|
|
8
|
+
const specs: SpecField[] = [
|
|
9
|
+
{ key: 'amountPerSymbol', label: 'Amount per symbol ($)', type: 'decimal', default: 4, min: 1, step: 0.01, options: null, help: '' },
|
|
10
|
+
{ key: 'stopLossPercent', label: 'Stop loss (%)', type: 'decimal', default: null, min: 0.1, step: 0.1, options: null, help: '' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('firstInvalidNumericSpec', () => {
|
|
14
|
+
it('flags a blank required numeric field', () => {
|
|
15
|
+
expect(firstInvalidNumericSpec(specs, { amountPerSymbol: '', stopLossPercent: 1 }))
|
|
16
|
+
.toBe('Amount per symbol ($)')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it.each([['' as const], [null as never], [undefined as never]])(
|
|
20
|
+
'accepts a blank optional field (cleared → %j)',
|
|
21
|
+
(blank) => {
|
|
22
|
+
expect(firstInvalidNumericSpec(specs, { amountPerSymbol: 10, stopLossPercent: blank }))
|
|
23
|
+
.toBeNull()
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
it('still validates a non-blank value on an optional field', () => {
|
|
28
|
+
expect(firstInvalidNumericSpec(specs, { amountPerSymbol: 10, stopLossPercent: NaN }))
|
|
29
|
+
.toBe('Stop loss (%)')
|
|
30
|
+
expect(firstInvalidNumericSpec(specs, { amountPerSymbol: 10, stopLossPercent: 2.5 }))
|
|
31
|
+
.toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('accepts a fully valid value set and tolerates missing specs', () => {
|
|
35
|
+
expect(firstInvalidNumericSpec(specs, { amountPerSymbol: 10, stopLossPercent: 1 })).toBeNull()
|
|
36
|
+
expect(firstInvalidNumericSpec(undefined, {})).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('buildSpecParams', () => {
|
|
41
|
+
it('seeds from defaults, keeping caller-supplied overlaps', () => {
|
|
42
|
+
expect(buildSpecParams(specs, { amountPerSymbol: 10 }).amountPerSymbol).toBe(10)
|
|
43
|
+
expect(buildSpecParams(specs, {}).amountPerSymbol).toBe(4)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('seeds an optional field with null (renders as an empty input)', () => {
|
|
47
|
+
expect(buildSpecParams(specs, {}).stopLossPercent).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns an empty map when there are no specs', () => {
|
|
51
|
+
expect(buildSpecParams(undefined, { anything: 1 })).toEqual({})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SpecField, SpecFieldValue } from '../types/specField'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for spec-driven forms (the value maps behind BaseSpecFields), so
|
|
5
|
+
* param seeding and numeric validation stay single-sourced across consumers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the value map for a spec set: keep any overlapping values the caller
|
|
10
|
+
* already has (e.g. a shared amount field carries across spec sets), otherwise
|
|
11
|
+
* fall back to the spec's default. No specs → empty map. A `default: null`
|
|
12
|
+
* seeds the field with null so it renders as an empty (optional) input.
|
|
13
|
+
*/
|
|
14
|
+
export function buildSpecParams(
|
|
15
|
+
specs: readonly SpecField[] | undefined,
|
|
16
|
+
existing: Record<string, SpecFieldValue>,
|
|
17
|
+
): Record<string, SpecFieldValue> {
|
|
18
|
+
const out: Record<string, SpecFieldValue> = {}
|
|
19
|
+
if (!specs) return out
|
|
20
|
+
for (const spec of specs) {
|
|
21
|
+
const prev = existing[spec.key]
|
|
22
|
+
out[spec.key] = prev !== undefined && prev !== null ? prev : (spec.default as SpecFieldValue)
|
|
23
|
+
}
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Label of the first blank / NaN numeric field (a cleared number input becomes
|
|
29
|
+
* '' under number coercion), or null when every numeric field holds a number.
|
|
30
|
+
* A spec whose `default` is null is OPTIONAL: blank means "knob disabled" and
|
|
31
|
+
* must pass; a non-blank value on an optional field is still validated as a
|
|
32
|
+
* number.
|
|
33
|
+
*/
|
|
34
|
+
export function firstInvalidNumericSpec(
|
|
35
|
+
specs: readonly SpecField[] | undefined,
|
|
36
|
+
params: Record<string, SpecFieldValue>,
|
|
37
|
+
): string | null {
|
|
38
|
+
if (!specs) return null
|
|
39
|
+
for (const spec of specs) {
|
|
40
|
+
if (spec.type === 'decimal' || spec.type === 'integer') {
|
|
41
|
+
const v = params[spec.key]
|
|
42
|
+
const blank = v === '' || v === null || v === undefined
|
|
43
|
+
if (spec.default === null && blank) continue
|
|
44
|
+
if (typeof v !== 'number' || Number.isNaN(v)) return spec.label
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|