@htlkg/components 0.0.11 → 0.0.13
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/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
- package/dist/components.css +4 -4
- package/dist/composables/index.js +23 -22
- package/dist/data/index.js +10 -10
- package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
- package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
- package/dist/index-QK97OdqQ.js.map +1 -1
- package/dist/index.js +34 -33
- package/dist/navigation/index.js +1 -1
- package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
- package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
- package/package.json +3 -3
- package/src/composables/index.ts +1 -0
- package/src/composables/useJsonForm.test.ts +272 -0
- package/src/composables/useJsonForm.ts +261 -0
- package/src/composables/useModal.test.ts +264 -0
- package/src/composables/useModal.ts +54 -8
- package/src/data/Chart/index.ts +2 -0
- package/src/data/DataList/index.ts +1 -0
- package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
- package/src/data/DataTable/index.ts +8 -0
- package/src/data/SearchableSelect/index.ts +1 -0
- package/src/data/Table/index.ts +1 -0
- package/src/data/index.ts +5 -15
- package/src/domain/BrandCard/index.ts +1 -0
- package/src/domain/BrandSelector/index.ts +1 -0
- package/src/domain/ProductBadge/index.ts +1 -0
- package/src/domain/UserAvatar/index.ts +1 -0
- package/src/domain/index.ts +4 -4
- package/src/forms/DateRange/index.ts +2 -0
- package/src/forms/JsonSchemaForm/index.ts +1 -0
- package/src/forms/index.ts +2 -3
- package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
- package/src/navigation/AdminWrapper/index.ts +1 -0
- package/src/navigation/Breadcrumbs/index.ts +1 -0
- package/src/navigation/Stepper/index.ts +2 -0
- package/src/navigation/Tabs/index.ts +2 -0
- package/src/navigation/index.ts +4 -6
- package/src/overlays/Alert/index.ts +1 -0
- package/src/overlays/Drawer/index.ts +1 -0
- package/src/overlays/Modal/index.ts +1 -0
- package/src/overlays/Notification/index.ts +1 -0
- package/src/overlays/index.ts +4 -4
- package/src/patterns/DASHBOARD_PAGE.md +642 -0
- package/src/patterns/DETAIL_PAGE.md +446 -0
- package/src/patterns/FORM_PAGE.md +439 -0
- package/src/patterns/LIST_PAGE.md +340 -0
- package/src/patterns/PAGE_PATTERNS.md +110 -0
- package/src/patterns/WIZARD_PAGE.md +733 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
- package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
- package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
- package/src/data/Table.vue +0 -295
- /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
- /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
- /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
- /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
- /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
- /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
- /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
- /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
- /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
- /package/src/data/{Table.md → Table/Table.md} +0 -0
- /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
- /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
- /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
- /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
- /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
- /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
- /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
- /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
- /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
- /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
- /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
- /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
- /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
- /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
- /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
- /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
- /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
- /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
- /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
- /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
- /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
- /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
- /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
- /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
- /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
- /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
- /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
- /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
- /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
- /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
- /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
- /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
- /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
- /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
- /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
- /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
- /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
- /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
# Wizard Page Pattern
|
|
2
|
+
|
|
3
|
+
Multi-step forms for complex entity creation with validation, progress tracking, and step-by-step navigation.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ AdminLayout │
|
|
10
|
+
├─────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ Page Header (title + breadcrumbs) │
|
|
12
|
+
├─────────────────────────────────────────────────────────────┤
|
|
13
|
+
│ Back Button │
|
|
14
|
+
├─────────────────────────────────────────────────────────────┤
|
|
15
|
+
│ Stepper (progress indicator) │
|
|
16
|
+
│ ├── Step 1: Complete ✓ │
|
|
17
|
+
│ ├── Step 2: Current → │
|
|
18
|
+
│ ├── Step 3: Upcoming │
|
|
19
|
+
│ └── Step N: Upcoming │
|
|
20
|
+
├─────────────────────────────────────────────────────────────┤
|
|
21
|
+
│ Step Content Container (lg:w-2/3 xl:w-1/2) │
|
|
22
|
+
│ ├── Step Title/Description │
|
|
23
|
+
│ ├── Form Fields (JsonSchemaForm or custom) │
|
|
24
|
+
│ └── Navigation Buttons (Back / Next / Complete) │
|
|
25
|
+
├─────────────────────────────────────────────────────────────┤
|
|
26
|
+
│ Final Step: Review & Submit │
|
|
27
|
+
│ ├── Summary Card │
|
|
28
|
+
│ ├── Missing Fields Alert (if any) │
|
|
29
|
+
│ └── Create/Submit Button │
|
|
30
|
+
└─────────────────────────────────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CRM Template Reference
|
|
34
|
+
|
|
35
|
+
Based on: `ui/src/components/Templates/CRM/CampaignsCreate.vue`
|
|
36
|
+
|
|
37
|
+
```vue
|
|
38
|
+
<!-- CRM Template Structure (raw @hotelinking/ui) -->
|
|
39
|
+
<uiWrapper :sidebar :topbar>
|
|
40
|
+
<uiViewHeader :pages="pages" title="Campaigns - Create" />
|
|
41
|
+
|
|
42
|
+
<!-- Back Button -->
|
|
43
|
+
<div class="flex justify-between items-center mb-6">
|
|
44
|
+
<uiButton :icon="ArrowLeftIcon" @click="handleBack">Back to Campaigns</uiButton>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Stepper -->
|
|
48
|
+
<uiStepsV4
|
|
49
|
+
:steps="stepsV4"
|
|
50
|
+
@stepClick="handleStepClick"
|
|
51
|
+
@stepCompleted="handleStepCompleted"
|
|
52
|
+
@stepCurrent="handleStepCurrent"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<!-- Step Content -->
|
|
56
|
+
<div v-if="currentStepIndex === 0" class="lg:w-2/3 xl:w-1/2">
|
|
57
|
+
<uiInput label="Campaign Name" v-model="campaignName" />
|
|
58
|
+
<uiTextArea label="Description" v-model="description" />
|
|
59
|
+
|
|
60
|
+
<!-- Navigation -->
|
|
61
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
62
|
+
<uiButton v-if="currentStepIndex > 0" :icon="ArrowLeftIcon" color="gray" @click="goToPreviousStep">
|
|
63
|
+
Back
|
|
64
|
+
</uiButton>
|
|
65
|
+
<uiButton :icon="ArrowRightIcon" color="primary" @click="goToNextStep">
|
|
66
|
+
Next
|
|
67
|
+
</uiButton>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Review Step -->
|
|
72
|
+
<div v-else-if="currentStepIndex === lastStepIndex" class="lg:w-2/3 xl:w-1/2">
|
|
73
|
+
<uiAlert v-if="missingFields.length" title="Missing Fields" type="danger" />
|
|
74
|
+
<uiStripedCard :title="campaignName" :items="reviewItems" />
|
|
75
|
+
<uiButton color="primary" @click="handleCreate">Create Campaign</uiButton>
|
|
76
|
+
</div>
|
|
77
|
+
</uiWrapper>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Enhanced Pattern (with @htlkg/*)
|
|
81
|
+
|
|
82
|
+
### Astro Page
|
|
83
|
+
|
|
84
|
+
```astro
|
|
85
|
+
---
|
|
86
|
+
// src/pages/[brandId]/admin/campaigns/create.astro
|
|
87
|
+
import Layout from '@/layouts/Layout.astro';
|
|
88
|
+
import CampaignWizard from '@/components/admin/CampaignWizard.vue';
|
|
89
|
+
|
|
90
|
+
export const prerender = false;
|
|
91
|
+
|
|
92
|
+
const { brandId } = Astro.params;
|
|
93
|
+
|
|
94
|
+
const layoutProps = {
|
|
95
|
+
title: 'Create Campaign',
|
|
96
|
+
breadcrumbs: [
|
|
97
|
+
{ name: 'Campaigns', href: `/${brandId}/admin/campaigns` },
|
|
98
|
+
{ name: 'Create', current: true },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
<Layout {...layoutProps}>
|
|
104
|
+
<CampaignWizard client:load brandId={brandId} />
|
|
105
|
+
</Layout>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Vue Component with useWizard
|
|
109
|
+
|
|
110
|
+
```vue
|
|
111
|
+
<!-- src/components/admin/CampaignWizard.vue -->
|
|
112
|
+
<script setup lang="ts">
|
|
113
|
+
import { computed } from 'vue';
|
|
114
|
+
import { Stepper } from '@htlkg/components/navigation';
|
|
115
|
+
import { JsonSchemaForm } from '@htlkg/components/forms';
|
|
116
|
+
import { useWizard, useNotifications } from '@htlkg/components/composables';
|
|
117
|
+
import { uiButton, uiAlert, uiStripedCard } from '@hotelinking/ui';
|
|
118
|
+
import {
|
|
119
|
+
ArrowLeftIcon,
|
|
120
|
+
ArrowRightIcon,
|
|
121
|
+
CheckIcon,
|
|
122
|
+
} from '@heroicons/vue/24/outline';
|
|
123
|
+
|
|
124
|
+
interface CampaignData {
|
|
125
|
+
name: string;
|
|
126
|
+
description: string;
|
|
127
|
+
segments: string[];
|
|
128
|
+
tags: string[];
|
|
129
|
+
templateId: string;
|
|
130
|
+
sendOption: 'immediately' | 'schedule';
|
|
131
|
+
scheduleDate?: string;
|
|
132
|
+
subjectLine: string;
|
|
133
|
+
senderName: string;
|
|
134
|
+
senderEmail: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface Props {
|
|
138
|
+
brandId: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const props = defineProps<Props>();
|
|
142
|
+
const emit = defineEmits<{
|
|
143
|
+
back: [];
|
|
144
|
+
created: [campaign: CampaignData];
|
|
145
|
+
}>();
|
|
146
|
+
|
|
147
|
+
const { notify } = useNotifications();
|
|
148
|
+
|
|
149
|
+
// Define wizard steps with JSON Schema for each step
|
|
150
|
+
const steps = [
|
|
151
|
+
{
|
|
152
|
+
id: 'details',
|
|
153
|
+
label: 'Campaign Details',
|
|
154
|
+
description: 'Basic campaign information',
|
|
155
|
+
schema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
name: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
title: 'Campaign Name',
|
|
161
|
+
minLength: 1,
|
|
162
|
+
},
|
|
163
|
+
description: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
title: 'Description',
|
|
166
|
+
minLength: 100, // Will auto-use textarea
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ['name'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'audience',
|
|
174
|
+
label: 'Audience Selection',
|
|
175
|
+
description: 'Select target segments and tags',
|
|
176
|
+
// Custom validation - no schema
|
|
177
|
+
validate: (data: Partial<CampaignData>) => {
|
|
178
|
+
return (data.segments?.length ?? 0) > 0 || (data.tags?.length ?? 0) > 0;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'template',
|
|
183
|
+
label: 'Email Design',
|
|
184
|
+
description: 'Select email template',
|
|
185
|
+
validate: (data: Partial<CampaignData>) => {
|
|
186
|
+
return !!data.templateId;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'schedule',
|
|
191
|
+
label: 'Scheduling',
|
|
192
|
+
description: 'Set send date and time',
|
|
193
|
+
schema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
sendOption: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
title: 'Send Option',
|
|
199
|
+
enum: ['immediately', 'schedule'],
|
|
200
|
+
default: 'schedule',
|
|
201
|
+
},
|
|
202
|
+
scheduleDate: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
title: 'Schedule Date',
|
|
205
|
+
format: 'date-time',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ['sendOption'],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'sender',
|
|
213
|
+
label: 'Subject & Sender',
|
|
214
|
+
description: 'Configure email subject and sender',
|
|
215
|
+
schema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
subjectLine: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
title: 'Subject Line',
|
|
221
|
+
minLength: 1,
|
|
222
|
+
},
|
|
223
|
+
senderName: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
title: 'Sender Name',
|
|
226
|
+
minLength: 1,
|
|
227
|
+
},
|
|
228
|
+
senderEmail: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
title: 'Sender Email',
|
|
231
|
+
format: 'email',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
required: ['subjectLine', 'senderName', 'senderEmail'],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'review',
|
|
239
|
+
label: 'Review & Send',
|
|
240
|
+
description: 'Review campaign before creating',
|
|
241
|
+
optional: true, // Review step doesn't need validation
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
// Initialize wizard
|
|
246
|
+
const wizard = useWizard<CampaignData>({
|
|
247
|
+
steps,
|
|
248
|
+
initialData: {
|
|
249
|
+
name: '',
|
|
250
|
+
description: '',
|
|
251
|
+
segments: [],
|
|
252
|
+
tags: [],
|
|
253
|
+
templateId: '',
|
|
254
|
+
sendOption: 'schedule',
|
|
255
|
+
subjectLine: '',
|
|
256
|
+
senderName: '',
|
|
257
|
+
senderEmail: '',
|
|
258
|
+
},
|
|
259
|
+
validateOnNext: true,
|
|
260
|
+
onComplete: async (data) => {
|
|
261
|
+
try {
|
|
262
|
+
await createCampaign(props.brandId, data);
|
|
263
|
+
notify({ type: 'success', message: 'Campaign created successfully' });
|
|
264
|
+
emit('created', data as CampaignData);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
notify({ type: 'error', message: 'Failed to create campaign' });
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
onStepChange: (from, to, data) => {
|
|
271
|
+
console.log(`Step changed from ${from} to ${to}`);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Review items for final step
|
|
276
|
+
const reviewItems = computed(() => {
|
|
277
|
+
const data = wizard.data.value;
|
|
278
|
+
return [
|
|
279
|
+
{ title: 'Campaign Name', text: data.name || 'Not set' },
|
|
280
|
+
{ title: 'Description', text: data.description || 'Not set' },
|
|
281
|
+
{ title: 'Segments', text: data.segments?.join(', ') || 'None selected' },
|
|
282
|
+
{ title: 'Tags', text: data.tags?.join(', ') || 'None selected' },
|
|
283
|
+
{ title: 'Template', text: data.templateId || 'Not selected' },
|
|
284
|
+
{ title: 'Send Option', text: data.sendOption === 'immediately' ? 'Send Now' : `Scheduled: ${data.scheduleDate}` },
|
|
285
|
+
{ title: 'Subject Line', text: data.subjectLine || 'Not set' },
|
|
286
|
+
{ title: 'Sender', text: `${data.senderName} <${data.senderEmail}>` || 'Not set' },
|
|
287
|
+
];
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Missing fields for review step
|
|
291
|
+
const missingFields = computed(() => {
|
|
292
|
+
const missing: string[] = [];
|
|
293
|
+
const data = wizard.data.value;
|
|
294
|
+
|
|
295
|
+
if (!data.name) missing.push('Campaign Name');
|
|
296
|
+
if ((data.segments?.length ?? 0) === 0 && (data.tags?.length ?? 0) === 0) {
|
|
297
|
+
missing.push('At least one segment or tag');
|
|
298
|
+
}
|
|
299
|
+
if (!data.templateId) missing.push('Email Template');
|
|
300
|
+
if (!data.subjectLine) missing.push('Subject Line');
|
|
301
|
+
if (!data.senderName) missing.push('Sender Name');
|
|
302
|
+
if (!data.senderEmail) missing.push('Sender Email');
|
|
303
|
+
|
|
304
|
+
return missing;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Handle form submission for schema-based steps
|
|
308
|
+
function handleStepSubmit(formData: Partial<CampaignData>) {
|
|
309
|
+
wizard.updateData(formData);
|
|
310
|
+
wizard.setStepValid(wizard.currentStep.value.id, true);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Handle form validation errors
|
|
314
|
+
function handleValidationError(errors: Array<{ field: string; message: string }>) {
|
|
315
|
+
wizard.setStepValid(wizard.currentStep.value.id, false);
|
|
316
|
+
if (errors.length > 0) {
|
|
317
|
+
wizard.setStepError(wizard.currentStep.value.id, errors[0].message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
</script>
|
|
321
|
+
|
|
322
|
+
<template>
|
|
323
|
+
<!-- Back Button -->
|
|
324
|
+
<div class="flex justify-between items-center mb-6">
|
|
325
|
+
<uiButton :icon="ArrowLeftIcon" @click="emit('back')">
|
|
326
|
+
Back to Campaigns
|
|
327
|
+
</uiButton>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<!-- Stepper -->
|
|
331
|
+
<Stepper
|
|
332
|
+
:steps="wizard.stepperSteps.value"
|
|
333
|
+
:current-step="wizard.currentStepIndex.value"
|
|
334
|
+
@step-click="wizard.goToStep"
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
<!-- Step Content -->
|
|
338
|
+
<div class="mt-8">
|
|
339
|
+
<!-- Step 1: Campaign Details -->
|
|
340
|
+
<div v-if="wizard.currentStep.value.id === 'details'" class="lg:w-2/3 xl:w-1/2">
|
|
341
|
+
<JsonSchemaForm
|
|
342
|
+
v-model="wizard.data.value"
|
|
343
|
+
:schema="wizard.currentStep.value.schema"
|
|
344
|
+
@submit="handleStepSubmit"
|
|
345
|
+
@validation-error="handleValidationError"
|
|
346
|
+
>
|
|
347
|
+
<template #actions>
|
|
348
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
349
|
+
<uiButton
|
|
350
|
+
:icon="ArrowRightIcon"
|
|
351
|
+
color="primary"
|
|
352
|
+
@click="wizard.goToNext"
|
|
353
|
+
>
|
|
354
|
+
Next
|
|
355
|
+
</uiButton>
|
|
356
|
+
</div>
|
|
357
|
+
</template>
|
|
358
|
+
</JsonSchemaForm>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Step 2: Audience Selection (Custom UI) -->
|
|
362
|
+
<div v-else-if="wizard.currentStep.value.id === 'audience'" class="lg:w-2/3 xl:w-1/2">
|
|
363
|
+
<!-- Segments Checkboxes -->
|
|
364
|
+
<div class="mb-8">
|
|
365
|
+
<h3 class="text-lg font-semibold mb-4">Select Segments</h3>
|
|
366
|
+
<!-- Custom segment selection UI here -->
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<!-- Tags Checkboxes -->
|
|
370
|
+
<div class="mb-8">
|
|
371
|
+
<h3 class="text-lg font-semibold mb-4">Select Tags</h3>
|
|
372
|
+
<!-- Custom tag selection UI here -->
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<!-- Navigation -->
|
|
376
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
377
|
+
<uiButton
|
|
378
|
+
:icon="ArrowLeftIcon"
|
|
379
|
+
color="gray"
|
|
380
|
+
@click="wizard.goToPrevious"
|
|
381
|
+
>
|
|
382
|
+
Back
|
|
383
|
+
</uiButton>
|
|
384
|
+
<uiButton
|
|
385
|
+
:icon="ArrowRightIcon"
|
|
386
|
+
color="primary"
|
|
387
|
+
@click="wizard.goToNext"
|
|
388
|
+
>
|
|
389
|
+
Next
|
|
390
|
+
</uiButton>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- Step 3: Template Selection (Custom UI) -->
|
|
395
|
+
<div v-else-if="wizard.currentStep.value.id === 'template'" class="mt-8">
|
|
396
|
+
<!-- Template Grid -->
|
|
397
|
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
|
398
|
+
<!-- Template cards here -->
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<!-- Navigation -->
|
|
402
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
403
|
+
<uiButton
|
|
404
|
+
:icon="ArrowLeftIcon"
|
|
405
|
+
color="gray"
|
|
406
|
+
@click="wizard.goToPrevious"
|
|
407
|
+
>
|
|
408
|
+
Back
|
|
409
|
+
</uiButton>
|
|
410
|
+
<uiButton
|
|
411
|
+
:icon="ArrowRightIcon"
|
|
412
|
+
color="primary"
|
|
413
|
+
@click="wizard.goToNext"
|
|
414
|
+
>
|
|
415
|
+
Next
|
|
416
|
+
</uiButton>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<!-- Step 4: Scheduling (JsonSchemaForm) -->
|
|
421
|
+
<div v-else-if="wizard.currentStep.value.id === 'schedule'" class="lg:w-2/3 xl:w-1/2">
|
|
422
|
+
<JsonSchemaForm
|
|
423
|
+
v-model="wizard.data.value"
|
|
424
|
+
:schema="wizard.currentStep.value.schema"
|
|
425
|
+
@submit="handleStepSubmit"
|
|
426
|
+
@validation-error="handleValidationError"
|
|
427
|
+
>
|
|
428
|
+
<template #actions>
|
|
429
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
430
|
+
<uiButton
|
|
431
|
+
:icon="ArrowLeftIcon"
|
|
432
|
+
color="gray"
|
|
433
|
+
@click="wizard.goToPrevious"
|
|
434
|
+
>
|
|
435
|
+
Back
|
|
436
|
+
</uiButton>
|
|
437
|
+
<uiButton
|
|
438
|
+
:icon="ArrowRightIcon"
|
|
439
|
+
color="primary"
|
|
440
|
+
@click="wizard.goToNext"
|
|
441
|
+
>
|
|
442
|
+
Next
|
|
443
|
+
</uiButton>
|
|
444
|
+
</div>
|
|
445
|
+
</template>
|
|
446
|
+
</JsonSchemaForm>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<!-- Step 5: Subject & Sender (JsonSchemaForm) -->
|
|
450
|
+
<div v-else-if="wizard.currentStep.value.id === 'sender'" class="lg:w-2/3 xl:w-1/2">
|
|
451
|
+
<JsonSchemaForm
|
|
452
|
+
v-model="wizard.data.value"
|
|
453
|
+
:schema="wizard.currentStep.value.schema"
|
|
454
|
+
@submit="handleStepSubmit"
|
|
455
|
+
@validation-error="handleValidationError"
|
|
456
|
+
>
|
|
457
|
+
<template #actions>
|
|
458
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
459
|
+
<uiButton
|
|
460
|
+
:icon="ArrowLeftIcon"
|
|
461
|
+
color="gray"
|
|
462
|
+
@click="wizard.goToPrevious"
|
|
463
|
+
>
|
|
464
|
+
Back
|
|
465
|
+
</uiButton>
|
|
466
|
+
<uiButton
|
|
467
|
+
:icon="ArrowRightIcon"
|
|
468
|
+
color="primary"
|
|
469
|
+
@click="wizard.goToNext"
|
|
470
|
+
>
|
|
471
|
+
Next
|
|
472
|
+
</uiButton>
|
|
473
|
+
</div>
|
|
474
|
+
</template>
|
|
475
|
+
</JsonSchemaForm>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<!-- Step 6: Review -->
|
|
479
|
+
<div v-else-if="wizard.currentStep.value.id === 'review'" class="lg:w-2/3 xl:w-1/2">
|
|
480
|
+
<!-- Missing Fields Alert -->
|
|
481
|
+
<uiAlert
|
|
482
|
+
v-if="missingFields.length > 0"
|
|
483
|
+
title="Missing Required Fields"
|
|
484
|
+
type="danger"
|
|
485
|
+
class="mb-6"
|
|
486
|
+
>
|
|
487
|
+
<p class="text-sm mb-2">Please complete the following before creating:</p>
|
|
488
|
+
<ul class="list-disc list-inside text-sm">
|
|
489
|
+
<li v-for="field in missingFields" :key="field">{{ field }}</li>
|
|
490
|
+
</ul>
|
|
491
|
+
</uiAlert>
|
|
492
|
+
|
|
493
|
+
<!-- Review Summary -->
|
|
494
|
+
<uiStripedCard
|
|
495
|
+
:title="wizard.data.value.name || 'New Campaign'"
|
|
496
|
+
:items="reviewItems"
|
|
497
|
+
class="mb-8"
|
|
498
|
+
/>
|
|
499
|
+
|
|
500
|
+
<!-- Navigation -->
|
|
501
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
502
|
+
<uiButton
|
|
503
|
+
:icon="ArrowLeftIcon"
|
|
504
|
+
color="gray"
|
|
505
|
+
@click="wizard.goToPrevious"
|
|
506
|
+
>
|
|
507
|
+
Back
|
|
508
|
+
</uiButton>
|
|
509
|
+
<uiButton
|
|
510
|
+
:icon="CheckIcon"
|
|
511
|
+
color="primary"
|
|
512
|
+
:loading="wizard.isCompleting.value"
|
|
513
|
+
:disabled="missingFields.length > 0"
|
|
514
|
+
@click="wizard.complete"
|
|
515
|
+
>
|
|
516
|
+
Create Campaign
|
|
517
|
+
</uiButton>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- Progress Indicator -->
|
|
523
|
+
<div class="mt-4 text-sm text-gray-500 text-center">
|
|
524
|
+
Step {{ wizard.currentStepIndex.value + 1 }} of {{ wizard.stepperSteps.value.length }}
|
|
525
|
+
({{ wizard.progress.value }}% complete)
|
|
526
|
+
</div>
|
|
527
|
+
</template>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Key Components
|
|
531
|
+
|
|
532
|
+
### useWizard Composable
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { useWizard } from '@htlkg/components/composables';
|
|
536
|
+
|
|
537
|
+
const wizard = useWizard<FormData>({
|
|
538
|
+
// Step definitions
|
|
539
|
+
steps: [
|
|
540
|
+
{
|
|
541
|
+
id: 'step1',
|
|
542
|
+
label: 'Step Label',
|
|
543
|
+
description: 'Optional description',
|
|
544
|
+
schema: jsonSchema, // JSON Schema for JsonSchemaForm
|
|
545
|
+
validate: (data) => true, // Custom validation function
|
|
546
|
+
optional: false, // Whether step can be skipped
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
|
|
550
|
+
// Initial form data
|
|
551
|
+
initialData: { field1: '', field2: '' },
|
|
552
|
+
|
|
553
|
+
// Validation behavior
|
|
554
|
+
validateOnNext: true, // Validate before advancing
|
|
555
|
+
allowBackWithoutValidation: true, // Allow going back without validation
|
|
556
|
+
|
|
557
|
+
// Callbacks
|
|
558
|
+
onComplete: async (data) => { /* Handle completion */ },
|
|
559
|
+
onStepChange: (from, to, data) => { /* Handle step change */ },
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Returned state and methods
|
|
563
|
+
wizard.currentStepIndex // Current step index (ref)
|
|
564
|
+
wizard.currentStep // Current step object (computed)
|
|
565
|
+
wizard.data // Form data (ref)
|
|
566
|
+
wizard.isFirstStep // Is first step (computed)
|
|
567
|
+
wizard.isLastStep // Is last step (computed)
|
|
568
|
+
wizard.isCompleting // Is completing (ref)
|
|
569
|
+
wizard.stepperSteps // Steps for Stepper component (computed)
|
|
570
|
+
wizard.stepValidation // Validation state per step (ref)
|
|
571
|
+
wizard.currentStepValid // Is current step valid (computed)
|
|
572
|
+
wizard.allStepsValid // Are all steps valid (computed)
|
|
573
|
+
wizard.stepErrors // Error messages per step (ref)
|
|
574
|
+
wizard.progress // Progress percentage (computed)
|
|
575
|
+
|
|
576
|
+
// Navigation
|
|
577
|
+
wizard.goToNext() // Go to next step (validates current)
|
|
578
|
+
wizard.goToPrevious() // Go to previous step
|
|
579
|
+
wizard.goToStep(index) // Go to specific step
|
|
580
|
+
wizard.canGoToStep(index) // Check if navigation is allowed
|
|
581
|
+
|
|
582
|
+
// Data management
|
|
583
|
+
wizard.updateData(data) // Update form data
|
|
584
|
+
wizard.updateStepData(stepId, data) // Update data for specific step
|
|
585
|
+
wizard.setStepValid(stepId, valid) // Set step validation status
|
|
586
|
+
wizard.setStepError(stepId, error) // Set step error message
|
|
587
|
+
wizard.resetStep(stepId) // Reset specific step
|
|
588
|
+
wizard.reset() // Reset entire wizard
|
|
589
|
+
|
|
590
|
+
// Completion
|
|
591
|
+
wizard.complete() // Validate all and complete
|
|
592
|
+
|
|
593
|
+
// Utilities
|
|
594
|
+
wizard.getStepIndex(stepId) // Get step index by ID
|
|
595
|
+
wizard.getStep(stepId) // Get step by ID
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Step Types
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
interface WizardStep<T> {
|
|
602
|
+
/** Unique step identifier */
|
|
603
|
+
id: string;
|
|
604
|
+
/** Display label for the step */
|
|
605
|
+
label: string;
|
|
606
|
+
/** Optional description */
|
|
607
|
+
description?: string;
|
|
608
|
+
/** JSON Schema for JsonSchemaForm (optional) */
|
|
609
|
+
schema?: JsonSchema;
|
|
610
|
+
/** Custom validation function (optional) */
|
|
611
|
+
validate?: (data: T) => boolean | Promise<boolean>;
|
|
612
|
+
/** Icon component for the step (optional) */
|
|
613
|
+
icon?: Component;
|
|
614
|
+
/** Whether step can be skipped */
|
|
615
|
+
optional?: boolean;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Step Status
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
type StepStatus = 'complete' | 'current' | 'upcoming' | 'error';
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Patterns
|
|
626
|
+
|
|
627
|
+
### Schema-Based Steps
|
|
628
|
+
|
|
629
|
+
For steps with simple form fields, use JSON Schema:
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
{
|
|
633
|
+
id: 'details',
|
|
634
|
+
label: 'Details',
|
|
635
|
+
schema: {
|
|
636
|
+
type: 'object',
|
|
637
|
+
properties: {
|
|
638
|
+
name: { type: 'string', title: 'Name', minLength: 1 },
|
|
639
|
+
email: { type: 'string', title: 'Email', format: 'email' },
|
|
640
|
+
},
|
|
641
|
+
required: ['name', 'email'],
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Custom Validation Steps
|
|
647
|
+
|
|
648
|
+
For steps with complex UI (grids, cards, custom selections):
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
{
|
|
652
|
+
id: 'selection',
|
|
653
|
+
label: 'Select Items',
|
|
654
|
+
validate: (data) => data.selectedItems?.length > 0,
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Optional Steps
|
|
659
|
+
|
|
660
|
+
For steps that can be skipped:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
{
|
|
664
|
+
id: 'extras',
|
|
665
|
+
label: 'Additional Options',
|
|
666
|
+
optional: true,
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Review Step
|
|
671
|
+
|
|
672
|
+
Always add a final review step:
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
{
|
|
676
|
+
id: 'review',
|
|
677
|
+
label: 'Review',
|
|
678
|
+
optional: true, // No validation needed
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Navigation Buttons Pattern
|
|
683
|
+
|
|
684
|
+
```vue
|
|
685
|
+
<div class="flex justify-end gap-4 mt-8">
|
|
686
|
+
<!-- Back Button (hide on first step) -->
|
|
687
|
+
<uiButton
|
|
688
|
+
v-if="!wizard.isFirstStep.value"
|
|
689
|
+
:icon="ArrowLeftIcon"
|
|
690
|
+
color="gray"
|
|
691
|
+
@click="wizard.goToPrevious"
|
|
692
|
+
>
|
|
693
|
+
Back
|
|
694
|
+
</uiButton>
|
|
695
|
+
|
|
696
|
+
<!-- Next Button (show on non-last steps) -->
|
|
697
|
+
<uiButton
|
|
698
|
+
v-if="!wizard.isLastStep.value"
|
|
699
|
+
:icon="ArrowRightIcon"
|
|
700
|
+
color="primary"
|
|
701
|
+
@click="wizard.goToNext"
|
|
702
|
+
>
|
|
703
|
+
Next
|
|
704
|
+
</uiButton>
|
|
705
|
+
|
|
706
|
+
<!-- Complete Button (show on last step) -->
|
|
707
|
+
<uiButton
|
|
708
|
+
v-else
|
|
709
|
+
:icon="CheckIcon"
|
|
710
|
+
color="primary"
|
|
711
|
+
:loading="wizard.isCompleting.value"
|
|
712
|
+
:disabled="!wizard.allStepsValid.value"
|
|
713
|
+
@click="wizard.complete"
|
|
714
|
+
>
|
|
715
|
+
Create
|
|
716
|
+
</uiButton>
|
|
717
|
+
</div>
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## Checklist
|
|
721
|
+
|
|
722
|
+
- [ ] Astro page with layout
|
|
723
|
+
- [ ] Vue component with `useWizard` composable
|
|
724
|
+
- [ ] Step definitions with schemas or custom validation
|
|
725
|
+
- [ ] `Stepper` component for progress visualization
|
|
726
|
+
- [ ] `JsonSchemaForm` for schema-based steps
|
|
727
|
+
- [ ] Custom UI for complex selection steps
|
|
728
|
+
- [ ] Navigation buttons (Back/Next/Complete)
|
|
729
|
+
- [ ] Review step with summary card
|
|
730
|
+
- [ ] Missing fields alert on review step
|
|
731
|
+
- [ ] Loading state during completion
|
|
732
|
+
- [ ] `useNotifications` for success/error messages
|
|
733
|
+
- [ ] Progress indicator (optional)
|