qdadm 0.13.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/CHANGELOG.md +270 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +48 -0
- package/src/assets/logo.svg +6 -0
- package/src/components/BoolCell.vue +28 -0
- package/src/components/dialogs/BulkStatusDialog.vue +43 -0
- package/src/components/dialogs/MultiStepDialog.vue +321 -0
- package/src/components/dialogs/SimpleDialog.vue +108 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
- package/src/components/display/CardsGrid.vue +155 -0
- package/src/components/display/CopyableId.vue +92 -0
- package/src/components/display/EmptyState.vue +114 -0
- package/src/components/display/IntensityBar.vue +171 -0
- package/src/components/display/RichCardsGrid.vue +220 -0
- package/src/components/editors/JsonEditorFoldable.vue +467 -0
- package/src/components/editors/JsonStructuredField.vue +218 -0
- package/src/components/editors/JsonViewer.vue +91 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +245 -0
- package/src/components/editors/ScopeEditor.vue +341 -0
- package/src/components/editors/VanillaJsonEditor.vue +185 -0
- package/src/components/forms/FormActions.vue +104 -0
- package/src/components/forms/FormField.vue +64 -0
- package/src/components/forms/FormTab.vue +217 -0
- package/src/components/forms/FormTabs.vue +108 -0
- package/src/components/index.js +44 -0
- package/src/components/layout/AppLayout.vue +430 -0
- package/src/components/layout/Breadcrumb.vue +106 -0
- package/src/components/layout/PageHeader.vue +75 -0
- package/src/components/layout/PageLayout.vue +93 -0
- package/src/components/lists/ActionButtons.vue +41 -0
- package/src/components/lists/ActionColumn.vue +37 -0
- package/src/components/lists/FilterBar.vue +53 -0
- package/src/components/lists/ListPage.vue +319 -0
- package/src/composables/index.js +19 -0
- package/src/composables/useApp.js +43 -0
- package/src/composables/useAuth.js +49 -0
- package/src/composables/useBareForm.js +143 -0
- package/src/composables/useBreadcrumb.js +221 -0
- package/src/composables/useDirtyState.js +103 -0
- package/src/composables/useEntityTitle.js +121 -0
- package/src/composables/useForm.js +254 -0
- package/src/composables/useGuardStore.js +37 -0
- package/src/composables/useJsonSyntax.js +101 -0
- package/src/composables/useListPageBuilder.js +1176 -0
- package/src/composables/useNavigation.js +89 -0
- package/src/composables/usePageBuilder.js +334 -0
- package/src/composables/useStatus.js +146 -0
- package/src/composables/useSubEditor.js +165 -0
- package/src/composables/useTabSync.js +110 -0
- package/src/composables/useUnsavedChangesGuard.js +122 -0
- package/src/entity/EntityManager.js +540 -0
- package/src/entity/index.js +11 -0
- package/src/entity/storage/ApiStorage.js +146 -0
- package/src/entity/storage/LocalStorage.js +220 -0
- package/src/entity/storage/MemoryStorage.js +201 -0
- package/src/entity/storage/index.js +10 -0
- package/src/index.js +29 -0
- package/src/kernel/Kernel.js +234 -0
- package/src/kernel/index.js +7 -0
- package/src/module/index.js +16 -0
- package/src/module/moduleRegistry.js +222 -0
- package/src/orchestrator/Orchestrator.js +141 -0
- package/src/orchestrator/index.js +8 -0
- package/src/orchestrator/useOrchestrator.js +61 -0
- package/src/plugin.js +142 -0
- package/src/styles/_alerts.css +48 -0
- package/src/styles/_code.css +33 -0
- package/src/styles/_dialogs.css +17 -0
- package/src/styles/_markdown.css +82 -0
- package/src/styles/_show-pages.css +84 -0
- package/src/styles/index.css +16 -0
- package/src/styles/main.css +845 -0
- package/src/styles/theme/components.css +286 -0
- package/src/styles/theme/index.css +10 -0
- package/src/styles/theme/tokens.css +125 -0
- package/src/styles/theme/utilities.css +172 -0
- package/src/utils/debugInjector.js +261 -0
- package/src/utils/formatters.js +165 -0
- package/src/utils/index.js +35 -0
- package/src/utils/transformers.js +105 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* MultiStepDialog - Reusable multi-step form dialog
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Step indicator with labels
|
|
7
|
+
* - Automatic navigation buttons (Cancel, Back, Next, Submit)
|
|
8
|
+
* - Per-step validation
|
|
9
|
+
* - Linear progression mode
|
|
10
|
+
* - Loading state for submit
|
|
11
|
+
* - Customizable button labels
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* <MultiStepDialog
|
|
15
|
+
* v-model:visible="showDialog"
|
|
16
|
+
* v-model:step="currentStep"
|
|
17
|
+
* title="Create Item"
|
|
18
|
+
* :steps="[
|
|
19
|
+
* { label: 'Step 1', valid: !!field1 },
|
|
20
|
+
* { label: 'Step 2', valid: canSubmit }
|
|
21
|
+
* ]"
|
|
22
|
+
* :loading="submitting"
|
|
23
|
+
* @submit="handleSubmit"
|
|
24
|
+
* @cancel="handleCancel"
|
|
25
|
+
* >
|
|
26
|
+
* <template #step-1>
|
|
27
|
+
* <!-- Step 1 content -->
|
|
28
|
+
* </template>
|
|
29
|
+
* <template #step-2>
|
|
30
|
+
* <!-- Step 2 content -->
|
|
31
|
+
* </template>
|
|
32
|
+
* </MultiStepDialog>
|
|
33
|
+
*
|
|
34
|
+
* Props:
|
|
35
|
+
* - visible: boolean (v-model) - controls dialog visibility
|
|
36
|
+
* - step: number (v-model) - current step (1-indexed)
|
|
37
|
+
* - title: string - dialog title
|
|
38
|
+
* - steps: Array<{ label: string, valid?: boolean }> - step definitions
|
|
39
|
+
* - loading: boolean - shows loading state on submit button
|
|
40
|
+
* - width: string - dialog width (default: '800px')
|
|
41
|
+
* - linear: boolean - require validation before next step (default: true)
|
|
42
|
+
* - submitLabel: string - label for submit button (default: 'Submit')
|
|
43
|
+
* - cancelLabel: string - label for cancel button (default: 'Cancel')
|
|
44
|
+
* - nextLabel: string - label for next button (default: 'Next')
|
|
45
|
+
* - backLabel: string - label for back button (default: 'Back')
|
|
46
|
+
* - showBackOnFirst: boolean - show back button on first step (default: false)
|
|
47
|
+
* - hideStepIndicator: boolean - hide the step indicator (default: false)
|
|
48
|
+
*
|
|
49
|
+
* Events:
|
|
50
|
+
* - submit: emitted when submit button is clicked on last step
|
|
51
|
+
* - cancel: emitted when cancel button is clicked
|
|
52
|
+
* - step-change: emitted when step changes, payload: { from, to }
|
|
53
|
+
*
|
|
54
|
+
* Slots:
|
|
55
|
+
* - step-{n}: content for step n (1-indexed)
|
|
56
|
+
* - step-{n}-header: optional header content for step n (shown above step content)
|
|
57
|
+
* - actions: override action buttons (receives { step, isFirst, isLast, canProceed, goNext, goPrev, submit, cancel })
|
|
58
|
+
*/
|
|
59
|
+
import { computed, watch } from 'vue'
|
|
60
|
+
import Dialog from 'primevue/dialog'
|
|
61
|
+
import Button from 'primevue/button'
|
|
62
|
+
import Stepper from 'primevue/stepper'
|
|
63
|
+
import StepList from 'primevue/steplist'
|
|
64
|
+
import Step from 'primevue/step'
|
|
65
|
+
import StepPanels from 'primevue/steppanels'
|
|
66
|
+
import StepPanel from 'primevue/steppanel'
|
|
67
|
+
|
|
68
|
+
const props = defineProps({
|
|
69
|
+
visible: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
default: false
|
|
72
|
+
},
|
|
73
|
+
step: {
|
|
74
|
+
type: Number,
|
|
75
|
+
default: 1
|
|
76
|
+
},
|
|
77
|
+
title: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: ''
|
|
80
|
+
},
|
|
81
|
+
steps: {
|
|
82
|
+
type: Array,
|
|
83
|
+
required: true,
|
|
84
|
+
validator: (v) => v.every(s => typeof s.label === 'string')
|
|
85
|
+
},
|
|
86
|
+
loading: {
|
|
87
|
+
type: Boolean,
|
|
88
|
+
default: false
|
|
89
|
+
},
|
|
90
|
+
width: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: '800px'
|
|
93
|
+
},
|
|
94
|
+
linear: {
|
|
95
|
+
type: Boolean,
|
|
96
|
+
default: true
|
|
97
|
+
},
|
|
98
|
+
submitLabel: {
|
|
99
|
+
type: String,
|
|
100
|
+
default: 'Submit'
|
|
101
|
+
},
|
|
102
|
+
cancelLabel: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: 'Cancel'
|
|
105
|
+
},
|
|
106
|
+
nextLabel: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: 'Next'
|
|
109
|
+
},
|
|
110
|
+
backLabel: {
|
|
111
|
+
type: String,
|
|
112
|
+
default: 'Back'
|
|
113
|
+
},
|
|
114
|
+
showBackOnFirst: {
|
|
115
|
+
type: Boolean,
|
|
116
|
+
default: false
|
|
117
|
+
},
|
|
118
|
+
hideStepIndicator: {
|
|
119
|
+
type: Boolean,
|
|
120
|
+
default: false
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const emit = defineEmits(['update:visible', 'update:step', 'submit', 'cancel', 'step-change'])
|
|
125
|
+
|
|
126
|
+
// Current step (1-indexed)
|
|
127
|
+
const currentStep = computed({
|
|
128
|
+
get: () => props.step,
|
|
129
|
+
set: (v) => emit('update:step', v)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Step helpers
|
|
133
|
+
const isFirstStep = computed(() => currentStep.value === 1)
|
|
134
|
+
const isLastStep = computed(() => currentStep.value === props.steps.length)
|
|
135
|
+
|
|
136
|
+
// Current step definition
|
|
137
|
+
const currentStepDef = computed(() => props.steps[currentStep.value - 1] || {})
|
|
138
|
+
|
|
139
|
+
// Can proceed to next step (validation)
|
|
140
|
+
const canProceed = computed(() => {
|
|
141
|
+
if (!props.linear) return true
|
|
142
|
+
return currentStepDef.value.valid !== false
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Can submit (last step validation)
|
|
146
|
+
const canSubmit = computed(() => {
|
|
147
|
+
return currentStepDef.value.valid !== false
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Navigation
|
|
151
|
+
function goNext() {
|
|
152
|
+
if (!isLastStep.value && canProceed.value) {
|
|
153
|
+
const from = currentStep.value
|
|
154
|
+
const to = currentStep.value + 1
|
|
155
|
+
currentStep.value = to
|
|
156
|
+
emit('step-change', { from, to })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function goPrev() {
|
|
161
|
+
if (!isFirstStep.value) {
|
|
162
|
+
const from = currentStep.value
|
|
163
|
+
const to = currentStep.value - 1
|
|
164
|
+
currentStep.value = to
|
|
165
|
+
emit('step-change', { from, to })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function goToStep(stepNumber) {
|
|
170
|
+
if (stepNumber >= 1 && stepNumber <= props.steps.length) {
|
|
171
|
+
// In linear mode, can only go back or to current+1 if valid
|
|
172
|
+
if (props.linear && stepNumber > currentStep.value) {
|
|
173
|
+
if (stepNumber > currentStep.value + 1 || !canProceed.value) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const from = currentStep.value
|
|
178
|
+
currentStep.value = stepNumber
|
|
179
|
+
emit('step-change', { from, to: stepNumber })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function submit() {
|
|
184
|
+
if (canSubmit.value) {
|
|
185
|
+
emit('submit')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function cancel() {
|
|
190
|
+
emit('cancel')
|
|
191
|
+
emit('update:visible', false)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Reset to first step when dialog opens
|
|
195
|
+
watch(() => props.visible, (newVal, oldVal) => {
|
|
196
|
+
if (newVal && !oldVal) {
|
|
197
|
+
// Don't reset if step is explicitly set
|
|
198
|
+
// currentStep.value = 1
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Expose for slot actions
|
|
203
|
+
const slotActions = computed(() => ({
|
|
204
|
+
step: currentStep.value,
|
|
205
|
+
isFirst: isFirstStep.value,
|
|
206
|
+
isLast: isLastStep.value,
|
|
207
|
+
canProceed: canProceed.value,
|
|
208
|
+
canSubmit: canSubmit.value,
|
|
209
|
+
goNext,
|
|
210
|
+
goPrev,
|
|
211
|
+
goToStep,
|
|
212
|
+
submit,
|
|
213
|
+
cancel
|
|
214
|
+
}))
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<Dialog
|
|
219
|
+
:visible="props.visible"
|
|
220
|
+
@update:visible="emit('update:visible', $event)"
|
|
221
|
+
:header="title"
|
|
222
|
+
:style="{ width: width }"
|
|
223
|
+
:modal="true"
|
|
224
|
+
:closable="!loading"
|
|
225
|
+
:closeOnEscape="!loading"
|
|
226
|
+
>
|
|
227
|
+
<div class="multi-step-dialog">
|
|
228
|
+
<Stepper v-model:value="currentStep" :linear="linear">
|
|
229
|
+
<!-- Step indicator -->
|
|
230
|
+
<StepList v-if="!hideStepIndicator">
|
|
231
|
+
<Step
|
|
232
|
+
v-for="(stepDef, idx) in steps"
|
|
233
|
+
:key="idx"
|
|
234
|
+
:value="idx + 1"
|
|
235
|
+
>
|
|
236
|
+
{{ stepDef.label }}
|
|
237
|
+
</Step>
|
|
238
|
+
</StepList>
|
|
239
|
+
|
|
240
|
+
<!-- Step panels -->
|
|
241
|
+
<StepPanels>
|
|
242
|
+
<StepPanel
|
|
243
|
+
v-for="(stepDef, idx) in steps"
|
|
244
|
+
:key="idx"
|
|
245
|
+
:value="idx + 1"
|
|
246
|
+
>
|
|
247
|
+
<div class="step-content">
|
|
248
|
+
<!-- Optional step header slot -->
|
|
249
|
+
<slot :name="`step-${idx + 1}-header`" v-bind="slotActions" />
|
|
250
|
+
|
|
251
|
+
<!-- Step content slot -->
|
|
252
|
+
<slot :name="`step-${idx + 1}`" v-bind="slotActions" />
|
|
253
|
+
|
|
254
|
+
<!-- Action buttons -->
|
|
255
|
+
<div class="step-actions">
|
|
256
|
+
<slot name="actions" v-bind="slotActions">
|
|
257
|
+
<!-- Back button -->
|
|
258
|
+
<Button
|
|
259
|
+
v-if="!isFirstStep || showBackOnFirst"
|
|
260
|
+
:label="backLabel"
|
|
261
|
+
icon="pi pi-arrow-left"
|
|
262
|
+
severity="secondary"
|
|
263
|
+
@click="goPrev"
|
|
264
|
+
:disabled="loading || isFirstStep"
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
<!-- Cancel button -->
|
|
268
|
+
<Button
|
|
269
|
+
:label="cancelLabel"
|
|
270
|
+
severity="secondary"
|
|
271
|
+
@click="cancel"
|
|
272
|
+
:disabled="loading"
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
<!-- Next button (not on last step) -->
|
|
276
|
+
<Button
|
|
277
|
+
v-if="!isLastStep"
|
|
278
|
+
:label="nextLabel"
|
|
279
|
+
icon="pi pi-arrow-right"
|
|
280
|
+
iconPos="right"
|
|
281
|
+
@click="goNext"
|
|
282
|
+
:disabled="!canProceed"
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<!-- Submit button (only on last step) -->
|
|
286
|
+
<Button
|
|
287
|
+
v-if="isLastStep"
|
|
288
|
+
:label="submitLabel"
|
|
289
|
+
icon="pi pi-check"
|
|
290
|
+
@click="submit"
|
|
291
|
+
:loading="loading"
|
|
292
|
+
:disabled="!canSubmit"
|
|
293
|
+
/>
|
|
294
|
+
</slot>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</StepPanel>
|
|
298
|
+
</StepPanels>
|
|
299
|
+
</Stepper>
|
|
300
|
+
</div>
|
|
301
|
+
</Dialog>
|
|
302
|
+
</template>
|
|
303
|
+
|
|
304
|
+
<style scoped>
|
|
305
|
+
.multi-step-dialog {
|
|
306
|
+
padding: 0.5rem 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.step-content {
|
|
310
|
+
padding: 1rem 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.step-actions {
|
|
314
|
+
display: flex;
|
|
315
|
+
justify-content: flex-end;
|
|
316
|
+
gap: 0.5rem;
|
|
317
|
+
margin-top: 1.5rem;
|
|
318
|
+
padding-top: 1rem;
|
|
319
|
+
border-top: 1px solid var(--p-surface-200);
|
|
320
|
+
}
|
|
321
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* SimpleDialog - Wrapper for common dialog patterns
|
|
4
|
+
*
|
|
5
|
+
* Simplifies the most common dialog use case:
|
|
6
|
+
* - Modal with title
|
|
7
|
+
* - Content slot
|
|
8
|
+
* - Cancel/Confirm footer buttons
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <SimpleDialog
|
|
12
|
+
* v-model:visible="showDialog"
|
|
13
|
+
* title="Confirm Action"
|
|
14
|
+
* :loading="saving"
|
|
15
|
+
* @confirm="onSave"
|
|
16
|
+
* >
|
|
17
|
+
* <p>Are you sure?</p>
|
|
18
|
+
* </SimpleDialog>
|
|
19
|
+
*
|
|
20
|
+
* For read-only dialogs (no confirm button):
|
|
21
|
+
* <SimpleDialog v-model:visible="showDialog" title="View" :showConfirm="false">
|
|
22
|
+
* <p>Content here</p>
|
|
23
|
+
* </SimpleDialog>
|
|
24
|
+
*
|
|
25
|
+
* For dialogs with extra action buttons:
|
|
26
|
+
* <SimpleDialog v-model:visible="showDialog" title="View Key" :showConfirm="false">
|
|
27
|
+
* <p>{{ key }}</p>
|
|
28
|
+
* <template #actions>
|
|
29
|
+
* <Button label="Copy" icon="pi pi-copy" @click="copyKey" />
|
|
30
|
+
* </template>
|
|
31
|
+
* </SimpleDialog>
|
|
32
|
+
*/
|
|
33
|
+
import Dialog from 'primevue/dialog'
|
|
34
|
+
import Button from 'primevue/button'
|
|
35
|
+
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
visible: { type: Boolean, default: false },
|
|
38
|
+
title: { type: String, required: true },
|
|
39
|
+
width: { type: String, default: '500px' },
|
|
40
|
+
|
|
41
|
+
// Footer buttons
|
|
42
|
+
showCancel: { type: Boolean, default: true },
|
|
43
|
+
showConfirm: { type: Boolean, default: true },
|
|
44
|
+
cancelLabel: { type: String, default: 'Cancel' },
|
|
45
|
+
confirmLabel: { type: String, default: 'Confirm' },
|
|
46
|
+
confirmIcon: { type: String, default: null },
|
|
47
|
+
confirmSeverity: { type: String, default: null },
|
|
48
|
+
confirmDisabled: { type: Boolean, default: false },
|
|
49
|
+
loading: { type: Boolean, default: false },
|
|
50
|
+
|
|
51
|
+
// Dialog behavior
|
|
52
|
+
closable: { type: Boolean, default: true },
|
|
53
|
+
dismissableMask: { type: Boolean, default: false }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(['update:visible', 'confirm', 'cancel'])
|
|
57
|
+
|
|
58
|
+
function onCancel() {
|
|
59
|
+
emit('cancel')
|
|
60
|
+
emit('update:visible', false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onConfirm() {
|
|
64
|
+
emit('confirm')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Allow closing via escape/mask only when not loading
|
|
68
|
+
const canClose = computed(() => props.closable && !props.loading)
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import { computed } from 'vue'
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<Dialog
|
|
77
|
+
:visible="visible"
|
|
78
|
+
:header="title"
|
|
79
|
+
:modal="true"
|
|
80
|
+
:style="{ width }"
|
|
81
|
+
:closable="canClose"
|
|
82
|
+
:closeOnEscape="canClose"
|
|
83
|
+
:dismissableMask="dismissableMask && canClose"
|
|
84
|
+
@update:visible="$emit('update:visible', $event)"
|
|
85
|
+
>
|
|
86
|
+
<slot />
|
|
87
|
+
|
|
88
|
+
<template #footer v-if="showCancel || showConfirm || $slots.actions">
|
|
89
|
+
<slot name="actions" />
|
|
90
|
+
<Button
|
|
91
|
+
v-if="showCancel"
|
|
92
|
+
:label="cancelLabel"
|
|
93
|
+
severity="secondary"
|
|
94
|
+
@click="onCancel"
|
|
95
|
+
:disabled="loading"
|
|
96
|
+
/>
|
|
97
|
+
<Button
|
|
98
|
+
v-if="showConfirm"
|
|
99
|
+
:label="confirmLabel"
|
|
100
|
+
:icon="confirmIcon"
|
|
101
|
+
:severity="confirmSeverity"
|
|
102
|
+
:loading="loading"
|
|
103
|
+
:disabled="confirmDisabled"
|
|
104
|
+
@click="onConfirm"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
</Dialog>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* UnsavedChangesDialog - Confirm leaving with unsaved changes
|
|
4
|
+
*
|
|
5
|
+
* Uses SimpleDialog with custom actions:
|
|
6
|
+
* - Save & Leave (optional): saves then navigates
|
|
7
|
+
* - Leave: discards changes and navigates
|
|
8
|
+
* - Stay: cancels navigation
|
|
9
|
+
*/
|
|
10
|
+
import SimpleDialog from './SimpleDialog.vue'
|
|
11
|
+
import Button from 'primevue/button'
|
|
12
|
+
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
visible: { type: Boolean, default: false },
|
|
15
|
+
saving: { type: Boolean, default: false },
|
|
16
|
+
message: { type: String, default: 'You have unsaved changes that will be lost.' },
|
|
17
|
+
hasOnSave: { type: Boolean, default: false }
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits(['update:visible', 'saveAndLeave', 'leave', 'stay'])
|
|
21
|
+
|
|
22
|
+
function onStay() {
|
|
23
|
+
emit('stay')
|
|
24
|
+
emit('update:visible', false)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onLeave() {
|
|
28
|
+
emit('leave')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function onSaveAndLeave() {
|
|
32
|
+
emit('saveAndLeave')
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<SimpleDialog
|
|
38
|
+
:visible="visible"
|
|
39
|
+
title="Unsaved Changes"
|
|
40
|
+
width="400px"
|
|
41
|
+
:closable="false"
|
|
42
|
+
:showCancel="true"
|
|
43
|
+
:showConfirm="true"
|
|
44
|
+
cancelLabel="Stay"
|
|
45
|
+
confirmLabel="Leave"
|
|
46
|
+
confirmSeverity="danger"
|
|
47
|
+
:loading="saving"
|
|
48
|
+
@cancel="onStay"
|
|
49
|
+
@confirm="onLeave"
|
|
50
|
+
@update:visible="$emit('update:visible', $event)"
|
|
51
|
+
>
|
|
52
|
+
<div class="dialog-content">
|
|
53
|
+
<i class="pi pi-exclamation-triangle dialog-icon"></i>
|
|
54
|
+
<p>{{ message }}</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<template #actions>
|
|
58
|
+
<Button
|
|
59
|
+
v-if="hasOnSave"
|
|
60
|
+
label="Save & Leave"
|
|
61
|
+
icon="pi pi-save"
|
|
62
|
+
severity="success"
|
|
63
|
+
:loading="saving"
|
|
64
|
+
@click="onSaveAndLeave"
|
|
65
|
+
/>
|
|
66
|
+
</template>
|
|
67
|
+
</SimpleDialog>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.dialog-content {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: flex-start;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dialog-icon {
|
|
78
|
+
font-size: 2rem;
|
|
79
|
+
color: var(--p-orange-500);
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.dialog-content p {
|
|
84
|
+
margin: 0;
|
|
85
|
+
line-height: 1.5;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* CardsGrid - Dynamic card zone component
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - cards: Array of card objects from useCardsZone composable
|
|
7
|
+
* - columns: Number of columns (2, 3, 4, or 'auto')
|
|
8
|
+
*
|
|
9
|
+
* Card types:
|
|
10
|
+
* 1. Simple stat card (default):
|
|
11
|
+
* { name: 'total', value: 42, label: 'Total', severity: 'success', icon: 'pi pi-check' }
|
|
12
|
+
*
|
|
13
|
+
* 2. Custom card with slot:
|
|
14
|
+
* { name: 'preview', custom: true }
|
|
15
|
+
* Then use: <template #card-preview>...</template>
|
|
16
|
+
*
|
|
17
|
+
* If no cards provided, renders nothing.
|
|
18
|
+
*/
|
|
19
|
+
defineProps({
|
|
20
|
+
cards: {
|
|
21
|
+
type: Array,
|
|
22
|
+
default: () => []
|
|
23
|
+
},
|
|
24
|
+
columns: {
|
|
25
|
+
type: [Number, String],
|
|
26
|
+
default: 'auto',
|
|
27
|
+
validator: (v) => ['auto', 2, 3, 4].includes(v)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function getSeverityClass(severity) {
|
|
32
|
+
if (!severity) return ''
|
|
33
|
+
return `stat-value--${severity}`
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div v-if="cards.length > 0" class="cards-grid" :class="`cards-grid--cols-${columns}`">
|
|
39
|
+
<template v-for="card in cards" :key="card.name">
|
|
40
|
+
<!-- Custom card: render slot -->
|
|
41
|
+
<div v-if="card.custom" class="card-custom" :class="card.class">
|
|
42
|
+
<slot :name="`card-${card.name}`" :card="card" ></slot>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Simple stat card -->
|
|
46
|
+
<div
|
|
47
|
+
v-else
|
|
48
|
+
class="stat-card"
|
|
49
|
+
:class="[{ 'stat-card--clickable': card.onClick }, card.class]"
|
|
50
|
+
@click="card.onClick?.()"
|
|
51
|
+
>
|
|
52
|
+
<div class="stat-value" :class="getSeverityClass(card.severity)">
|
|
53
|
+
<i v-if="card.icon" :class="card.icon" class="stat-icon"></i>
|
|
54
|
+
{{ card.value }}
|
|
55
|
+
</div>
|
|
56
|
+
<div class="stat-label">{{ card.label }}</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.cards-grid {
|
|
64
|
+
display: grid;
|
|
65
|
+
gap: 1rem;
|
|
66
|
+
margin-bottom: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.cards-grid--cols-auto {
|
|
70
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.cards-grid--cols-2 {
|
|
74
|
+
grid-template-columns: repeat(2, 1fr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cards-grid--cols-3 {
|
|
78
|
+
grid-template-columns: repeat(3, 1fr);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.cards-grid--cols-4 {
|
|
82
|
+
grid-template-columns: repeat(4, 1fr);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.stat-card,
|
|
86
|
+
.card-custom {
|
|
87
|
+
background: var(--p-surface-0);
|
|
88
|
+
border: 1px solid var(--p-surface-200);
|
|
89
|
+
border-radius: 0.5rem;
|
|
90
|
+
padding: 1.25rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.stat-card {
|
|
94
|
+
text-align: center;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.stat-card--clickable {
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.stat-card--clickable:hover {
|
|
103
|
+
border-color: var(--p-primary-300);
|
|
104
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.stat-value {
|
|
108
|
+
font-size: 2rem;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
color: var(--p-primary-500);
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
gap: 0.5rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.stat-value--success {
|
|
118
|
+
color: var(--p-green-500);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.stat-value--danger {
|
|
122
|
+
color: var(--p-red-500);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.stat-value--warning {
|
|
126
|
+
color: var(--p-orange-500);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.stat-value--info {
|
|
130
|
+
color: var(--p-blue-500);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.stat-icon {
|
|
134
|
+
font-size: 1.5rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.stat-label {
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
color: var(--p-surface-600);
|
|
140
|
+
margin-top: 0.25rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@media (max-width: 768px) {
|
|
144
|
+
.cards-grid--cols-3,
|
|
145
|
+
.cards-grid--cols-4 {
|
|
146
|
+
grid-template-columns: repeat(2, 1fr);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@media (max-width: 480px) {
|
|
151
|
+
.cards-grid {
|
|
152
|
+
grid-template-columns: 1fr;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</style>
|