@quazardous/qdadm 1.21.0 → 2.0.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 CHANGED
@@ -31,7 +31,7 @@ import { SecurityChecker, PermissionMatcher, PermissionRegistry } from '@quazard
31
31
  import { PersistableRoleProvider, createLocalStorageRolesProvider } from '@quazardous/qdadm/security'
32
32
 
33
33
  // Composables
34
- import { useForm, useBareForm, useListPage } from '@quazardous/qdadm/composables'
34
+ import { useBareForm, useListPage } from '@quazardous/qdadm/composables'
35
35
 
36
36
  // Components
37
37
  import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "1.21.0",
3
+ "version": "2.0.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -6,10 +6,17 @@
6
6
  *
7
7
  * Usage:
8
8
  * <VanillaJsonEditor v-model="jsonData" mode="tree" />
9
+ *
10
+ * Inline JSON Schema validation (errors highlighted live in the editor):
11
+ * <VanillaJsonEditor v-model="jsonData" :schema="mySchema" />
12
+ *
13
+ * Or a raw validator for advanced cases (takes precedence over `schema`):
14
+ * <VanillaJsonEditor v-model="jsonData" :validator="myValidator" />
9
15
  */
10
16
 
11
17
  import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
12
18
  import { JSONEditor, type Content, type ContentErrors, type OnChangeStatus, type Mode } from 'vanilla-jsoneditor'
19
+ import { buildJsonValidator, type Validator, type JSONSchema, type JsonValidatorOptions } from './jsonValidator'
13
20
 
14
21
  type JsonValue = Record<string, unknown> | unknown[] | string | null
15
22
 
@@ -21,6 +28,14 @@ interface Props {
21
28
  mainMenuBar?: boolean
22
29
  navigationBar?: boolean
23
30
  statusBar?: boolean
31
+ /** JSON Schema for inline Ajv validation (errors highlighted in the editor). */
32
+ schema?: JSONSchema
33
+ /** Raw validator function — takes precedence over `schema`. */
34
+ validator?: Validator
35
+ /** Optional `$ref` definitions passed through to the Ajv validator. */
36
+ schemaDefinitions?: JsonValidatorOptions['schemaDefinitions']
37
+ /** Optional Ajv options passed through to the Ajv validator. */
38
+ ajvOptions?: JsonValidatorOptions['ajvOptions']
24
39
  }
25
40
 
26
41
  const props = withDefaults(defineProps<Props>(), {
@@ -30,9 +45,23 @@ const props = withDefaults(defineProps<Props>(), {
30
45
  readOnly: false,
31
46
  mainMenuBar: true,
32
47
  navigationBar: true,
33
- statusBar: true
48
+ statusBar: true,
49
+ schema: undefined,
50
+ validator: undefined,
51
+ schemaDefinitions: undefined,
52
+ ajvOptions: undefined
34
53
  })
35
54
 
55
+ // Resolve the effective validator from props (raw validator > schema > none).
56
+ function resolveValidator(): Validator | undefined {
57
+ return buildJsonValidator({
58
+ validator: props.validator,
59
+ schema: props.schema,
60
+ schemaDefinitions: props.schemaDefinitions,
61
+ ajvOptions: props.ajvOptions
62
+ })
63
+ }
64
+
36
65
  const emit = defineEmits<{
37
66
  'update:modelValue': [value: JsonValue]
38
67
  'change': [value: JsonValue]
@@ -76,6 +105,7 @@ onMounted(() => {
76
105
  mainMenuBar: props.mainMenuBar,
77
106
  navigationBar: props.navigationBar,
78
107
  statusBar: props.statusBar,
108
+ validator: resolveValidator(),
79
109
  onChange: (updatedContent: Content, _previousContent: Content, { contentErrors }: OnChangeStatus) => {
80
110
  // Skip if this change came from a programmatic prop update
81
111
  if (updatingFromProp.value) return
@@ -144,6 +174,17 @@ watch(() => props.readOnly, (newReadOnly: boolean) => {
144
174
  }
145
175
  })
146
176
 
177
+ // Watch validator/schema changes — rebuild and re-apply so edits re-validate live
178
+ watch(
179
+ () => [props.validator, props.schema, props.schemaDefinitions, props.ajvOptions],
180
+ () => {
181
+ if (editor) {
182
+ editor.updateProps({ validator: resolveValidator() })
183
+ }
184
+ },
185
+ { deep: true }
186
+ )
187
+
147
188
  // Cleanup
148
189
  onUnmounted(() => {
149
190
  if (editor) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * jsonValidator - build a vanilla-jsoneditor `Validator` from wrapper props.
3
+ *
4
+ * Pure helper extracted from VanillaJsonEditor so the resolution logic
5
+ * (raw validator vs JSON Schema vs none) is unit-testable without mounting the
6
+ * Svelte editor. See qdadm #1050.
7
+ */
8
+ import {
9
+ createAjvValidator,
10
+ type Validator,
11
+ type JSONSchema,
12
+ type AjvValidatorOptions,
13
+ } from 'vanilla-jsoneditor'
14
+
15
+ export type { Validator, JSONSchema }
16
+
17
+ export interface JsonValidatorOptions {
18
+ /** Raw validator function — takes precedence over `schema` when both are set. */
19
+ validator?: Validator
20
+ /** JSON Schema; compiled to a validator via Ajv (bundled in vanilla-jsoneditor). */
21
+ schema?: JSONSchema
22
+ /** Optional `$ref` definitions passed through to `createAjvValidator`. */
23
+ schemaDefinitions?: AjvValidatorOptions['schemaDefinitions']
24
+ /** Optional Ajv options passed through to `createAjvValidator`. */
25
+ ajvOptions?: AjvValidatorOptions['ajvOptions']
26
+ }
27
+
28
+ /**
29
+ * Resolve the effective validator:
30
+ * - an explicit `validator` wins;
31
+ * - otherwise a `schema` is compiled with `createAjvValidator`;
32
+ * - otherwise `undefined` (no validation — the wrapper's default behavior).
33
+ */
34
+ export function buildJsonValidator(options: JsonValidatorOptions): Validator | undefined {
35
+ if (options.validator) return options.validator
36
+ if (options.schema) {
37
+ return createAjvValidator({
38
+ schema: options.schema,
39
+ schemaDefinitions: options.schemaDefinitions,
40
+ ajvOptions: options.ajvOptions,
41
+ })
42
+ }
43
+ return undefined
44
+ }
@@ -27,8 +27,6 @@ export { default as FormPage } from './edit/FormPage.vue'
27
27
  export { default as FormField } from './edit/FormField.vue'
28
28
  export { default as FormInput } from './edit/FormInput.vue'
29
29
  export { default as FormActions } from './edit/FormActions.vue'
30
- export { default as FormTabs } from './edit/FormTabs.vue' // Deprecated: use FieldGroups with layout="tabs"
31
- export { default as FormTab } from './edit/FormTab.vue' // Deprecated: use FieldGroups with layout="tabs"
32
30
  export { default as LookupField } from './edit/LookupField.vue'
33
31
  export { default as LookupPickerDialog } from './edit/LookupPickerDialog.vue'
34
32
 
@@ -41,7 +39,6 @@ export { default as ParentCard } from './show/ParentCard.vue'
41
39
  // Lists
42
40
  export { default as ListPage } from './lists/ListPage.vue'
43
41
  export { default as ActionButtons } from './lists/ActionButtons.vue'
44
- export { default as ActionColumn } from './lists/ActionColumn.vue'
45
42
  export { default as FilterBar } from './lists/FilterBar.vue'
46
43
 
47
44
  // Editors (vanilla-jsoneditor free)
@@ -50,7 +50,7 @@ interface UserData {
50
50
  const features = inject<FeaturesConfig>('qdadmFeatures', { poweredBy: true, breadcrumb: true })
51
51
  const homeRoute = inject<string>('qdadmHomeRoute', 'home')
52
52
 
53
- // Guard dialog from shared store (registered by useBareForm/useForm when a form is active)
53
+ // Guard dialog from shared store (registered by useBareForm/useEntityItemFormPage when a form is active)
54
54
  const guardDialog = useGuardDialog()
55
55
 
56
56
  const router = useRouter()
@@ -238,7 +238,7 @@ watch(() => route.fullPath, () => {
238
238
  })
239
239
 
240
240
  // Navigation context (breadcrumb + navlinks from route config)
241
- // Entity data comes from activeStack (populated by useEntityItemPage/useForm)
241
+ // Entity data comes from activeStack (populated by useEntityItemPage/useEntityItemFormPage)
242
242
  const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext()
243
243
 
244
244
  // Allow child pages to override breadcrumb/navlinks via provide/inject
@@ -50,7 +50,7 @@ import DefaultBreadcrumb from './defaults/DefaultBreadcrumb.vue'
50
50
  const slots = useSlots()
51
51
  const hasMainSlot: boolean = !!slots.main
52
52
 
53
- // Guard dialog from shared store (registered by useBareForm/useForm when a form is active)
53
+ // Guard dialog from shared store (registered by useBareForm/useEntityItemFormPage when a form is active)
54
54
  const guardDialog = useGuardDialog()
55
55
 
56
56
  // Provide breadcrumb/navlinks override mechanism for child pages
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
47
47
  badges: () => []
48
48
  })
49
49
 
50
- // Auto-injected title from useForm (if available)
50
+ // Auto-injected title from the form composables (if available)
51
51
  const injectedTitleParts = inject<Ref<TitleParts | null> | null>('qdadmPageTitleParts', null)
52
52
 
53
53
  // Use props first, then injected, then fallback
@@ -28,6 +28,7 @@
28
28
  import { computed, type PropType } from 'vue'
29
29
  import { RouterLink, type RouteLocationRaw } from 'vue-router'
30
30
  import Tag from 'primevue/tag'
31
+ import { formatDate, formatNumber, formatCurrency } from '../../utils/formatters'
31
32
 
32
33
  type DisplayType =
33
34
  | 'text'
@@ -101,13 +102,12 @@ const textValue = computed(() => {
101
102
  return String(props.value)
102
103
  })
103
104
 
104
- // Format number value
105
+ // Format number value (browser locale by default — utils/formatters policy)
105
106
  const numberValue = computed(() => {
106
107
  if (isEmpty.value) return '-'
107
108
  const num = Number(props.value)
108
109
  if (isNaN(num)) return String(props.value)
109
- const locale = props.field.locale || 'en-US'
110
- return new Intl.NumberFormat(locale).format(num)
110
+ return formatNumber(num, {}, props.field.locale)
111
111
  })
112
112
 
113
113
  // Format currency value
@@ -115,9 +115,7 @@ const currencyValue = computed(() => {
115
115
  if (isEmpty.value) return '-'
116
116
  const num = Number(props.value)
117
117
  if (isNaN(num)) return String(props.value)
118
- const locale = props.field.locale || 'en-US'
119
- const currency = props.field.currencyCode || 'USD'
120
- return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(num)
118
+ return formatCurrency(num, props.field.currencyCode || 'USD', props.field.locale)
121
119
  })
122
120
 
123
121
  // Format boolean value
@@ -143,9 +141,8 @@ const dateValue = computed(() => {
143
141
  const v = props.value as unknown
144
142
  const date = (typeof v === 'object' && v instanceof Date) ? v : new Date(String(v))
145
143
  if (isNaN(date.getTime())) return String(props.value)
146
- const locale = props.field.locale || 'en-US'
147
144
  const format = props.field.dateFormat || 'short'
148
- return new Intl.DateTimeFormat(locale, { dateStyle: format as 'short' | 'medium' | 'long' | 'full' }).format(date)
145
+ return formatDate(date, { dateStyle: format as 'short' | 'medium' | 'long' | 'full' }, props.field.locale)
149
146
  })
150
147
 
151
148
  // Format datetime value
@@ -154,8 +151,7 @@ const datetimeValue = computed(() => {
154
151
  const v = props.value as unknown
155
152
  const date = (typeof v === 'object' && v instanceof Date) ? v : new Date(String(v))
156
153
  if (isNaN(date.getTime())) return String(props.value)
157
- const locale = props.field.locale || 'en-US'
158
- return new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short' }).format(date)
154
+ return formatDate(date, { dateStyle: 'short', timeStyle: 'short' }, props.field.locale)
159
155
  })
160
156
 
161
157
  // Format select value (lookup label from options)
@@ -15,12 +15,6 @@ export {
15
15
  type SemanticBreadcrumbItem,
16
16
  } from './useSemanticBreadcrumb'
17
17
  export { useDirtyState, type UseDirtyStateReturn } from './useDirtyState'
18
- export {
19
- useForm,
20
- type UseFormOptions,
21
- type UseFormReturn,
22
- type FormAlterConfig,
23
- } from './useForm'
24
18
  export {
25
19
  useEntityItemFormPage,
26
20
  useEntityItemFormPage as useFormPageBuilder,
@@ -132,7 +126,6 @@ export {
132
126
  type UseUserImpersonatorReturn,
133
127
  type UserRecord,
134
128
  } from './useUserImpersonator'
135
- export { useCurrentEntity, type UseCurrentEntityReturn } from './useCurrentEntity'
136
129
  export {
137
130
  useOptionsLookup,
138
131
  type UseOptionsLookupConfig,
@@ -35,6 +35,7 @@ import { useEntityItemPage, type ParentConfig, type UseEntityItemPageReturn } fr
35
35
  import { useActiveStack } from '../chain/useActiveStack.js'
36
36
  import { FilterQuery } from '../query/FilterQuery'
37
37
  import { useI18n } from '../i18n/useI18n'
38
+ import { formatDateOnly } from '../utils/formatters'
38
39
 
39
40
  // Import types from dedicated types file
40
41
  import type {
@@ -1307,18 +1308,12 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
1307
1308
  })
1308
1309
 
1309
1310
  // ============ UTILITIES ============
1311
+ // Browser-locale by default (unified policy — see utils/formatters).
1310
1312
  function formatDate(
1311
1313
  dateStr: string | null | undefined,
1312
1314
  options: Intl.DateTimeFormatOptions = {}
1313
1315
  ): string {
1314
- if (!dateStr) return '-'
1315
- const defaultOptions: Intl.DateTimeFormatOptions = {
1316
- day: '2-digit',
1317
- month: '2-digit',
1318
- year: 'numeric',
1319
- ...options,
1320
- }
1321
- return new Date(dateStr).toLocaleDateString('fr-FR', defaultOptions)
1316
+ return formatDateOnly(dateStr, options)
1322
1317
  }
1323
1318
 
1324
1319
  // ============ STANDARD ACTIONS ============
@@ -2,7 +2,7 @@
2
2
  * useNavContext - Route-aware navigation context for breadcrumb and navlinks
3
3
  *
4
4
  * Uses semantic breadcrumb as the source of truth for navigation structure.
5
- * Entity data comes from activeStack (set by useEntityItemPage/useForm).
5
+ * Entity data comes from activeStack (set by useEntityItemPage/useEntityItemFormPage).
6
6
  *
7
7
  * Semantic breadcrumb kinds:
8
8
  * - entity-list: Entity collection (e.g., /books)
@@ -2,7 +2,7 @@
2
2
  * usePageTitle - Provide custom page title for PageHeader
3
3
  *
4
4
  * Use this composable in custom pages to set the title displayed in PageHeader.
5
- * For standard CRUD pages, useForm handles this automatically.
5
+ * For standard CRUD pages, useEntityItemFormPage handles this automatically.
6
6
  *
7
7
  * Usage:
8
8
  * ```ts
@@ -77,7 +77,7 @@ export function usePageTitle(
77
77
  }
78
78
  }
79
79
 
80
- // Provide to PageHeader via same key as useForm
80
+ // Provide to PageHeader via same key as the form composables
81
81
  provide('qdadmPageTitleParts', titleParts)
82
82
 
83
83
  return { titleParts, setTitle }
@@ -137,7 +137,7 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
137
137
  const apiResponse = (await storage.request('GET', endpoint, {
138
138
  params: requestParams,
139
139
  context,
140
- })) as { data?: unknown }
140
+ })) as { data?: unknown; total?: number; pagination?: { total?: number } }
141
141
  // Normalize response
142
142
  const data = apiResponse.data ?? apiResponse
143
143
  const dataObj = data as {
@@ -150,9 +150,14 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
150
150
  items: Array.isArray(data)
151
151
  ? (data as any[])
152
152
  : dataObj.items || dataObj.data || [],
153
+ // Wrapper shapes like { data: [...], pagination: { total } } or
154
+ // { data: [...], total } carry the total on apiResponse, not on the
155
+ // unwrapped array — read both levels before falling back to page length.
153
156
  total:
154
157
  dataObj.total ??
155
158
  dataObj.pagination?.total ??
159
+ apiResponse.total ??
160
+ apiResponse.pagination?.total ??
156
161
  (Array.isArray(data) ? (data as any[]).length : 0),
157
162
  }
158
163
  } else {
@@ -57,11 +57,6 @@ export class ApiStorage<T extends EntityRecord = EntityRecord> extends IStorage<
57
57
  supportsCaching: true,
58
58
  }
59
59
 
60
- /** @deprecated Use static ApiStorage.capabilities.supportsCaching instead */
61
- get supportsCaching(): boolean {
62
- return ApiStorage.capabilities.supportsCaching
63
- }
64
-
65
60
  readonly endpoint: string
66
61
  readonly responseItemsKey: string
67
62
  readonly responseTotalKey: string
@@ -27,11 +27,6 @@ export class LocalStorage<T extends EntityRecord = EntityRecord> extends IStorag
27
27
  supportsCaching: false,
28
28
  }
29
29
 
30
- /** @deprecated Use static LocalStorage.capabilities.supportsCaching instead */
31
- get supportsCaching(): boolean {
32
- return LocalStorage.capabilities.supportsCaching
33
- }
34
-
35
30
  readonly key: string
36
31
  readonly idField: string
37
32
  readonly generateId: () => string
@@ -38,11 +38,6 @@ export class MemoryStorage<T extends EntityRecord = EntityRecord> extends IStora
38
38
  supportsCaching: false,
39
39
  }
40
40
 
41
- /** @deprecated Use static MemoryStorage.capabilities.supportsCaching instead */
42
- get supportsCaching(): boolean {
43
- return MemoryStorage.capabilities.supportsCaching
44
- }
45
-
46
41
  readonly idField: string
47
42
  readonly generateId: () => string
48
43
  protected _data: Map<string, T>
@@ -35,11 +35,6 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
35
35
  supportsCaching: false,
36
36
  }
37
37
 
38
- /** @deprecated Use static MockApiStorage.capabilities.supportsCaching instead */
39
- get supportsCaching(): boolean {
40
- return MockApiStorage.capabilities.supportsCaching
41
- }
42
-
43
38
  /**
44
39
  * Instance capabilities getter.
45
40
  * Merges static capabilities with instance-specific ones like requiresAuth.
@@ -91,11 +91,6 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
91
91
  supportsCaching: true,
92
92
  }
93
93
 
94
- /** @deprecated Use SdkStorage.capabilities.supportsCaching instead */
95
- get supportsCaching(): boolean {
96
- return SdkStorage.capabilities.supportsCaching
97
- }
98
-
99
94
  readonly clientSidePagination: boolean
100
95
 
101
96
  protected _sdk: SdkInstance | null
package/src/index.ts CHANGED
@@ -166,12 +166,6 @@ export {
166
166
  type SemanticBreadcrumbItem,
167
167
  } from './composables/useSemanticBreadcrumb'
168
168
  export { useDirtyState, type UseDirtyStateReturn } from './composables/useDirtyState'
169
- export {
170
- useForm,
171
- type UseFormOptions,
172
- type UseFormReturn,
173
- type FormAlterConfig,
174
- } from './composables/useForm'
175
169
  export {
176
170
  useEntityItemFormPage,
177
171
  useEntityItemFormPage as useFormPageBuilder,
@@ -292,7 +286,6 @@ export {
292
286
  type UseUserImpersonatorReturn,
293
287
  type UserRecord,
294
288
  } from './composables/useUserImpersonator'
295
- export { useCurrentEntity, type UseCurrentEntityReturn } from './composables/useCurrentEntity'
296
289
  export {
297
290
  useOptionsLookup,
298
291
  type UseOptionsLookupConfig,
@@ -16,22 +16,27 @@
16
16
  * Format a date string to localized date+time
17
17
  * Default format: toLocaleString() (e.g., "20/12/2025, 14:30:00")
18
18
  *
19
+ * The locale defaults to the browser locale everywhere in qdadm — pass
20
+ * `locale` only for an explicit per-field override.
21
+ *
19
22
  * @param dateStr - ISO date string or Date object
20
23
  * @param options - Intl.DateTimeFormat options
24
+ * @param locale - Explicit locale override (default: browser locale)
21
25
  * @returns Formatted date string or '-' if empty
22
26
  */
23
27
  export function formatDate(
24
28
  dateStr: string | Date | null | undefined,
25
- options: Intl.DateTimeFormatOptions = {}
29
+ options: Intl.DateTimeFormatOptions = {},
30
+ locale?: string
26
31
  ): string {
27
32
  if (!dateStr) return '-'
28
33
  const date = new Date(dateStr)
29
34
  if (isNaN(date.getTime())) return '-'
30
35
 
31
36
  if (Object.keys(options).length === 0) {
32
- return date.toLocaleString()
37
+ return date.toLocaleString(locale)
33
38
  }
34
- return date.toLocaleString(undefined, options)
39
+ return date.toLocaleString(locale, options)
35
40
  }
36
41
 
37
42
  /**
@@ -61,15 +66,23 @@ export function formatDateTimeShort(dateStr: string | Date | null | undefined):
61
66
 
62
67
  /**
63
68
  * Format to date only (20/12/2025)
69
+ *
70
+ * @param options - Intl.DateTimeFormat overrides merged over the defaults
71
+ * @param locale - Explicit locale override (default: browser locale)
64
72
  */
65
- export function formatDateOnly(dateStr: string | Date | null | undefined): string {
73
+ export function formatDateOnly(
74
+ dateStr: string | Date | null | undefined,
75
+ options: Intl.DateTimeFormatOptions = {},
76
+ locale?: string
77
+ ): string {
66
78
  if (!dateStr) return '-'
67
79
  const date = new Date(dateStr)
68
80
  if (isNaN(date.getTime())) return '-'
69
- return date.toLocaleDateString(undefined, {
81
+ return date.toLocaleDateString(locale, {
70
82
  year: 'numeric',
71
83
  month: '2-digit',
72
84
  day: '2-digit',
85
+ ...options,
73
86
  })
74
87
  }
75
88
 
@@ -142,13 +155,31 @@ export function formatDurationBetween(
142
155
 
143
156
  /**
144
157
  * Format a number with locale-specific separators
158
+ *
159
+ * @param locale - Explicit locale override (default: browser locale)
145
160
  */
146
161
  export function formatNumber(
147
162
  value: number | null | undefined,
148
- options: Intl.NumberFormatOptions = {}
163
+ options: Intl.NumberFormatOptions = {},
164
+ locale?: string
165
+ ): string {
166
+ if (value === null || value === undefined) return '-'
167
+ return value.toLocaleString(locale, options)
168
+ }
169
+
170
+ /**
171
+ * Format a currency amount (e.g., "$1,234.56")
172
+ *
173
+ * @param currencyCode - ISO 4217 code (default 'USD')
174
+ * @param locale - Explicit locale override (default: browser locale)
175
+ */
176
+ export function formatCurrency(
177
+ value: number | null | undefined,
178
+ currencyCode = 'USD',
179
+ locale?: string
149
180
  ): string {
150
181
  if (value === null || value === undefined) return '-'
151
- return value.toLocaleString(undefined, options)
182
+ return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode }).format(value)
152
183
  }
153
184
 
154
185
  /**
@@ -13,6 +13,7 @@ export {
13
13
  formatDuration,
14
14
  formatDurationBetween,
15
15
  formatNumber,
16
+ formatCurrency,
16
17
  formatBytes,
17
18
  formatPercent,
18
19
  } from './formatters'