mgv-backoffice 1.36.0 → 1.36.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.
Files changed (60) hide show
  1. package/README.md +1794 -1719
  2. package/dist/components/BaseBadge.vue.d.ts +1 -11
  3. package/dist/components/BaseDropdown.vue.d.ts +1 -1
  4. package/dist/ui-lib.js +942 -925
  5. package/dist/ui-lib.umd.cjs +1 -1
  6. package/dist/utils/specForm.d.ts +1 -1
  7. package/package.json +54 -54
  8. package/src/components/BaseAlert.vue +104 -104
  9. package/src/components/BaseBadge.vue +12 -2
  10. package/src/components/BaseBarDistribution.test.ts +51 -51
  11. package/src/components/BaseBarDistribution.vue +73 -73
  12. package/src/components/BaseChipButton.vue +42 -42
  13. package/src/components/BaseCodeBlock.test.ts +70 -70
  14. package/src/components/BaseCodeBlock.vue +74 -74
  15. package/src/components/BaseCollapsibleSection.vue +113 -113
  16. package/src/components/BaseConfirmModal.vue +114 -114
  17. package/src/components/BaseCredentialsForm.test.ts +108 -108
  18. package/src/components/BaseCredentialsForm.vue +241 -241
  19. package/src/components/BaseDropdown.test.ts +87 -87
  20. package/src/components/BaseDropdown.vue +172 -172
  21. package/src/components/BaseEntityPickerModal.vue +330 -330
  22. package/src/components/BaseFileDropzone.vue +94 -94
  23. package/src/components/BaseFilterChip.test.ts +44 -44
  24. package/src/components/BaseFilterChip.vue +67 -67
  25. package/src/components/BaseModalShell.vue +209 -209
  26. package/src/components/BasePageHeader.vue +120 -120
  27. package/src/components/BasePillPickerModal.test.ts +95 -95
  28. package/src/components/BaseRemoveButton.vue +22 -22
  29. package/src/components/BaseSpecFields.test.ts +39 -0
  30. package/src/components/BaseSpecFields.vue +27 -3
  31. package/src/components/BaseStatBreakdown.test.ts +56 -56
  32. package/src/components/BaseStatBreakdown.vue +49 -49
  33. package/src/components/BaseStatusPill.vue +54 -54
  34. package/src/components/BaseTextInputModal.vue +140 -140
  35. package/src/components/BaseToolbarButton.test.ts +48 -48
  36. package/src/components/BaseToolbarButton.vue +94 -94
  37. package/src/composables/useFieldClasses.test.ts +55 -55
  38. package/src/composables/useFieldClasses.ts +56 -56
  39. package/src/composables/usePolling.test.ts +112 -112
  40. package/src/composables/usePolling.ts +102 -102
  41. package/src/composables/useQueryParamSync.test.ts +56 -56
  42. package/src/composables/useQueryParamSync.ts +42 -42
  43. package/src/composables/useThemeClasses.test.ts +34 -34
  44. package/src/composables/useThemeClasses.ts +137 -137
  45. package/src/index.ts +138 -138
  46. package/src/style.css +32 -32
  47. package/src/types/distribution.ts +7 -7
  48. package/src/types/pillPicker.ts +14 -14
  49. package/src/types/specField.ts +30 -30
  50. package/src/types/statBreakdown.ts +6 -6
  51. package/src/utils/format.test.ts +264 -264
  52. package/src/utils/format.ts +216 -216
  53. package/src/utils/httpColors.test.ts +100 -100
  54. package/src/utils/httpColors.ts +99 -99
  55. package/src/utils/kvRows.test.ts +47 -47
  56. package/src/utils/kvRows.ts +34 -34
  57. package/src/utils/specForm.test.ts +64 -53
  58. package/src/utils/specForm.ts +49 -48
  59. package/src/utils/validate.test.ts +82 -82
  60. package/src/utils/validate.ts +76 -76
@@ -1,114 +1,114 @@
1
- <template>
2
- <BaseModalShell
3
- :title="title"
4
- :icon="ExclamationTriangleIcon"
5
- :icon-bg-class="variant === 'danger'
6
- ? isDark ? 'bg-red-900/30' : 'bg-red-50'
7
- : isDark ? 'bg-amber-900/30' : 'bg-amber-50'"
8
- :icon-class="variant === 'danger' ? 'text-red-600' : 'text-amber-600'"
9
- :manual-close="submitting"
10
- @cancel="handleCancel"
11
- >
12
- <!--
13
- Default renders the plain `message` string. Callers that need rich
14
- text (bold names, inline code, etc.) can override the `message` slot
15
- and render their own markup — the `message` prop is still used as the
16
- accessible/fallback text and by callers that don't override.
17
- -->
18
- <slot name="message">
19
- <p class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
20
- {{ message }}
21
- </p>
22
- </slot>
23
-
24
- <!--
25
- Optional extra content rendered below the message — e.g. a warning
26
- banner or an opt-in checkbox the caller wants the user to see before
27
- confirming. Empty for the common confirm/cancel case.
28
- -->
29
- <slot />
30
-
31
- <template #footer>
32
- <button
33
- type="button"
34
- :disabled="submitting"
35
- class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
36
- :class="[
37
- t.ghostButton,
38
- submitting ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
39
- ]"
40
- @click="handleCancel"
41
- >
42
- {{ cancelText }}
43
- </button>
44
-
45
- <button
46
- type="button"
47
- :disabled="submitting"
48
- class="inline-flex items-center justify-center gap-2 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
49
- :class="[
50
- variant === 'danger' ? 'focus-visible:ring-red-500' : 'focus-visible:ring-emerald-500',
51
- variant === 'danger'
52
- ? submitting ? 'bg-red-400 cursor-not-allowed' : 'bg-red-600 hover:bg-red-700 cursor-pointer'
53
- : submitting ? 'bg-emerald-400 cursor-not-allowed' : 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer',
54
- ]"
55
- @click="handleConfirm"
56
- >
57
- <svg
58
- v-if="submitting"
59
- class="w-4 h-4 animate-spin"
60
- fill="none"
61
- viewBox="0 0 24 24"
62
- aria-hidden="true"
63
- >
64
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
65
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
66
- </svg>
67
- {{ submitting ? submittingText : confirmText }}
68
- </button>
69
- </template>
70
- </BaseModalShell>
71
- </template>
72
-
73
- <script setup lang="ts">
74
- import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
75
- import BaseModalShell from './BaseModalShell.vue'
76
- import { useTheme } from '../composables/useTheme'
77
- import { useThemeClasses } from '../composables/useThemeClasses'
78
-
79
- interface Props {
80
- title: string
81
- message: string
82
- confirmText?: string
83
- cancelText?: string
84
- submittingText?: string
85
- variant?: 'danger' | 'warning'
86
- submitting?: boolean
87
- }
88
-
89
- const props = withDefaults(defineProps<Props>(), {
90
- confirmText: "Yes, I'm sure",
91
- cancelText: 'No, cancel',
92
- submittingText: 'Working...',
93
- variant: 'danger',
94
- submitting: false,
95
- })
96
-
97
- const emit = defineEmits<{
98
- confirm: []
99
- cancel: []
100
- }>()
101
-
102
- const { isDark } = useTheme()
103
- const t = useThemeClasses()
104
-
105
- function handleConfirm() {
106
- if (props.submitting) return
107
- emit('confirm')
108
- }
109
-
110
- function handleCancel() {
111
- if (props.submitting) return
112
- emit('cancel')
113
- }
114
- </script>
1
+ <template>
2
+ <BaseModalShell
3
+ :title="title"
4
+ :icon="ExclamationTriangleIcon"
5
+ :icon-bg-class="variant === 'danger'
6
+ ? isDark ? 'bg-red-900/30' : 'bg-red-50'
7
+ : isDark ? 'bg-amber-900/30' : 'bg-amber-50'"
8
+ :icon-class="variant === 'danger' ? 'text-red-600' : 'text-amber-600'"
9
+ :manual-close="submitting"
10
+ @cancel="handleCancel"
11
+ >
12
+ <!--
13
+ Default renders the plain `message` string. Callers that need rich
14
+ text (bold names, inline code, etc.) can override the `message` slot
15
+ and render their own markup — the `message` prop is still used as the
16
+ accessible/fallback text and by callers that don't override.
17
+ -->
18
+ <slot name="message">
19
+ <p class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
20
+ {{ message }}
21
+ </p>
22
+ </slot>
23
+
24
+ <!--
25
+ Optional extra content rendered below the message — e.g. a warning
26
+ banner or an opt-in checkbox the caller wants the user to see before
27
+ confirming. Empty for the common confirm/cancel case.
28
+ -->
29
+ <slot />
30
+
31
+ <template #footer>
32
+ <button
33
+ type="button"
34
+ :disabled="submitting"
35
+ class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
36
+ :class="[
37
+ t.ghostButton,
38
+ submitting ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
39
+ ]"
40
+ @click="handleCancel"
41
+ >
42
+ {{ cancelText }}
43
+ </button>
44
+
45
+ <button
46
+ type="button"
47
+ :disabled="submitting"
48
+ class="inline-flex items-center justify-center gap-2 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
49
+ :class="[
50
+ variant === 'danger' ? 'focus-visible:ring-red-500' : 'focus-visible:ring-emerald-500',
51
+ variant === 'danger'
52
+ ? submitting ? 'bg-red-400 cursor-not-allowed' : 'bg-red-600 hover:bg-red-700 cursor-pointer'
53
+ : submitting ? 'bg-emerald-400 cursor-not-allowed' : 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer',
54
+ ]"
55
+ @click="handleConfirm"
56
+ >
57
+ <svg
58
+ v-if="submitting"
59
+ class="w-4 h-4 animate-spin"
60
+ fill="none"
61
+ viewBox="0 0 24 24"
62
+ aria-hidden="true"
63
+ >
64
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
65
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
66
+ </svg>
67
+ {{ submitting ? submittingText : confirmText }}
68
+ </button>
69
+ </template>
70
+ </BaseModalShell>
71
+ </template>
72
+
73
+ <script setup lang="ts">
74
+ import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
75
+ import BaseModalShell from './BaseModalShell.vue'
76
+ import { useTheme } from '../composables/useTheme'
77
+ import { useThemeClasses } from '../composables/useThemeClasses'
78
+
79
+ interface Props {
80
+ title: string
81
+ message: string
82
+ confirmText?: string
83
+ cancelText?: string
84
+ submittingText?: string
85
+ variant?: 'danger' | 'warning'
86
+ submitting?: boolean
87
+ }
88
+
89
+ const props = withDefaults(defineProps<Props>(), {
90
+ confirmText: "Yes, I'm sure",
91
+ cancelText: 'No, cancel',
92
+ submittingText: 'Working...',
93
+ variant: 'danger',
94
+ submitting: false,
95
+ })
96
+
97
+ const emit = defineEmits<{
98
+ confirm: []
99
+ cancel: []
100
+ }>()
101
+
102
+ const { isDark } = useTheme()
103
+ const t = useThemeClasses()
104
+
105
+ function handleConfirm() {
106
+ if (props.submitting) return
107
+ emit('confirm')
108
+ }
109
+
110
+ function handleCancel() {
111
+ if (props.submitting) return
112
+ emit('cancel')
113
+ }
114
+ </script>
@@ -1,108 +1,108 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { flushPromises, mount } from '@vue/test-utils'
3
- import BaseCredentialsForm from './BaseCredentialsForm.vue'
4
- import type { CredentialsView } from './BaseCredentialsForm.vue'
5
-
6
- const view = (over: Partial<CredentialsView> = {}): CredentialsView => ({
7
- keyId: 'PK123',
8
- baseUrl: 'https://api.example.com',
9
- dataUrl: 'https://data.example.com',
10
- hasSecret: true,
11
- ...over,
12
- })
13
-
14
- const defaults = { baseUrl: 'https://default.base', dataUrl: 'https://default.data' }
15
-
16
- function mountCard(fetchFn = vi.fn().mockResolvedValue(view()), updateFn = vi.fn()) {
17
- const wrapper = mount(BaseCredentialsForm, {
18
- props: { title: 'Alpaca API', idPrefix: 'alpaca', fetchFn, updateFn, defaults },
19
- })
20
- return { wrapper, fetchFn, updateFn }
21
- }
22
-
23
- beforeEach(() => {
24
- localStorage.clear()
25
- })
26
-
27
- describe('BaseCredentialsForm', () => {
28
- it('loads on mount and fills the form from the fetched view', async () => {
29
- const { wrapper, fetchFn } = mountCard()
30
- await flushPromises()
31
- expect(fetchFn).toHaveBeenCalledTimes(1)
32
- expect((wrapper.get('#alpaca-key-id').element as HTMLInputElement).value).toBe('PK123')
33
- expect((wrapper.get('#alpaca-base-url').element as HTMLInputElement).value).toBe(
34
- 'https://api.example.com',
35
- )
36
- })
37
-
38
- it('falls back to the defaults when the server has no URLs stored', async () => {
39
- const { wrapper } = mountCard(
40
- vi.fn().mockResolvedValue(view({ keyId: null, baseUrl: '', dataUrl: '', hasSecret: false })),
41
- )
42
- await flushPromises()
43
- expect((wrapper.get('#alpaca-base-url').element as HTMLInputElement).value).toBe(
44
- defaults.baseUrl,
45
- )
46
- expect((wrapper.get('#alpaca-data-url').element as HTMLInputElement).value).toBe(
47
- defaults.dataUrl,
48
- )
49
- })
50
-
51
- it('emits load-error when the fetch rejects', async () => {
52
- const { wrapper } = mountCard(vi.fn().mockRejectedValue(new Error('boom')))
53
- await flushPromises()
54
- expect(wrapper.emitted('load-error')).toEqual([['boom']])
55
- })
56
-
57
- it('refuses to save without a key and emits the configured message', async () => {
58
- const { wrapper, updateFn } = mountCard(
59
- vi.fn().mockResolvedValue(view({ keyId: null, hasSecret: false })),
60
- )
61
- await flushPromises()
62
- await wrapper.get('button').trigger('click')
63
- expect(updateFn).not.toHaveBeenCalled()
64
- expect(wrapper.emitted('error')?.[0]).toEqual(['API key is required.'])
65
- })
66
-
67
- it('requires a secret only while none is stored', async () => {
68
- const { wrapper, updateFn } = mountCard(
69
- vi.fn().mockResolvedValue(view({ hasSecret: false })),
70
- )
71
- await flushPromises()
72
- await wrapper.get('button').trigger('click')
73
- expect(updateFn).not.toHaveBeenCalled()
74
- expect(wrapper.emitted('error')?.[0]).toEqual(['Secret key is required.'])
75
- })
76
-
77
- it('saves trimmed values, omits a blank secret, and emits saved', async () => {
78
- const updateFn = vi.fn().mockResolvedValue(view())
79
- const { wrapper } = mountCard(vi.fn().mockResolvedValue(view()), updateFn)
80
- await flushPromises()
81
- await wrapper.get('#alpaca-key-id').setValue(' PK999 ')
82
- await wrapper.get('button').trigger('click')
83
- await flushPromises()
84
- expect(updateFn).toHaveBeenCalledWith({
85
- keyId: 'PK999',
86
- secretKey: undefined,
87
- baseUrl: 'https://api.example.com',
88
- dataUrl: 'https://data.example.com',
89
- })
90
- expect(wrapper.emitted('saved')).toEqual([['Credentials saved.']])
91
- })
92
-
93
- it('emits error with the server message when the update rejects', async () => {
94
- const updateFn = vi.fn().mockRejectedValue(new Error('validation failed'))
95
- const { wrapper } = mountCard(vi.fn().mockResolvedValue(view()), updateFn)
96
- await flushPromises()
97
- await wrapper.get('button').trigger('click')
98
- await flushPromises()
99
- expect(wrapper.emitted('error')?.[0]).toEqual(['validation failed'])
100
- })
101
-
102
- it('exposes load() so a parent Reload can re-pull the card', async () => {
103
- const { wrapper, fetchFn } = mountCard()
104
- await flushPromises()
105
- await (wrapper.vm as unknown as { load: () => Promise<void> }).load()
106
- expect(fetchFn).toHaveBeenCalledTimes(2)
107
- })
108
- })
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import BaseCredentialsForm from './BaseCredentialsForm.vue'
4
+ import type { CredentialsView } from './BaseCredentialsForm.vue'
5
+
6
+ const view = (over: Partial<CredentialsView> = {}): CredentialsView => ({
7
+ keyId: 'PK123',
8
+ baseUrl: 'https://api.example.com',
9
+ dataUrl: 'https://data.example.com',
10
+ hasSecret: true,
11
+ ...over,
12
+ })
13
+
14
+ const defaults = { baseUrl: 'https://default.base', dataUrl: 'https://default.data' }
15
+
16
+ function mountCard(fetchFn = vi.fn().mockResolvedValue(view()), updateFn = vi.fn()) {
17
+ const wrapper = mount(BaseCredentialsForm, {
18
+ props: { title: 'Alpaca API', idPrefix: 'alpaca', fetchFn, updateFn, defaults },
19
+ })
20
+ return { wrapper, fetchFn, updateFn }
21
+ }
22
+
23
+ beforeEach(() => {
24
+ localStorage.clear()
25
+ })
26
+
27
+ describe('BaseCredentialsForm', () => {
28
+ it('loads on mount and fills the form from the fetched view', async () => {
29
+ const { wrapper, fetchFn } = mountCard()
30
+ await flushPromises()
31
+ expect(fetchFn).toHaveBeenCalledTimes(1)
32
+ expect((wrapper.get('#alpaca-key-id').element as HTMLInputElement).value).toBe('PK123')
33
+ expect((wrapper.get('#alpaca-base-url').element as HTMLInputElement).value).toBe(
34
+ 'https://api.example.com',
35
+ )
36
+ })
37
+
38
+ it('falls back to the defaults when the server has no URLs stored', async () => {
39
+ const { wrapper } = mountCard(
40
+ vi.fn().mockResolvedValue(view({ keyId: null, baseUrl: '', dataUrl: '', hasSecret: false })),
41
+ )
42
+ await flushPromises()
43
+ expect((wrapper.get('#alpaca-base-url').element as HTMLInputElement).value).toBe(
44
+ defaults.baseUrl,
45
+ )
46
+ expect((wrapper.get('#alpaca-data-url').element as HTMLInputElement).value).toBe(
47
+ defaults.dataUrl,
48
+ )
49
+ })
50
+
51
+ it('emits load-error when the fetch rejects', async () => {
52
+ const { wrapper } = mountCard(vi.fn().mockRejectedValue(new Error('boom')))
53
+ await flushPromises()
54
+ expect(wrapper.emitted('load-error')).toEqual([['boom']])
55
+ })
56
+
57
+ it('refuses to save without a key and emits the configured message', async () => {
58
+ const { wrapper, updateFn } = mountCard(
59
+ vi.fn().mockResolvedValue(view({ keyId: null, hasSecret: false })),
60
+ )
61
+ await flushPromises()
62
+ await wrapper.get('button').trigger('click')
63
+ expect(updateFn).not.toHaveBeenCalled()
64
+ expect(wrapper.emitted('error')?.[0]).toEqual(['API key is required.'])
65
+ })
66
+
67
+ it('requires a secret only while none is stored', async () => {
68
+ const { wrapper, updateFn } = mountCard(
69
+ vi.fn().mockResolvedValue(view({ hasSecret: false })),
70
+ )
71
+ await flushPromises()
72
+ await wrapper.get('button').trigger('click')
73
+ expect(updateFn).not.toHaveBeenCalled()
74
+ expect(wrapper.emitted('error')?.[0]).toEqual(['Secret key is required.'])
75
+ })
76
+
77
+ it('saves trimmed values, omits a blank secret, and emits saved', async () => {
78
+ const updateFn = vi.fn().mockResolvedValue(view())
79
+ const { wrapper } = mountCard(vi.fn().mockResolvedValue(view()), updateFn)
80
+ await flushPromises()
81
+ await wrapper.get('#alpaca-key-id').setValue(' PK999 ')
82
+ await wrapper.get('button').trigger('click')
83
+ await flushPromises()
84
+ expect(updateFn).toHaveBeenCalledWith({
85
+ keyId: 'PK999',
86
+ secretKey: undefined,
87
+ baseUrl: 'https://api.example.com',
88
+ dataUrl: 'https://data.example.com',
89
+ })
90
+ expect(wrapper.emitted('saved')).toEqual([['Credentials saved.']])
91
+ })
92
+
93
+ it('emits error with the server message when the update rejects', async () => {
94
+ const updateFn = vi.fn().mockRejectedValue(new Error('validation failed'))
95
+ const { wrapper } = mountCard(vi.fn().mockResolvedValue(view()), updateFn)
96
+ await flushPromises()
97
+ await wrapper.get('button').trigger('click')
98
+ await flushPromises()
99
+ expect(wrapper.emitted('error')?.[0]).toEqual(['validation failed'])
100
+ })
101
+
102
+ it('exposes load() so a parent Reload can re-pull the card', async () => {
103
+ const { wrapper, fetchFn } = mountCard()
104
+ await flushPromises()
105
+ await (wrapper.vm as unknown as { load: () => Promise<void> }).load()
106
+ expect(fetchFn).toHaveBeenCalledTimes(2)
107
+ })
108
+ })