@vue-skuilder/edit-ui 0.1.17 → 0.1.20

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.
@@ -1,59 +1,131 @@
1
1
  <template>
2
2
  <div class="navigation-strategy-editor">
3
- <div v-if="loading">
3
+ <div v-if="loading" class="text-center pa-4">
4
4
  <v-progress-circular indeterminate color="secondary"></v-progress-circular>
5
5
  </div>
6
- <div v-else>
7
- <h2 class="text-h5 mb-4">Navigation Strategies</h2>
8
-
9
- <div v-if="strategies.length === 0" class="no-strategies">
10
- <p>No navigation strategies defined for this course.</p>
6
+ <div v-else class="editor-layout">
7
+ <!-- Left Column: Strategy List -->
8
+ <div class="strategy-list-column">
9
+ <div class="d-flex align-center mb-2">
10
+ <h3 class="text-h6">Strategies</h3>
11
+ <v-spacer></v-spacer>
12
+ <v-btn size="small" color="primary" @click="startNewStrategy" density="compact">
13
+ <v-icon start size="small">mdi-plus</v-icon>
14
+ New
15
+ </v-btn>
16
+ </div>
17
+
18
+ <v-alert v-if="strategies.length === 0" type="info" density="compact">
19
+ No strategies defined
20
+ </v-alert>
21
+
22
+ <navigation-strategy-list
23
+ v-else
24
+ :strategies="strategies"
25
+ :default-strategy-id="defaultStrategyId"
26
+ @update:default-strategy="setDefaultStrategy"
27
+ @edit="editStrategy"
28
+ @delete="confirmDeleteStrategy"
29
+ />
11
30
  </div>
12
31
 
13
- <navigation-strategy-list
14
- v-else
15
- :strategies="strategies"
16
- :default-strategy-id="defaultStrategyId"
17
- @update:default-strategy="setDefaultStrategy"
18
- @edit="editStrategy"
19
- @delete="confirmDeleteStrategy"
20
- />
21
-
22
- <v-btn color="primary" class="mt-4" @click="openCreateDialog">
23
- <v-icon start>mdi-plus</v-icon>
24
- Add New Strategy
25
- </v-btn>
26
-
27
- <v-dialog v-model="showCreateDialog" max-width="600px">
28
- <v-card>
29
- <v-card-title>Create New Navigation Strategy</v-card-title>
30
- <v-card-text>
31
- <v-text-field v-model="newStrategy.name" label="Strategy Name" required></v-text-field>
32
- <v-text-field v-model="newStrategy.description" label="Description" required></v-text-field>
33
- <v-textarea
34
- v-model="newStrategy.cardIds"
35
- label="Card IDs"
36
- placeholder="Enter card IDs, one per line or separated by commas"
37
- rows="10"
38
- required
39
- ></v-textarea>
40
- </v-card-text>
41
- <v-card-actions>
42
- <v-spacer></v-spacer>
43
- <v-btn color="blue darken-1" text @click="showCreateDialog = false">Cancel</v-btn>
44
- <v-btn color="blue darken-1" text @click="saveNewStrategy">Save</v-btn>
45
- </v-card-actions>
46
- </v-card>
47
- </v-dialog>
32
+ <!-- Right Column: Strategy Form -->
33
+ <div class="strategy-form-column">
34
+ <div class="form-header d-flex align-center mb-3">
35
+ <h3 class="text-h6">{{ editingStrategy ? 'Edit Strategy' : 'New Strategy' }}</h3>
36
+ <v-spacer></v-spacer>
37
+ <v-btn
38
+ v-if="editingStrategy"
39
+ size="small"
40
+ variant="text"
41
+ @click="cancelEdit"
42
+ density="compact"
43
+ >
44
+ Cancel
45
+ </v-btn>
46
+ </div>
47
+
48
+ <v-select
49
+ v-model="newStrategy.type"
50
+ label="Type"
51
+ :items="strategyTypes"
52
+ item-title="label"
53
+ item-value="value"
54
+ density="compact"
55
+ class="mb-3"
56
+ ></v-select>
57
+
58
+ <v-text-field
59
+ v-model="newStrategy.name"
60
+ label="Name"
61
+ density="compact"
62
+ class="mb-3"
63
+ ></v-text-field>
64
+
65
+ <v-text-field
66
+ v-model="newStrategy.description"
67
+ label="Description"
68
+ density="compact"
69
+ class="mb-3"
70
+ ></v-text-field>
71
+
72
+ <!-- Strategy-specific configuration forms -->
73
+ <hardcoded-order-config-form
74
+ v-if="newStrategy.type === 'hardcoded'"
75
+ v-model="newStrategy.config"
76
+ />
77
+
78
+ <hierarchy-config-form
79
+ v-else-if="newStrategy.type === 'hierarchy'"
80
+ v-model="newStrategy.config"
81
+ :course-id="courseId"
82
+ />
83
+
84
+ <interference-config-form
85
+ v-else-if="newStrategy.type === 'interference'"
86
+ v-model="newStrategy.config"
87
+ :course-id="courseId"
88
+ />
89
+
90
+ <relative-priority-config-form
91
+ v-else-if="newStrategy.type === 'relativePriority'"
92
+ v-model="newStrategy.config"
93
+ :course-id="courseId"
94
+ />
95
+
96
+ <v-alert v-else type="warning" density="compact">
97
+ Unknown strategy type: {{ newStrategy.type }}
98
+ </v-alert>
99
+
100
+ <div class="form-actions mt-4">
101
+ <v-btn
102
+ color="primary"
103
+ @click="saveStrategy"
104
+ :disabled="!newStrategy.name"
105
+ size="small"
106
+ >
107
+ {{ editingStrategy ? 'Update' : 'Create' }}
108
+ </v-btn>
109
+ <v-btn
110
+ v-if="editingStrategy"
111
+ variant="text"
112
+ @click="cancelEdit"
113
+ size="small"
114
+ >
115
+ Cancel
116
+ </v-btn>
117
+ </div>
118
+ </div>
48
119
 
120
+ <!-- Delete Confirmation (still a dialog, but small) -->
49
121
  <v-dialog v-model="showDeleteConfirm" max-width="400px">
50
122
  <v-card>
51
- <v-card-title class="text-h5">Delete Strategy</v-card-title>
52
- <v-card-text> Are you sure you want to delete the strategy "{{ strategyToDelete?.name }}"? </v-card-text>
123
+ <v-card-title class="text-subtitle-1">Delete Strategy</v-card-title>
124
+ <v-card-text>Delete "{{ strategyToDelete?.name }}"?</v-card-text>
53
125
  <v-card-actions>
54
126
  <v-spacer></v-spacer>
55
- <v-btn color="error" @click="deleteStrategy">Delete</v-btn>
56
- <v-btn @click="showDeleteConfirm = false">Cancel</v-btn>
127
+ <v-btn size="small" @click="showDeleteConfirm = false">Cancel</v-btn>
128
+ <v-btn size="small" color="error" @click="deleteStrategy">Delete</v-btn>
57
129
  </v-card-actions>
58
130
  </v-card>
59
131
  </v-dialog>
@@ -65,14 +137,21 @@
65
137
  import { defineComponent } from 'vue';
66
138
  import type { ContentNavigationStrategyData } from '@vue-skuilder/db/src/core/types/contentNavigationStrategy';
67
139
  import NavigationStrategyList from './NavigationStrategyList.vue';
140
+ import HardcodedOrderConfigForm from './HardcodedOrderConfigForm.vue';
141
+ import HierarchyConfigForm from './HierarchyConfigForm.vue';
142
+ import InterferenceConfigForm from './InterferenceConfigForm.vue';
143
+ import RelativePriorityConfigForm from './RelativePriorityConfigForm.vue';
68
144
  import { getDataLayer, DocType, Navigators } from '@vue-skuilder/db';
69
- import { DocTypePrefixes } from '@vue-skuilder/db/src/core/types/types-legacy';
70
145
 
71
146
  export default defineComponent({
72
147
  name: 'NavigationStrategyEditor',
73
148
 
74
149
  components: {
75
150
  NavigationStrategyList,
151
+ HardcodedOrderConfigForm,
152
+ HierarchyConfigForm,
153
+ InterferenceConfigForm,
154
+ RelativePriorityConfigForm,
76
155
  },
77
156
 
78
157
  props: {
@@ -88,12 +167,19 @@ export default defineComponent({
88
167
  loading: true,
89
168
  showDeleteConfirm: false,
90
169
  strategyToDelete: null as ContentNavigationStrategyData | null,
91
- showCreateDialog: false,
170
+ strategyTypes: [
171
+ { label: 'Hardcoded Order', value: 'hardcoded' },
172
+ { label: 'Hierarchy Definition', value: 'hierarchy' },
173
+ { label: 'Interference Mitigator', value: 'interference' },
174
+ { label: 'Relative Priority', value: 'relativePriority' },
175
+ ],
92
176
  newStrategy: {
177
+ type: 'hardcoded' as string,
93
178
  name: '',
94
179
  description: '',
95
- cardIds: '',
180
+ config: { cardIds: [] } as any,
96
181
  },
182
+ editingStrategy: null as ContentNavigationStrategyData | null,
97
183
  defaultStrategyId: null as string | null,
98
184
  };
99
185
  },
@@ -102,7 +188,80 @@ export default defineComponent({
102
188
  await this.loadStrategies();
103
189
  },
104
190
 
191
+ watch: {
192
+ 'newStrategy.type'(newType: string) {
193
+ // Reset config when strategy type changes
194
+ this.newStrategy.config = this.getDefaultConfig(newType);
195
+ },
196
+ },
197
+
105
198
  methods: {
199
+ getDefaultConfig(strategyType: string) {
200
+ switch (strategyType) {
201
+ case 'hardcoded':
202
+ return { cardIds: [] };
203
+ case 'hierarchy':
204
+ return {
205
+ prerequisites: {},
206
+ delegateStrategy: 'elo',
207
+ };
208
+ case 'interference':
209
+ return {
210
+ interferenceSets: [],
211
+ maturityThreshold: {
212
+ minCount: 10,
213
+ minElapsedDays: 3,
214
+ },
215
+ defaultDecay: 0.8,
216
+ delegateStrategy: 'elo',
217
+ };
218
+ case 'relativePriority':
219
+ return {
220
+ tagPriorities: {},
221
+ defaultPriority: 0.5,
222
+ combineMode: 'max',
223
+ priorityInfluence: 0.5,
224
+ delegateStrategy: 'elo',
225
+ };
226
+ default:
227
+ return {};
228
+ }
229
+ },
230
+
231
+ // Map implementing class to strategy type
232
+ getStrategyTypeFromClass(implementingClass: string): string {
233
+ switch (implementingClass) {
234
+ case Navigators.HARDCODED:
235
+ return 'hardcoded';
236
+ case Navigators.HIERARCHY:
237
+ return 'hierarchy';
238
+ case Navigators.INTERFERENCE:
239
+ return 'interference';
240
+ case Navigators.RELATIVE_PRIORITY:
241
+ return 'relativePriority';
242
+ default:
243
+ return 'hardcoded';
244
+ }
245
+ },
246
+
247
+ // Parse serialized data back to config object
248
+ parseSerializedData(strategyType: string, serializedData: string): any {
249
+ try {
250
+ const parsed = JSON.parse(serializedData);
251
+
252
+ if (strategyType === 'hardcoded') {
253
+ // Hardcoded stores just the array, wrap it
254
+ return { cardIds: Array.isArray(parsed) ? parsed : [] };
255
+ } else {
256
+ // Other strategies store the full config object
257
+ return parsed;
258
+ }
259
+ } catch (error) {
260
+ console.error('Failed to parse strategy data:', error);
261
+ return this.getDefaultConfig(strategyType);
262
+ }
263
+ },
264
+
106
265
  async loadStrategies() {
107
266
  this.loading = true;
108
267
  try {
@@ -156,18 +315,68 @@ export default defineComponent({
156
315
  }
157
316
  },
158
317
 
159
- openCreateDialog() {
160
- this.newStrategy = { name: '', description: '', cardIds: '' };
161
- this.showCreateDialog = true;
318
+ startNewStrategy() {
319
+ const defaultType = 'hardcoded';
320
+ this.newStrategy = {
321
+ type: defaultType,
322
+ name: '',
323
+ description: '',
324
+ config: this.getDefaultConfig(defaultType),
325
+ };
326
+ this.editingStrategy = null;
162
327
  },
163
328
 
164
- async saveNewStrategy() {
165
- if (!this.newStrategy.name || !this.newStrategy.cardIds) {
166
- // Basic validation
167
- alert('Strategy Name and Card IDs are required.');
329
+ cancelEdit() {
330
+ this.startNewStrategy();
331
+ },
332
+
333
+ editStrategy(strategy: ContentNavigationStrategyData) {
334
+ const strategyType = this.getStrategyTypeFromClass(strategy.implementingClass);
335
+ const config = this.parseSerializedData(strategyType, strategy.serializedData);
336
+
337
+ this.newStrategy = {
338
+ type: strategyType,
339
+ name: strategy.name,
340
+ description: strategy.description,
341
+ config,
342
+ };
343
+ this.editingStrategy = strategy;
344
+ },
345
+
346
+ async saveStrategy() {
347
+ if (!this.newStrategy.name) {
348
+ alert('Strategy Name is required.');
349
+ return;
350
+ }
351
+
352
+ // Validate config based on strategy type
353
+ if (this.newStrategy.type === 'hardcoded' && this.newStrategy.config.cardIds.length === 0) {
354
+ alert('At least one card ID is required for hardcoded order strategy.');
168
355
  return;
169
356
  }
170
357
 
358
+ if (this.newStrategy.type === 'hierarchy') {
359
+ const prereqCount = Object.keys(this.newStrategy.config.prerequisites || {}).length;
360
+ if (prereqCount === 0) {
361
+ alert('At least one prerequisite rule is required for hierarchy strategy.');
362
+ return;
363
+ }
364
+ }
365
+
366
+ if (this.newStrategy.type === 'interference') {
367
+ if (!this.newStrategy.config.interferenceSets || this.newStrategy.config.interferenceSets.length === 0) {
368
+ alert('At least one interference group is required for interference strategy.');
369
+ return;
370
+ }
371
+ }
372
+
373
+ if (this.newStrategy.type === 'relativePriority') {
374
+ if (!this.newStrategy.config.tagPriorities || Object.keys(this.newStrategy.config.tagPriorities).length === 0) {
375
+ alert('At least one tag priority must be set for relative priority strategy.');
376
+ return;
377
+ }
378
+ }
379
+
171
380
  this.loading = true;
172
381
  try {
173
382
  const dataLayer = getDataLayer();
@@ -175,27 +384,53 @@ export default defineComponent({
175
384
  const userName = userDB.getUsername();
176
385
  const courseDB = dataLayer.getCourseDB(this.courseId);
177
386
 
178
- // Process card IDs
179
- const cardIdArray = this.newStrategy.cardIds
180
- .split(/[\n,]+/)
181
- .map((id) => id.trim())
182
- .filter((id) => id);
183
-
184
- const strategyData: ContentNavigationStrategyData = {
185
- _id: `NAVIGATION_STRATEGY-${Date.now()}`,
186
- docType: DocType.NAVIGATION_STRATEGY,
187
- name: this.newStrategy.name,
188
- description: this.newStrategy.description,
189
- implementingClass: Navigators.HARDCODED,
190
- author: userName,
191
- course: this.courseId,
192
- serializedData: JSON.stringify(cardIdArray),
387
+ // Map strategy type to implementing class
388
+ const implementingClassMap: Record<string, string> = {
389
+ hardcoded: Navigators.HARDCODED,
390
+ hierarchy: Navigators.HIERARCHY,
391
+ interference: Navigators.INTERFERENCE,
392
+ relativePriority: Navigators.RELATIVE_PRIORITY,
193
393
  };
194
394
 
195
- await courseDB.addNavigationStrategy(strategyData);
395
+ // Serialize config based on strategy type
396
+ let serializedData: string;
397
+ if (this.newStrategy.type === 'hardcoded') {
398
+ // Hardcoded stores just the array of card IDs
399
+ serializedData = JSON.stringify(this.newStrategy.config.cardIds);
400
+ } else {
401
+ // Other strategies store their full config object
402
+ serializedData = JSON.stringify(this.newStrategy.config);
403
+ }
404
+
405
+ if (this.editingStrategy) {
406
+ // Update existing strategy
407
+ const strategyData: ContentNavigationStrategyData = {
408
+ ...this.editingStrategy,
409
+ name: this.newStrategy.name,
410
+ description: this.newStrategy.description,
411
+ implementingClass: implementingClassMap[this.newStrategy.type],
412
+ serializedData,
413
+ };
414
+
415
+ await courseDB.updateNavigationStrategy(this.editingStrategy._id, strategyData);
416
+ } else {
417
+ // Create new strategy
418
+ const strategyData: ContentNavigationStrategyData = {
419
+ _id: `NAVIGATION_STRATEGY-${Date.now()}`,
420
+ docType: DocType.NAVIGATION_STRATEGY,
421
+ name: this.newStrategy.name,
422
+ description: this.newStrategy.description,
423
+ implementingClass: implementingClassMap[this.newStrategy.type],
424
+ author: userName,
425
+ course: this.courseId,
426
+ serializedData,
427
+ };
428
+
429
+ await courseDB.addNavigationStrategy(strategyData);
430
+ }
196
431
 
197
- this.showCreateDialog = false;
198
432
  await this.loadStrategies(); // Refresh the list
433
+ this.startNewStrategy(); // Reset form
199
434
  } catch (error) {
200
435
  console.error('Failed to save new strategy:', error);
201
436
  alert('Error saving strategy. See console for details.');
@@ -203,11 +438,6 @@ export default defineComponent({
203
438
  this.loading = false;
204
439
  },
205
440
 
206
- editStrategy(strategy: ContentNavigationStrategyData) {
207
- // Strategy editing is not yet implemented
208
- console.log(`Editing strategy ${strategy._id} is not yet implemented`);
209
- },
210
-
211
441
  confirmDeleteStrategy(strategy: ContentNavigationStrategyData) {
212
442
  this.strategyToDelete = strategy;
213
443
  this.showDeleteConfirm = true;
@@ -254,14 +484,45 @@ export default defineComponent({
254
484
 
255
485
  <style scoped>
256
486
  .navigation-strategy-editor {
257
- padding: 16px;
487
+ height: 100%;
488
+ overflow: hidden;
489
+ }
490
+
491
+ .editor-layout {
492
+ display: grid;
493
+ grid-template-columns: 400px 1fr;
494
+ gap: 24px;
495
+ height: 100%;
496
+ overflow: hidden;
497
+ }
498
+
499
+ .strategy-list-column {
500
+ overflow-y: auto;
501
+ padding-right: 12px;
502
+ }
503
+
504
+ .strategy-form-column {
505
+ overflow-y: auto;
506
+ padding-left: 12px;
507
+ border-left: 1px solid #e0e0e0;
508
+ }
509
+
510
+ .form-actions {
511
+ display: flex;
512
+ gap: 8px;
258
513
  }
259
514
 
260
- .no-strategies {
261
- margin: 20px 0;
262
- padding: 20px;
263
- background-color: #f5f5f5;
264
- border-radius: 4px;
265
- text-align: center;
515
+ @media (max-width: 960px) {
516
+ .editor-layout {
517
+ grid-template-columns: 1fr;
518
+ grid-template-rows: auto 1fr;
519
+ }
520
+
521
+ .strategy-form-column {
522
+ border-left: none;
523
+ border-top: 1px solid #e0e0e0;
524
+ padding-left: 0;
525
+ padding-top: 12px;
526
+ }
266
527
  }
267
528
  </style>
@@ -1,41 +1,45 @@
1
1
  <template>
2
2
  <div class="navigation-strategy-list">
3
- <v-radio-group :model-value="defaultStrategyId" @update:modelValue="$emit('update:defaultStrategy', $event)">
4
- <v-list>
3
+ <v-radio-group :model-value="defaultStrategyId" @update:modelValue="$emit('update:defaultStrategy', $event)" density="compact">
4
+ <v-list density="compact">
5
5
  <v-list-item
6
6
  v-for="strategy in strategies"
7
7
  :key="strategy._id"
8
- lines="three"
8
+ lines="two"
9
9
  >
10
10
  <template #prepend>
11
11
  <v-radio :value="strategy._id"></v-radio>
12
12
  </template>
13
13
 
14
- <v-list-item-title class="text-h6">
14
+ <v-list-item-title class="text-subtitle-2">
15
15
  {{ strategy.name }}
16
16
  </v-list-item-title>
17
17
 
18
- <v-list-item-subtitle>{{ strategy.description }}</v-list-item-subtitle>
19
-
20
- <v-list-item-subtitle class="strategy-details mt-2">
21
- <div><strong>Type:</strong> {{ strategy.implementingClass }}</div>
22
- <div v-if="strategy.serializedData"><strong>Configuration:</strong> {{ getDisplayConfig(strategy) }}</div>
18
+ <v-list-item-subtitle class="text-caption">
19
+ {{ strategy.implementingClass }}
20
+ <span v-if="strategy.description"> • {{ strategy.description }}</span>
23
21
  </v-list-item-subtitle>
24
22
 
25
23
  <template #append>
26
24
  <div class="d-flex">
27
- <v-btn icon size="small" title="Edit Strategy (coming soon)" class="mr-1" disabled>
28
- <v-icon>mdi-pencil</v-icon>
25
+ <v-btn
26
+ icon
27
+ size="x-small"
28
+ variant="text"
29
+ title="Edit Strategy"
30
+ @click="$emit('edit', strategy)"
31
+ >
32
+ <v-icon size="small">mdi-pencil</v-icon>
29
33
  </v-btn>
30
34
 
31
35
  <v-btn
32
36
  icon
33
- size="small"
37
+ size="x-small"
38
+ variant="text"
34
39
  title="Delete Strategy"
35
- class="mr-1"
36
40
  @click="$emit('delete', strategy)"
37
41
  >
38
- <v-icon>mdi-delete</v-icon>
42
+ <v-icon size="small">mdi-delete</v-icon>
39
43
  </v-btn>
40
44
  </div>
41
45
  </template>
@@ -89,13 +93,6 @@ export default defineComponent({
89
93
 
90
94
  <style scoped>
91
95
  .navigation-strategy-list {
92
- margin: 16px 0;
93
- }
94
-
95
-
96
-
97
- .strategy-details {
98
- font-size: 0.9em;
99
- color: rgba(0, 0, 0, 0.6);
96
+ /* Compact list styling */
100
97
  }
101
98
  </style>