mgv-backoffice 1.28.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/BaseDropdown.vue.d.ts +11 -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 +841 -580
- 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/BaseDropdown.test.ts +23 -0
- package/src/components/BaseDropdown.vue +21 -5
- 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/index.ts
CHANGED
|
@@ -45,6 +45,9 @@ export { default as BaseSegmentedControl } from './components/BaseSegmentedContr
|
|
|
45
45
|
export { default as BaseTable } from './components/BaseTable.vue'
|
|
46
46
|
export { default as BaseSpecFields } from './components/BaseSpecFields.vue'
|
|
47
47
|
export { default as BaseStatBreakdown } from './components/BaseStatBreakdown.vue'
|
|
48
|
+
export { default as BaseFilterChip } from './components/BaseFilterChip.vue'
|
|
49
|
+
export { default as BaseCredentialsForm } from './components/BaseCredentialsForm.vue'
|
|
50
|
+
export type { CredentialsView, CredentialsUpdate } from './components/BaseCredentialsForm.vue'
|
|
48
51
|
|
|
49
52
|
// Composables
|
|
50
53
|
export { useTheme, initTheme } from './composables/useTheme'
|
|
@@ -58,6 +61,7 @@ export { useMobileSidebar } from './composables/useMobileSidebar'
|
|
|
58
61
|
export { useSidebarCollapse } from './composables/useSidebarCollapse'
|
|
59
62
|
export type { UseSidebarCollapseOptions } from './composables/useSidebarCollapse'
|
|
60
63
|
export { useNotifications } from './composables/useNotifications'
|
|
64
|
+
export { useQueryParamSync } from './composables/useQueryParamSync'
|
|
61
65
|
|
|
62
66
|
// Enums
|
|
63
67
|
export { AlertEnum } from './enums/AlertEnum'
|
|
@@ -96,11 +100,13 @@ export {
|
|
|
96
100
|
fmtNumber,
|
|
97
101
|
fmtDate,
|
|
98
102
|
fmtDateTime,
|
|
103
|
+
fmtDateTimeMs,
|
|
99
104
|
fmtDateShort,
|
|
100
105
|
fmtPrice,
|
|
101
106
|
fmtPct,
|
|
102
107
|
fmtUsd,
|
|
103
108
|
fmtDuration,
|
|
104
109
|
} from './utils/format'
|
|
110
|
+
export { buildSpecParams, firstInvalidNumericSpec } from './utils/specForm'
|
|
105
111
|
export { computePnL } from './utils/pnl'
|
|
106
112
|
export type { PnL, PnLInputs } from './utils/pnl'
|
package/src/types/specField.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface SpecField {
|
|
|
11
11
|
key: string
|
|
12
12
|
label: string
|
|
13
13
|
type: SpecFieldType
|
|
14
|
+
/**
|
|
15
|
+
* Default value the form seeds the field with. `null` marks the field as
|
|
16
|
+
* OPTIONAL for `firstInvalidNumericSpec` (blank = "knob disabled").
|
|
17
|
+
*/
|
|
18
|
+
default?: SpecFieldValue | null
|
|
14
19
|
/** Choices for `type: 'select'`. */
|
|
15
20
|
options?: string[] | null
|
|
16
21
|
/** Step for numeric inputs; defaults to 1 (integer) / 0.01 (decimal). */
|
package/src/utils/format.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
fmtNumber,
|
|
4
4
|
fmtDate,
|
|
5
5
|
fmtDateTime,
|
|
6
|
+
fmtDateTimeMs,
|
|
6
7
|
fmtDateShort,
|
|
7
8
|
fmtPrice,
|
|
8
9
|
fmtPct,
|
|
@@ -10,6 +11,21 @@ import {
|
|
|
10
11
|
fmtDuration,
|
|
11
12
|
} from './format'
|
|
12
13
|
|
|
14
|
+
describe('fmtDateTimeMs', () => {
|
|
15
|
+
it('keeps the millisecond fraction so same-second rows stay distinguishable', () => {
|
|
16
|
+
const out = fmtDateTimeMs('2026-06-08T14:30:51.123Z')
|
|
17
|
+
expect(out).toMatch(/51[.,:]?\d*123|123/) // ms fraction present, locale separators vary
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('accepts epoch millis', () => {
|
|
21
|
+
expect(fmtDateTimeMs(0)).toMatch(/19?70/)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('falls back to the raw value for unparseable input', () => {
|
|
25
|
+
expect(fmtDateTimeMs('not-a-date')).toBe('Invalid Date')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
13
29
|
const EM_DASH = '—'
|
|
14
30
|
|
|
15
31
|
// Fraction digits are locale-stable even when the grouping/decimal
|
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
|
+
}
|