mgv-backoffice 1.35.0 → 1.36.1

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 (62) hide show
  1. package/README.md +1794 -1686
  2. package/dist/components/BaseBarDistribution.vue.d.ts +24 -0
  3. package/dist/components/BasePageHeader.vue.d.ts +8 -2
  4. package/dist/index.d.ts +2 -0
  5. package/dist/types/distribution.d.ts +7 -0
  6. package/dist/ui-lib.css +1 -1
  7. package/dist/ui-lib.js +634 -574
  8. package/dist/ui-lib.umd.cjs +1 -1
  9. package/dist/utils/specForm.d.ts +1 -1
  10. package/package.json +54 -54
  11. package/src/components/BaseAlert.vue +104 -104
  12. package/src/components/BaseBarDistribution.test.ts +51 -0
  13. package/src/components/BaseBarDistribution.vue +73 -0
  14. package/src/components/BaseChipButton.vue +42 -42
  15. package/src/components/BaseCodeBlock.test.ts +70 -70
  16. package/src/components/BaseCodeBlock.vue +74 -74
  17. package/src/components/BaseCollapsibleSection.vue +113 -113
  18. package/src/components/BaseConfirmModal.vue +114 -114
  19. package/src/components/BaseCredentialsForm.test.ts +108 -108
  20. package/src/components/BaseCredentialsForm.vue +241 -241
  21. package/src/components/BaseDropdown.test.ts +87 -87
  22. package/src/components/BaseDropdown.vue +172 -172
  23. package/src/components/BaseEntityPickerModal.vue +330 -330
  24. package/src/components/BaseFileDropzone.vue +94 -94
  25. package/src/components/BaseFilterChip.test.ts +44 -44
  26. package/src/components/BaseFilterChip.vue +67 -67
  27. package/src/components/BaseModalShell.vue +209 -209
  28. package/src/components/BasePageHeader.vue +120 -103
  29. package/src/components/BasePillPickerModal.test.ts +95 -95
  30. package/src/components/BaseRemoveButton.vue +22 -22
  31. package/src/components/BaseSpecFields.test.ts +39 -0
  32. package/src/components/BaseSpecFields.vue +27 -3
  33. package/src/components/BaseStatBreakdown.test.ts +56 -56
  34. package/src/components/BaseStatBreakdown.vue +49 -49
  35. package/src/components/BaseStatusPill.vue +54 -54
  36. package/src/components/BaseTextInputModal.vue +140 -140
  37. package/src/components/BaseToolbarButton.test.ts +48 -48
  38. package/src/components/BaseToolbarButton.vue +94 -94
  39. package/src/composables/useFieldClasses.test.ts +55 -55
  40. package/src/composables/useFieldClasses.ts +56 -56
  41. package/src/composables/usePolling.test.ts +112 -112
  42. package/src/composables/usePolling.ts +102 -90
  43. package/src/composables/useQueryParamSync.test.ts +56 -56
  44. package/src/composables/useQueryParamSync.ts +42 -42
  45. package/src/composables/useThemeClasses.test.ts +34 -34
  46. package/src/composables/useThemeClasses.ts +137 -137
  47. package/src/index.ts +138 -136
  48. package/src/style.css +32 -32
  49. package/src/types/distribution.ts +7 -0
  50. package/src/types/pillPicker.ts +14 -14
  51. package/src/types/specField.ts +30 -30
  52. package/src/types/statBreakdown.ts +6 -6
  53. package/src/utils/format.test.ts +264 -264
  54. package/src/utils/format.ts +216 -216
  55. package/src/utils/httpColors.test.ts +100 -100
  56. package/src/utils/httpColors.ts +99 -99
  57. package/src/utils/kvRows.test.ts +47 -47
  58. package/src/utils/kvRows.ts +34 -34
  59. package/src/utils/specForm.test.ts +64 -53
  60. package/src/utils/specForm.ts +49 -48
  61. package/src/utils/validate.test.ts +82 -82
  62. 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
+ })