@vue-skuilder/platform-ui 0.1.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.
- package/LICENCE +661 -0
- package/README.md +64 -0
- package/dist/assets/Roboto-Black-B0ZKieaB.woff +0 -0
- package/dist/assets/Roboto-Black-VhoA2qKx.woff2 +0 -0
- package/dist/assets/Roboto-BlackItalic-D0gSnuIb.woff +0 -0
- package/dist/assets/Roboto-BlackItalic-D4yie1YO.woff2 +0 -0
- package/dist/assets/Roboto-Bold-D9plYbeK.woff +0 -0
- package/dist/assets/Roboto-Bold-hN3duQhD.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-BWDm51uc.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-CyLKvOHD.woff +0 -0
- package/dist/assets/Roboto-Light-Cu-PAxXt.woff +0 -0
- package/dist/assets/Roboto-Light-DHTugVNA.woff2 +0 -0
- package/dist/assets/Roboto-LightItalic-CZg5kHIB.woff +0 -0
- package/dist/assets/Roboto-LightItalic-JQyp2Y3P.woff2 +0 -0
- package/dist/assets/Roboto-Medium-ByKogCTi.woff2 +0 -0
- package/dist/assets/Roboto-Medium-b81vv18W.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-DFQ-RYa0.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-i1eR0KbF.woff2 +0 -0
- package/dist/assets/Roboto-Regular-BX5l9hRW.woff +0 -0
- package/dist/assets/Roboto-Regular-C6rbFxYz.woff2 +0 -0
- package/dist/assets/Roboto-RegularItalic-BjnLZsam.woff +0 -0
- package/dist/assets/Roboto-RegularItalic-CvPUdkvM.woff2 +0 -0
- package/dist/assets/Roboto-Thin-BfJvJcog.woff +0 -0
- package/dist/assets/Roboto-Thin-NicBC1pN.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-CKlCjrO_.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-DnIWFxRE.woff +0 -0
- package/dist/assets/index-CQ-sNKGW.css +14 -0
- package/dist/assets/index-EbqpUgvM.js +161 -0
- package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/assets/workbox-window.prod.es5-p40uij6f.js +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/img/icons/safari-pinned-tab.svg +149 -0
- package/dist/index.html +19 -0
- package/dist/manifest.json +20 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/robots.txt +2 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-1be04862.js +1 -0
- package/package.json +105 -0
- package/src/App.vue +156 -0
- package/src/ENVIRONMENT_VARS.ts +79 -0
- package/src/components/Classrooms/ClassroomCtrlPanel.vue +206 -0
- package/src/components/Classrooms/CreateClassroom.vue +159 -0
- package/src/components/Classrooms/JoinCode.vue +83 -0
- package/src/components/Courses/CourseCardBrowser.vue +365 -0
- package/src/components/Courses/CourseEditor.vue +164 -0
- package/src/components/Courses/CourseInformation.vue +164 -0
- package/src/components/Courses/CourseRouter.vue +116 -0
- package/src/components/Courses/CourseStubCard.vue +76 -0
- package/src/components/Courses/EloModeration.vue +122 -0
- package/src/components/Courses/TagInformation.vue +209 -0
- package/src/components/Edit/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/Edit/BulkImportView.vue +633 -0
- package/src/components/Edit/CardBrowser.vue +79 -0
- package/src/components/Edit/ComponentRegistration/ComponentRegistration.vue +235 -0
- package/src/components/Edit/ComponentRegistration/UnregisteredComponentsTable.vue +19 -0
- package/src/components/Edit/CourseEditor.vue +162 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue +170 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyList.vue +92 -0
- package/src/components/Edit/TagsInput.vue +247 -0
- package/src/components/Edit/ViewableDataInputForm/DataInputForm.vue +524 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/Edit/ViewableDataInputForm/OptionsFieldInput.ts +161 -0
- package/src/components/Study/SessionConfiguration.vue +371 -0
- package/src/components/TextSwap.vue +65 -0
- package/src/components/User/UserStats.vue +30 -0
- package/src/dev/DataInputFormTester.vue +117 -0
- package/src/dev/readme.md +3 -0
- package/src/enums.ts +0 -0
- package/src/glyphs.txt +933 -0
- package/src/main.ts +45 -0
- package/src/plugins/vuetify.ts +41 -0
- package/src/registerServiceWorker.ts +18 -0
- package/src/router.ts +184 -0
- package/src/server/index.spec.ts +192 -0
- package/src/server/index.ts +71 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/store.mock.ts +122 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/types/shims-vuetify.d.ts +12 -0
- package/src/types/svg.d.ts +4 -0
- package/src/utils/bulkImport/index.ts +94 -0
- package/src/views/About.vue +29 -0
- package/src/views/Admin.vue +128 -0
- package/src/views/Classrooms.vue +258 -0
- package/src/views/Courses.vue +265 -0
- package/src/views/Home.vue +154 -0
- package/src/views/Login.vue +75 -0
- package/src/views/ReleaseNotes.vue +20 -0
- package/src/views/SignUp.vue +32 -0
- package/src/views/Study.vue +261 -0
- package/src/views/User.vue +109 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<h3>DataShapes</h3>
|
|
4
|
+
<ul>
|
|
5
|
+
<li v-for="dataShape in dataShapes" :key="dataShape.name" class="ma-2">
|
|
6
|
+
<v-btn v-if="!dataShape.registered" size="small" @click="registerShape(dataShape.name)"> Register </v-btn>
|
|
7
|
+
<span v-else class="inset"> (Registered) </span>
|
|
8
|
+
{{ dataShape.name }}
|
|
9
|
+
<ul>
|
|
10
|
+
<div v-for="view in dataShape.dataShape.views" :key="view.name">
|
|
11
|
+
<li v-if="view">
|
|
12
|
+
{{ view.name }}
|
|
13
|
+
</li>
|
|
14
|
+
</div>
|
|
15
|
+
</ul>
|
|
16
|
+
</li>
|
|
17
|
+
</ul>
|
|
18
|
+
|
|
19
|
+
<h3>Questions</h3>
|
|
20
|
+
<ul>
|
|
21
|
+
<li v-for="question in questions" :key="question.name" class="ma-2">
|
|
22
|
+
<v-btn v-if="!question.registered" size="small" @click="registerQuestionView(question.name)"> Register </v-btn>
|
|
23
|
+
<span v-else class="inset"> (Registered) </span>
|
|
24
|
+
{{ question.name }}
|
|
25
|
+
</li>
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { defineComponent } from 'vue';
|
|
32
|
+
import { Displayable } from '@vue-skuilder/common-ui';
|
|
33
|
+
import { allCourses } from '@vue-skuilder/courses';
|
|
34
|
+
import { getDataLayer, CourseDBInterface } from '@vue-skuilder/db';
|
|
35
|
+
import {
|
|
36
|
+
NameSpacer,
|
|
37
|
+
QuestionDescriptor,
|
|
38
|
+
CourseConfig,
|
|
39
|
+
DataShape55,
|
|
40
|
+
QuestionType55,
|
|
41
|
+
DataShape,
|
|
42
|
+
} from '@vue-skuilder/common';
|
|
43
|
+
import * as _ from 'lodash';
|
|
44
|
+
import { getCurrentUser } from '@vue-skuilder/common-ui';
|
|
45
|
+
|
|
46
|
+
export interface DataShapeRegistrationStatus {
|
|
47
|
+
name: string;
|
|
48
|
+
course: string;
|
|
49
|
+
dataShape: DataShape;
|
|
50
|
+
registered: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QuestionRegistrationStatus {
|
|
54
|
+
name: string;
|
|
55
|
+
course: string;
|
|
56
|
+
question: typeof Displayable;
|
|
57
|
+
registered: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default defineComponent({
|
|
61
|
+
name: 'ComponentRegistration',
|
|
62
|
+
|
|
63
|
+
props: {
|
|
64
|
+
course: {
|
|
65
|
+
type: String,
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
dataShapes: [] as (DataShapeRegistrationStatus & { displayable: typeof Displayable })[],
|
|
73
|
+
questions: [] as QuestionRegistrationStatus[],
|
|
74
|
+
courseDatashapes: [] as DataShape55[],
|
|
75
|
+
courseQuestionTypes: [] as QuestionType55[],
|
|
76
|
+
courseConfig: undefined as CourseConfig | undefined,
|
|
77
|
+
courseDB: null as CourseDBInterface | null,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async created() {
|
|
82
|
+
this.courseDB = getDataLayer().getCourseDB(this.course);
|
|
83
|
+
this.courseConfig = await this.courseDB.getCourseConfig();
|
|
84
|
+
this.courseDatashapes = this.courseConfig.dataShapes;
|
|
85
|
+
this.courseQuestionTypes = this.courseConfig.questionTypes;
|
|
86
|
+
|
|
87
|
+
const dataShapeData = allCourses.allDataShapes();
|
|
88
|
+
|
|
89
|
+
allCourses.allDataShapesRaw().forEach((ds) => {
|
|
90
|
+
console.log(`[ComponentRegistration] Datashape:\n${JSON.stringify(ds)}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
dataShapeData.forEach((shape) => {
|
|
94
|
+
const index = this.courseDatashapes.find((test) => {
|
|
95
|
+
return test.name === NameSpacer.getDataShapeString(shape);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.dataShapes.push({
|
|
99
|
+
name: shape.dataShape,
|
|
100
|
+
course: shape.course,
|
|
101
|
+
dataShape: allCourses.getDataShape(shape),
|
|
102
|
+
registered: index !== undefined,
|
|
103
|
+
displayable: shape.displayable,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.dataShapes = _.sortBy(this.dataShapes, ['registered', 'name']);
|
|
108
|
+
|
|
109
|
+
const courseNameList = allCourses.courses.map((course) => course.name);
|
|
110
|
+
const questionData: Array<[QuestionDescriptor, typeof Displayable]> = [];
|
|
111
|
+
|
|
112
|
+
courseNameList.forEach((course) => {
|
|
113
|
+
const courseQs = allCourses.getCourse(course)!.questions;
|
|
114
|
+
|
|
115
|
+
courseQs.forEach((courseQ) => {
|
|
116
|
+
questionData.push([
|
|
117
|
+
{
|
|
118
|
+
course,
|
|
119
|
+
questionType: courseQ.name,
|
|
120
|
+
},
|
|
121
|
+
courseQ,
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
questionData.forEach((question) => {
|
|
127
|
+
const index = this.courseQuestionTypes.find((test) => {
|
|
128
|
+
return NameSpacer.getQuestionString(question[0]) === test.name;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.questions.push({
|
|
132
|
+
course: question[0].course,
|
|
133
|
+
name: question[1].name,
|
|
134
|
+
registered: index !== undefined,
|
|
135
|
+
question: question[1],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
methods: {
|
|
141
|
+
async registerShape(shapeName: string) {
|
|
142
|
+
const shape = this.dataShapes.find((findShape) => {
|
|
143
|
+
return findShape.name === shapeName;
|
|
144
|
+
})!;
|
|
145
|
+
|
|
146
|
+
this.courseConfig!.dataShapes.push({
|
|
147
|
+
name: NameSpacer.getDataShapeString({
|
|
148
|
+
dataShape: shape.name,
|
|
149
|
+
course: shape.course,
|
|
150
|
+
}),
|
|
151
|
+
questionTypes: [],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const update = await this.courseDB!.updateCourseConfig(this.courseConfig!);
|
|
155
|
+
|
|
156
|
+
if (update.ok) {
|
|
157
|
+
shape.registered = true;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async registerQuestionView(questionName: string) {
|
|
162
|
+
const question = this.questions.find((q) => {
|
|
163
|
+
return q.name === questionName;
|
|
164
|
+
})!;
|
|
165
|
+
|
|
166
|
+
const nsQuestionName = NameSpacer.getQuestionString({
|
|
167
|
+
course: question.course,
|
|
168
|
+
questionType: question.name,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.courseConfig!.questionTypes.push({
|
|
172
|
+
name: nsQuestionName,
|
|
173
|
+
viewList: question.question.views.map((v) => {
|
|
174
|
+
if (v.name) {
|
|
175
|
+
return v.name;
|
|
176
|
+
} else {
|
|
177
|
+
return 'unnamedComponent';
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
dataShapeList: question.question.dataShapes.map((d) =>
|
|
181
|
+
NameSpacer.getDataShapeString({
|
|
182
|
+
course: question.course,
|
|
183
|
+
dataShape: d.name,
|
|
184
|
+
})
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
question.question.dataShapes.forEach((ds) => {
|
|
189
|
+
const nsDatashapeName = NameSpacer.getDataShapeString({
|
|
190
|
+
course: question.course,
|
|
191
|
+
dataShape: ds.name,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
for (const db of this.courseConfig!.dataShapes) {
|
|
195
|
+
if (db.name === nsDatashapeName) {
|
|
196
|
+
db.questionTypes.push(nsQuestionName);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const update = await this.courseDB!.updateCourseConfig(this.courseConfig!);
|
|
202
|
+
const u = await getCurrentUser();
|
|
203
|
+
|
|
204
|
+
if (update.ok) {
|
|
205
|
+
question.registered = true;
|
|
206
|
+
console.log(`[ComponentRegistration]
|
|
207
|
+
Question: ${JSON.stringify(question)}
|
|
208
|
+
CourseID: ${this.course}
|
|
209
|
+
`);
|
|
210
|
+
if (question.question.seedData) {
|
|
211
|
+
console.log(`[ComponentRegistration] Question has seed data!`);
|
|
212
|
+
question.question.seedData.forEach((d) => {
|
|
213
|
+
this.courseDB!.addNote(question.course, question.question.dataShapes[0], d, u.getUsername(), []);
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`[ComponentRegistration] Question has NO seed data!`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<style scoped>
|
|
225
|
+
div {
|
|
226
|
+
margin-top: 15px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.inset {
|
|
230
|
+
background-color: rgb(240, 240, 243);
|
|
231
|
+
font-size: smaller;
|
|
232
|
+
padding: 2px;
|
|
233
|
+
border-radius: 2px;
|
|
234
|
+
}
|
|
235
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<table>
|
|
4
|
+
<thead>
|
|
5
|
+
<td></td>
|
|
6
|
+
</thead>
|
|
7
|
+
</table>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { defineComponent } from 'vue';
|
|
13
|
+
|
|
14
|
+
// [ ] delete this file
|
|
15
|
+
|
|
16
|
+
export default defineComponent({
|
|
17
|
+
name: 'UnregisteredComponentsTable',
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="course" class="courseEditor">
|
|
3
|
+
<div v-if="loading">
|
|
4
|
+
<v-progress-circular indeterminate color="secondary"></v-progress-circular>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-else>
|
|
7
|
+
<h1 class="text-h4">
|
|
8
|
+
<router-link to="/q">Quilts</router-link> /
|
|
9
|
+
<router-link :to="`/q/${courseConfig ? courseConfig.name : course}`">{{ courseConfig?.name }}</router-link>
|
|
10
|
+
</h1>
|
|
11
|
+
|
|
12
|
+
<v-tabs v-model="currentTab" bg-color="primary" grow>
|
|
13
|
+
<v-tab value="single">Single Card Input</v-tab>
|
|
14
|
+
<v-tab value="bulk">Bulk Import</v-tab>
|
|
15
|
+
<v-tab value="registration">Navigation</v-tab>
|
|
16
|
+
<v-tab value="registration">Component Registration</v-tab>
|
|
17
|
+
</v-tabs>
|
|
18
|
+
|
|
19
|
+
<v-window v-model="currentTab">
|
|
20
|
+
<v-window-item value="single">
|
|
21
|
+
<v-container fluid>
|
|
22
|
+
<v-select
|
|
23
|
+
v-model="selectedShape"
|
|
24
|
+
label="What kind of content are you adding?"
|
|
25
|
+
:items="registeredDataShapes.map((shape) => shape.name)"
|
|
26
|
+
class="mt-4"
|
|
27
|
+
/>
|
|
28
|
+
<data-input-form
|
|
29
|
+
v-if="selectedShape !== '' && courseConfig && dataShape"
|
|
30
|
+
:data-shape="dataShape"
|
|
31
|
+
:course-cfg="courseConfig"
|
|
32
|
+
/>
|
|
33
|
+
</v-container>
|
|
34
|
+
</v-window-item>
|
|
35
|
+
|
|
36
|
+
<v-window-item value="bulk">
|
|
37
|
+
<v-container fluid>
|
|
38
|
+
<bulk-import-view v-if="courseConfig" :course-cfg="courseConfig" class="mt-4" />
|
|
39
|
+
</v-container>
|
|
40
|
+
</v-window-item>
|
|
41
|
+
|
|
42
|
+
<v-window-item value="registration">
|
|
43
|
+
<v-container fluid>
|
|
44
|
+
<component-registration :course="course" class="mt-4" />
|
|
45
|
+
</v-container>
|
|
46
|
+
</v-window-item>
|
|
47
|
+
|
|
48
|
+
<v-window-item value="navigation">
|
|
49
|
+
<v-container fluid>
|
|
50
|
+
<navigation-strategy-editor :course-id="course" />
|
|
51
|
+
</v-container>
|
|
52
|
+
</v-window-item>
|
|
53
|
+
|
|
54
|
+
</v-window>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script lang="ts">
|
|
60
|
+
import { defineComponent } from 'vue';
|
|
61
|
+
import ComponentRegistration from '@/components/Edit/ComponentRegistration/ComponentRegistration.vue';
|
|
62
|
+
import NavigationStrategyEditor from '@/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue';
|
|
63
|
+
import { allCourses } from '@vue-skuilder/courses';
|
|
64
|
+
import { BlanksCard, BlanksCardDataShapes } from '@vue-skuilder/courses';
|
|
65
|
+
import { CourseConfig, NameSpacer, DataShape } from '@vue-skuilder/common';
|
|
66
|
+
import DataInputForm from './ViewableDataInputForm/DataInputForm.vue';
|
|
67
|
+
import BulkImportView from './BulkImportView.vue'; // Added import
|
|
68
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
69
|
+
import { useDataInputFormStore } from '@/stores/useDataInputFormStore';
|
|
70
|
+
|
|
71
|
+
export default defineComponent({
|
|
72
|
+
name: 'CourseEditor',
|
|
73
|
+
|
|
74
|
+
components: {
|
|
75
|
+
DataInputForm,
|
|
76
|
+
ComponentRegistration,
|
|
77
|
+
NavigationStrategyEditor,
|
|
78
|
+
BulkImportView,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
props: {
|
|
82
|
+
course: {
|
|
83
|
+
type: String,
|
|
84
|
+
required: true,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
data() {
|
|
89
|
+
return {
|
|
90
|
+
registeredDataShapes: [] as DataShape[],
|
|
91
|
+
dataShapes: [] as DataShape[],
|
|
92
|
+
selectedShape: BlanksCard.dataShapes[0].name,
|
|
93
|
+
courseConfig: null as CourseConfig | null,
|
|
94
|
+
dataShape: BlanksCardDataShapes[0] as DataShape,
|
|
95
|
+
loading: true,
|
|
96
|
+
currentTab: 'single',
|
|
97
|
+
dataInputFormStore: useDataInputFormStore(),
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
watch: {
|
|
102
|
+
selectedShape: {
|
|
103
|
+
handler(value?: string) {
|
|
104
|
+
if (value) {
|
|
105
|
+
this.dataShape = this.getDataShape(value);
|
|
106
|
+
this.dataInputFormStore.setDataShape(this.dataShape);
|
|
107
|
+
|
|
108
|
+
this.dataInputFormStore.dataInputForm.course = this.courseConfig;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async created() {
|
|
115
|
+
this.courseConfig = await getDataLayer().getCoursesDB().getCourseConfig(this.course);
|
|
116
|
+
|
|
117
|
+
// for testing getCourseTagStubs...
|
|
118
|
+
// log(JSON.stringify(await getCourseTagStubs(this.course)));
|
|
119
|
+
|
|
120
|
+
// this.dataShapes = BaseCards.dataShapes;
|
|
121
|
+
// this.registeredDataShapes = BaseCards.dataShapes;
|
|
122
|
+
// BaseCards.dataShapes.forEach((shape) => {
|
|
123
|
+
// this.dataShapes.push(shape);
|
|
124
|
+
// this.registeredDataShapes.push(shape);
|
|
125
|
+
// });
|
|
126
|
+
|
|
127
|
+
// #55 make all 'programmed' datashapes available, rather than
|
|
128
|
+
// the previous code-based name scoping
|
|
129
|
+
allCourses.courses.forEach((course) => {
|
|
130
|
+
course.questions.forEach((question) => {
|
|
131
|
+
question.dataShapes.forEach((ds) => {
|
|
132
|
+
this.dataShapes.push(ds);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.courseConfig.dataShapes.forEach((ds) => {
|
|
138
|
+
this.registeredDataShapes.push(
|
|
139
|
+
this.dataShapes.find((shape) => {
|
|
140
|
+
return shape.name === NameSpacer.getDataShapeDescriptor(ds.name).dataShape;
|
|
141
|
+
})!
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.loading = false;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
methods: {
|
|
149
|
+
getDataShape(shapeName: string): DataShape {
|
|
150
|
+
return this.dataShapes.find((shape) => {
|
|
151
|
+
return shape.name === shapeName;
|
|
152
|
+
})!;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<style scoped>
|
|
159
|
+
div {
|
|
160
|
+
margin-top: 15px;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="navigation-strategy-editor">
|
|
3
|
+
<div v-if="loading">
|
|
4
|
+
<v-progress-circular indeterminate color="secondary"></v-progress-circular>
|
|
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>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<navigation-strategy-list
|
|
14
|
+
v-else
|
|
15
|
+
:strategies="strategies"
|
|
16
|
+
@edit="editStrategy"
|
|
17
|
+
@delete="confirmDeleteStrategy"
|
|
18
|
+
/>
|
|
19
|
+
|
|
20
|
+
<v-btn color="primary" class="mt-4" disabled title="New strategy types coming soon">
|
|
21
|
+
<v-icon start>mdi-plus</v-icon>
|
|
22
|
+
Add New Strategy
|
|
23
|
+
</v-btn>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
<v-dialog v-model="showDeleteConfirm" max-width="400px">
|
|
28
|
+
<v-card>
|
|
29
|
+
<v-card-title class="text-h5">Delete Strategy</v-card-title>
|
|
30
|
+
<v-card-text> Are you sure you want to delete the strategy "{{ strategyToDelete?.name }}"? </v-card-text>
|
|
31
|
+
<v-card-actions>
|
|
32
|
+
<v-spacer></v-spacer>
|
|
33
|
+
<v-btn color="error" @click="deleteStrategy">Delete</v-btn>
|
|
34
|
+
<v-btn @click="showDeleteConfirm = false">Cancel</v-btn>
|
|
35
|
+
</v-card-actions>
|
|
36
|
+
</v-card>
|
|
37
|
+
</v-dialog>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { defineComponent } from 'vue';
|
|
44
|
+
import type { ContentNavigationStrategyData } from '@vue-skuilder/db/src/core/types/contentNavigationStrategy';
|
|
45
|
+
import NavigationStrategyList from './NavigationStrategyList.vue';
|
|
46
|
+
import { getDataLayer, DocType, Navigators } from '@vue-skuilder/db';
|
|
47
|
+
|
|
48
|
+
export default defineComponent({
|
|
49
|
+
name: 'NavigationStrategyEditor',
|
|
50
|
+
|
|
51
|
+
components: {
|
|
52
|
+
NavigationStrategyList,
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
props: {
|
|
56
|
+
courseId: {
|
|
57
|
+
type: String,
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
data() {
|
|
63
|
+
return {
|
|
64
|
+
strategies: [] as ContentNavigationStrategyData[],
|
|
65
|
+
loading: true,
|
|
66
|
+
showDeleteConfirm: false,
|
|
67
|
+
strategyToDelete: null as ContentNavigationStrategyData | null
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async created() {
|
|
72
|
+
await this.loadStrategies();
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
methods: {
|
|
76
|
+
async loadStrategies() {
|
|
77
|
+
this.loading = true;
|
|
78
|
+
try {
|
|
79
|
+
const dataLayer = getDataLayer();
|
|
80
|
+
const courseDB = dataLayer.getCourseDB(this.courseId);
|
|
81
|
+
|
|
82
|
+
// Get all navigation strategies
|
|
83
|
+
this.strategies = await courseDB.getAllNavigationStrategies();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Failed to load navigation strategies:', error);
|
|
86
|
+
// In case of error, use a placeholder
|
|
87
|
+
this.strategies = [
|
|
88
|
+
{
|
|
89
|
+
id: 'ELO',
|
|
90
|
+
docType: DocType.NAVIGATION_STRATEGY,
|
|
91
|
+
name: 'ELO',
|
|
92
|
+
description: 'Default ELO-based navigation strategy',
|
|
93
|
+
implementingClass: Navigators.ELO,
|
|
94
|
+
course: this.courseId,
|
|
95
|
+
serializedData: '',
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
this.loading = false;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
createNewStrategy() {
|
|
103
|
+
// Disabled for now - new strategy types will be implemented in the future
|
|
104
|
+
console.log('Creating new strategies is not yet implemented');
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
editStrategy(strategy: ContentNavigationStrategyData) {
|
|
108
|
+
// Strategy editing is not yet implemented
|
|
109
|
+
console.log(`Editing strategy ${strategy.id} is not yet implemented`);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
confirmDeleteStrategy(strategy: ContentNavigationStrategyData) {
|
|
115
|
+
this.strategyToDelete = strategy;
|
|
116
|
+
this.showDeleteConfirm = true;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async deleteStrategy() {
|
|
120
|
+
if (!this.strategyToDelete) return;
|
|
121
|
+
|
|
122
|
+
this.loading = true;
|
|
123
|
+
try {
|
|
124
|
+
const dataLayer = getDataLayer();
|
|
125
|
+
const courseDB = dataLayer.getCourseDB(this.courseId);
|
|
126
|
+
|
|
127
|
+
// Since deleteNavigationStrategy doesn't exist in the interface yet,
|
|
128
|
+
// we'll use updateNavigationStrategy with an empty/invalid strategy that
|
|
129
|
+
// will be ignored by the system
|
|
130
|
+
const emptyStrategy: ContentNavigationStrategyData = {
|
|
131
|
+
id: this.strategyToDelete!.id,
|
|
132
|
+
docType: DocType.NAVIGATION_STRATEGY,
|
|
133
|
+
name: "DELETED",
|
|
134
|
+
description: "This strategy has been deleted",
|
|
135
|
+
implementingClass: "",
|
|
136
|
+
course: this.courseId,
|
|
137
|
+
serializedData: ""
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Update with empty strategy
|
|
141
|
+
await courseDB.updateNavigationStrategy(this.strategyToDelete!.id, emptyStrategy);
|
|
142
|
+
console.log(`Strategy ${this.strategyToDelete!.id} marked as deleted`);
|
|
143
|
+
|
|
144
|
+
// Remove from our local array
|
|
145
|
+
this.strategies = this.strategies.filter((s) => s.id !== this.strategyToDelete?.id);
|
|
146
|
+
|
|
147
|
+
this.showDeleteConfirm = false;
|
|
148
|
+
this.strategyToDelete = null;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Failed to delete navigation strategy:', error);
|
|
151
|
+
}
|
|
152
|
+
this.loading = false;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<style scoped>
|
|
159
|
+
.navigation-strategy-editor {
|
|
160
|
+
padding: 16px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.no-strategies {
|
|
164
|
+
margin: 20px 0;
|
|
165
|
+
padding: 20px;
|
|
166
|
+
background-color: #f5f5f5;
|
|
167
|
+
border-radius: 4px;
|
|
168
|
+
text-align: center;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="navigation-strategy-list">
|
|
3
|
+
<v-list>
|
|
4
|
+
<v-list-item
|
|
5
|
+
v-for="strategy in strategies"
|
|
6
|
+
:key="strategy.id"
|
|
7
|
+
lines="three"
|
|
8
|
+
>
|
|
9
|
+
<template #prepend>
|
|
10
|
+
<v-icon> mdi-navigation </v-icon>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<v-list-item-title class="text-h6">
|
|
14
|
+
{{ strategy.name }}
|
|
15
|
+
</v-list-item-title>
|
|
16
|
+
|
|
17
|
+
<v-list-item-subtitle>{{ strategy.description }}</v-list-item-subtitle>
|
|
18
|
+
|
|
19
|
+
<v-list-item-subtitle class="strategy-details mt-2">
|
|
20
|
+
<div><strong>Type:</strong> {{ strategy.implementingClass }}</div>
|
|
21
|
+
<div v-if="strategy.serializedData"><strong>Configuration:</strong> {{ getDisplayConfig(strategy) }}</div>
|
|
22
|
+
</v-list-item-subtitle>
|
|
23
|
+
|
|
24
|
+
<template #append>
|
|
25
|
+
<div class="d-flex">
|
|
26
|
+
<v-btn icon size="small" title="Edit Strategy (coming soon)" class="mr-1" disabled>
|
|
27
|
+
<v-icon>mdi-pencil</v-icon>
|
|
28
|
+
</v-btn>
|
|
29
|
+
|
|
30
|
+
<v-btn
|
|
31
|
+
icon
|
|
32
|
+
size="small"
|
|
33
|
+
title="Delete Strategy"
|
|
34
|
+
class="mr-1"
|
|
35
|
+
@click="$emit('delete', strategy)"
|
|
36
|
+
>
|
|
37
|
+
<v-icon>mdi-delete</v-icon>
|
|
38
|
+
</v-btn>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
</v-list-item>
|
|
42
|
+
</v-list>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import { defineComponent, PropType } from 'vue';
|
|
48
|
+
import type { ContentNavigationStrategyData } from '@vue-skuilder/db/src/core/types/contentNavigationStrategy';
|
|
49
|
+
|
|
50
|
+
export default defineComponent({
|
|
51
|
+
name: 'NavigationStrategyList',
|
|
52
|
+
|
|
53
|
+
props: {
|
|
54
|
+
strategies: {
|
|
55
|
+
type: Array as PropType<ContentNavigationStrategyData[]>,
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
emits: ['edit', 'delete'],
|
|
61
|
+
|
|
62
|
+
methods: {
|
|
63
|
+
getDisplayConfig(strategy: ContentNavigationStrategyData): string {
|
|
64
|
+
if (!strategy.serializedData) return 'No configuration';
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Try to parse the serialized data to show a more user-friendly display
|
|
68
|
+
const config = JSON.parse(strategy.serializedData);
|
|
69
|
+
return Object.keys(config)
|
|
70
|
+
.map((key) => `${key}: ${config[key]}`)
|
|
71
|
+
.join(', ');
|
|
72
|
+
} catch {
|
|
73
|
+
// If it's not valid JSON, just return as is
|
|
74
|
+
return strategy.serializedData;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style scoped>
|
|
82
|
+
.navigation-strategy-list {
|
|
83
|
+
margin: 16px 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
.strategy-details {
|
|
89
|
+
font-size: 0.9em;
|
|
90
|
+
color: rgba(0, 0, 0, 0.6);
|
|
91
|
+
}
|
|
92
|
+
</style>
|