@vue-skuilder/edit-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/dist/assets/index.css +1 -0
- package/dist/edit-ui.es.js +71090 -0
- package/dist/edit-ui.es.js.map +1 -0
- package/dist/edit-ui.umd.js +83 -0
- package/dist/edit-ui.umd.js.map +1 -0
- package/package.json +67 -0
- package/src/components/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/BulkImportView.vue +633 -0
- package/src/components/CourseEditor.vue +164 -0
- package/src/components/ViewableDataInputForm/DataInputForm.vue +533 -0
- package/src/components/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/OptionsFieldInput.ts +161 -0
- package/src/components/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/index.ts +21 -0
- package/src/index.ts +6 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
</v-window>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script lang="ts">
|
|
59
|
+
import { defineComponent } from 'vue';
|
|
60
|
+
// import ComponentRegistration from '@vue-skuilder/platform-ui/src/components/Edit/ComponentRegistration/ComponentRegistration.vue';
|
|
61
|
+
// import NavigationStrategyEditor from '@vue-skuilder/platform-ui/src/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue';
|
|
62
|
+
import { allCourses } from '@vue-skuilder/courses';
|
|
63
|
+
import { BlanksCard, BlanksCardDataShapes } from '@vue-skuilder/courses';
|
|
64
|
+
import { CourseConfig, NameSpacer, DataShape } from '@vue-skuilder/common';
|
|
65
|
+
import DataInputForm from './ViewableDataInputForm/DataInputForm.vue';
|
|
66
|
+
import BulkImportView from './BulkImportView.vue'; // Added import
|
|
67
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
68
|
+
import { useDataInputFormStore } from '../stores/useDataInputFormStore';
|
|
69
|
+
|
|
70
|
+
export default defineComponent({
|
|
71
|
+
name: 'CourseEditor',
|
|
72
|
+
|
|
73
|
+
components: {
|
|
74
|
+
DataInputForm,
|
|
75
|
+
// ComponentRegistration,
|
|
76
|
+
// NavigationStrategyEditor,
|
|
77
|
+
BulkImportView,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
props: {
|
|
81
|
+
course: {
|
|
82
|
+
type: String,
|
|
83
|
+
required: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
data() {
|
|
88
|
+
return {
|
|
89
|
+
registeredDataShapes: [] as DataShape[],
|
|
90
|
+
dataShapes: [] as DataShape[],
|
|
91
|
+
selectedShape: BlanksCard.dataShapes[0].name,
|
|
92
|
+
courseConfig: null as CourseConfig | null,
|
|
93
|
+
dataShape: BlanksCardDataShapes[0] as DataShape,
|
|
94
|
+
loading: true,
|
|
95
|
+
currentTab: 'single',
|
|
96
|
+
dataInputFormStore: useDataInputFormStore(),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
watch: {
|
|
101
|
+
selectedShape: {
|
|
102
|
+
handler(value?: string) {
|
|
103
|
+
if (value) {
|
|
104
|
+
this.dataShape = this.getDataShape(value);
|
|
105
|
+
this.dataInputFormStore.setDataShape(this.dataShape);
|
|
106
|
+
|
|
107
|
+
this.dataInputFormStore.dataInputForm.course = this.courseConfig;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async created() {
|
|
114
|
+
this.courseConfig = await getDataLayer().getCoursesDB().getCourseConfig(this.course);
|
|
115
|
+
|
|
116
|
+
// for testing getCourseTagStubs...
|
|
117
|
+
// log(JSON.stringify(await getCourseTagStubs(this.course)));
|
|
118
|
+
|
|
119
|
+
// this.dataShapes = BaseCards.dataShapes;
|
|
120
|
+
// this.registeredDataShapes = BaseCards.dataShapes;
|
|
121
|
+
// BaseCards.dataShapes.forEach((shape) => {
|
|
122
|
+
// this.dataShapes.push(shape);
|
|
123
|
+
// this.registeredDataShapes.push(shape);
|
|
124
|
+
// });
|
|
125
|
+
|
|
126
|
+
// #55 make all 'programmed' datashapes available, rather than
|
|
127
|
+
// the previous code-based name scoping
|
|
128
|
+
allCourses.courses.forEach((course) => {
|
|
129
|
+
course.questions.forEach((question) => {
|
|
130
|
+
question.dataShapes.forEach((ds) => {
|
|
131
|
+
this.dataShapes.push(ds);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Defensive check: handle loading states where courseConfig might not be ready
|
|
137
|
+
if (this.courseConfig && this.courseConfig.dataShapes) {
|
|
138
|
+
this.courseConfig.dataShapes.forEach((ds) => {
|
|
139
|
+
this.registeredDataShapes.push(
|
|
140
|
+
this.dataShapes.find((shape) => {
|
|
141
|
+
return shape.name === NameSpacer.getDataShapeDescriptor(ds.name).dataShape;
|
|
142
|
+
})!
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.loading = false;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
methods: {
|
|
151
|
+
getDataShape(shapeName: string): DataShape {
|
|
152
|
+
return this.dataShapes.find((shape) => {
|
|
153
|
+
return shape.name === shapeName;
|
|
154
|
+
})!;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<style scoped>
|
|
161
|
+
div {
|
|
162
|
+
margin-top: 15px;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12" xl="6">
|
|
5
|
+
<v-form ma-2 autocomplete="off">
|
|
6
|
+
<div v-for="(field, i) in dataShape.fields" :key="dataShape.fields.indexOf(field)">
|
|
7
|
+
<!-- image and audio inputs are semi deprecated - not in use right now -
|
|
8
|
+
superceded by the generic fillIn type that allows images and audio from the
|
|
9
|
+
general mediaDragDropUploader -->
|
|
10
|
+
<!-- <audio-input v-else-if="field.type === audio" :ref="(el: FieldInputInstance) => setFieldInputRef(el, i)" :field="field" :autofocus="i == 0" /> -->
|
|
11
|
+
<!-- <image-input v-else-if="field.type === img" :ref="(el: FieldInputInstance) => setFieldInputRef(el, i)" :field="field" :autofocus="i == 0" /> -->
|
|
12
|
+
|
|
13
|
+
<string-input
|
|
14
|
+
v-if="field.type === ftString"
|
|
15
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
16
|
+
:field="field"
|
|
17
|
+
:autofocus="i == 0"
|
|
18
|
+
/>
|
|
19
|
+
<chess-puzzle-input
|
|
20
|
+
v-else-if="field.type === chessPuzzle"
|
|
21
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
22
|
+
:field="field"
|
|
23
|
+
:autofocus="i == 0"
|
|
24
|
+
/>
|
|
25
|
+
<number-input
|
|
26
|
+
v-else-if="field.type === num"
|
|
27
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
28
|
+
:field="field"
|
|
29
|
+
:autofocus="i == 0"
|
|
30
|
+
/>
|
|
31
|
+
<integer-input
|
|
32
|
+
v-else-if="field.type === int"
|
|
33
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
34
|
+
:field="field"
|
|
35
|
+
:autofocus="i == 0"
|
|
36
|
+
/>
|
|
37
|
+
<markdown-input
|
|
38
|
+
v-else-if="field.type === mkd"
|
|
39
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
40
|
+
:field="field"
|
|
41
|
+
:autofocus="i == 0"
|
|
42
|
+
data-cy="markdown-input"
|
|
43
|
+
/>
|
|
44
|
+
<midi-input
|
|
45
|
+
v-else-if="field.type === midi"
|
|
46
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
47
|
+
:field="field"
|
|
48
|
+
:autofocus="i == 0"
|
|
49
|
+
/>
|
|
50
|
+
<media-drag-drop-uploader
|
|
51
|
+
v-else-if="field.type === uploader"
|
|
52
|
+
:ref="(el: FieldInputInstance) => setFieldInputRef(el, i)"
|
|
53
|
+
:field="field"
|
|
54
|
+
:autofocus="i == 0"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<tags-input ref="tagsInput" :hide-submit="true" :course-i-d="courseCfg.courseID" card-i-d="" />
|
|
59
|
+
<v-btn
|
|
60
|
+
v-if="!previewComponent"
|
|
61
|
+
data-cy="add-card-btn"
|
|
62
|
+
class="float-right"
|
|
63
|
+
type="submit"
|
|
64
|
+
color="primary"
|
|
65
|
+
:loading="uploading"
|
|
66
|
+
:disabled="!allowSubmit"
|
|
67
|
+
@click.prevent="submit"
|
|
68
|
+
>
|
|
69
|
+
Add card
|
|
70
|
+
<v-icon end>mdi-plus-circle</v-icon>
|
|
71
|
+
</v-btn>
|
|
72
|
+
<div v-else>Input validated: {{ inputIsValidated }}</div>
|
|
73
|
+
</v-form>
|
|
74
|
+
</v-col>
|
|
75
|
+
<v-col cols="12" xl="6">
|
|
76
|
+
<card-browser v-if="inputIsValidated" class="ml-4" :views="shapeViews" :data="[previewInput]" />
|
|
77
|
+
</v-col>
|
|
78
|
+
</v-row>
|
|
79
|
+
</v-container>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<script lang="ts">
|
|
83
|
+
import { defineComponent } from 'vue';
|
|
84
|
+
import { DataShape } from '@vue-skuilder/common';
|
|
85
|
+
import { CardBrowser } from '@vue-skuilder/common-ui';
|
|
86
|
+
import { TagsInput } from '@vue-skuilder/common-ui';
|
|
87
|
+
import type { TagsInputInstance } from '@vue-skuilder/common-ui/src/components/TagsInput.vue';
|
|
88
|
+
import { FieldInputInstance, isFieldInput } from './FieldInput.types';
|
|
89
|
+
import { alertUser } from '@vue-skuilder/common-ui';
|
|
90
|
+
import { allCourses } from '@vue-skuilder/courses';
|
|
91
|
+
import { getDataLayer, CourseDBInterface } from '@vue-skuilder/db';
|
|
92
|
+
import { FieldType, Status, CourseConfig, NameSpacer, ShapeDescriptor } from '@vue-skuilder/common';
|
|
93
|
+
import _ from 'lodash';
|
|
94
|
+
import IntegerInput from './FieldInputs/IntegerInput.vue';
|
|
95
|
+
import MarkdownInput from './FieldInputs/MarkdownInput.vue';
|
|
96
|
+
import MediaDragDropUploader from './FieldInputs/MediaDragDropUploader.vue';
|
|
97
|
+
import MidiInput from './FieldInputs/MidiInput.vue';
|
|
98
|
+
import NumberInput from './FieldInputs/NumberInput.vue';
|
|
99
|
+
import StringInput from './FieldInputs/StringInput.vue';
|
|
100
|
+
import ChessPuzzleInput from './FieldInputs/ChessPuzzleInput.vue';
|
|
101
|
+
import { CourseElo } from '@vue-skuilder/common';
|
|
102
|
+
import { useDataInputFormStore } from '../../stores/useDataInputFormStore';
|
|
103
|
+
import { ViewData } from '@vue-skuilder/common';
|
|
104
|
+
import { Question, getCurrentUser } from '@vue-skuilder/common-ui';
|
|
105
|
+
|
|
106
|
+
type StringIndexable = { [x: string]: unknown };
|
|
107
|
+
|
|
108
|
+
type QorNull = null | typeof Question;
|
|
109
|
+
|
|
110
|
+
export interface ComponentData {
|
|
111
|
+
tag: string;
|
|
112
|
+
tags: string[];
|
|
113
|
+
autoCompleteSuggestions: string[];
|
|
114
|
+
timer?: NodeJS.Timeout;
|
|
115
|
+
dataInputFormStore: ReturnType<typeof useDataInputFormStore>;
|
|
116
|
+
fieldInputRefs: (FieldInputInstance | null)[];
|
|
117
|
+
courseDB: CourseDBInterface | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default defineComponent({
|
|
121
|
+
name: 'DataInputForm',
|
|
122
|
+
|
|
123
|
+
components: {
|
|
124
|
+
// AudioInput,
|
|
125
|
+
// ImageInput,
|
|
126
|
+
NumberInput,
|
|
127
|
+
StringInput,
|
|
128
|
+
IntegerInput,
|
|
129
|
+
MarkdownInput,
|
|
130
|
+
MidiInput,
|
|
131
|
+
CardBrowser,
|
|
132
|
+
MediaDragDropUploader,
|
|
133
|
+
TagsInput,
|
|
134
|
+
ChessPuzzleInput,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
props: {
|
|
138
|
+
courseCfg: {
|
|
139
|
+
type: Object as () => CourseConfig,
|
|
140
|
+
required: true,
|
|
141
|
+
default: () => ({
|
|
142
|
+
courseID: 'default-test',
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
dataShape: {
|
|
146
|
+
type: Object as () => DataShape,
|
|
147
|
+
required: true,
|
|
148
|
+
},
|
|
149
|
+
previewComponent: {
|
|
150
|
+
type: Object as () => QorNull,
|
|
151
|
+
required: false,
|
|
152
|
+
default: null,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
data(): ComponentData {
|
|
157
|
+
return {
|
|
158
|
+
tag: '',
|
|
159
|
+
tags: [],
|
|
160
|
+
autoCompleteSuggestions: [],
|
|
161
|
+
timer: undefined,
|
|
162
|
+
dataInputFormStore: useDataInputFormStore(),
|
|
163
|
+
fieldInputRefs: [] as (FieldInputInstance | null)[],
|
|
164
|
+
courseDB: null as CourseDBInterface | null,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
computed: {
|
|
169
|
+
ftString() {
|
|
170
|
+
return FieldType.STRING;
|
|
171
|
+
},
|
|
172
|
+
int() {
|
|
173
|
+
return FieldType.INT;
|
|
174
|
+
},
|
|
175
|
+
num() {
|
|
176
|
+
return FieldType.NUMBER;
|
|
177
|
+
},
|
|
178
|
+
img() {
|
|
179
|
+
return FieldType.IMAGE;
|
|
180
|
+
},
|
|
181
|
+
mkd() {
|
|
182
|
+
return FieldType.MARKDOWN;
|
|
183
|
+
},
|
|
184
|
+
audio() {
|
|
185
|
+
return FieldType.AUDIO;
|
|
186
|
+
},
|
|
187
|
+
midi() {
|
|
188
|
+
return FieldType.MIDI;
|
|
189
|
+
},
|
|
190
|
+
uploader() {
|
|
191
|
+
return FieldType.MEDIA_UPLOADS;
|
|
192
|
+
},
|
|
193
|
+
chessPuzzle() {
|
|
194
|
+
return FieldType.CHESS_PUZZLE;
|
|
195
|
+
},
|
|
196
|
+
fieldInputs(): FieldInputInstance[] {
|
|
197
|
+
return Array.from(this.fieldInputRefs.values()) as FieldInputInstance[];
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
shapeViews: {
|
|
201
|
+
get() {
|
|
202
|
+
return this.dataInputFormStore.dataInputForm.shapeViews;
|
|
203
|
+
},
|
|
204
|
+
set(views: ViewData[]) {
|
|
205
|
+
this.dataInputFormStore.dataInputForm.shapeViews = views;
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
allowSubmit() {
|
|
210
|
+
return this.dataInputFormStore.dataInputForm.fieldStore.isValidated;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
fieldStore() {
|
|
214
|
+
return this.dataInputFormStore.dataInputForm.fieldStore;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
uploading: {
|
|
218
|
+
get(): boolean {
|
|
219
|
+
return this.dataInputFormStore.dataInputForm.uploading;
|
|
220
|
+
},
|
|
221
|
+
set(uploading: boolean) {
|
|
222
|
+
this.dataInputFormStore.dataInputForm.uploading = uploading;
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
previewInput() {
|
|
227
|
+
// this.convertInput();
|
|
228
|
+
return this.fieldStore.getPreview as unknown as ViewData;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
convertedInput() {
|
|
232
|
+
// this.convertInput();
|
|
233
|
+
return this.fieldStore.convertedInput;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
inputIsValidated(): boolean {
|
|
237
|
+
const store = this.dataInputFormStore.dataInputForm.fieldStore;
|
|
238
|
+
return store.isValidated;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
datashapeDescriptor(): ShapeDescriptor {
|
|
242
|
+
// Defensive check: handle loading states where courseCfg might not be ready
|
|
243
|
+
if (!this.courseCfg || !this.courseCfg.dataShapes) {
|
|
244
|
+
return {
|
|
245
|
+
course: '',
|
|
246
|
+
dataShape: '',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const ds of this.courseCfg.dataShapes) {
|
|
251
|
+
const descriptor = NameSpacer.getDataShapeDescriptor(ds.name);
|
|
252
|
+
if (descriptor.dataShape === this.dataShape.name) {
|
|
253
|
+
return descriptor;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
course: '',
|
|
259
|
+
dataShape: '',
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
watch: {
|
|
265
|
+
dataShape: {
|
|
266
|
+
handler() {
|
|
267
|
+
if (!this.previewComponent) {
|
|
268
|
+
this.getImplementingViews();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
immediate: true,
|
|
272
|
+
},
|
|
273
|
+
store: {
|
|
274
|
+
handler() {
|
|
275
|
+
this.convertInput();
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
'dataShape.fields'(newFields) {
|
|
279
|
+
console.log(`[DataInputForm].watch(fields): newFields ${JSON.stringify(newFields)}`);
|
|
280
|
+
console.log(`[DataInputForm].watch(fields): fields ${JSON.stringify(this.dataShape.fields)}`);
|
|
281
|
+
this.fieldInputRefs = new Array(newFields.length).fill(null);
|
|
282
|
+
console.log(`[DataInputForm].watch(fields): fieldRefs ${JSON.stringify(this.fieldInputRefs)}`);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
created() {
|
|
287
|
+
this.uploading = false;
|
|
288
|
+
this.courseDB = getDataLayer().getCourseDB(this.courseCfg.courseID!);
|
|
289
|
+
|
|
290
|
+
this.getCourseTags();
|
|
291
|
+
this.dataInputFormStore.setDataShape(this.dataShape);
|
|
292
|
+
console.log(`[DataInputForm].created: fields: ${JSON.stringify(this.dataShape.fields)}`);
|
|
293
|
+
this.fieldInputRefs = new Array(this.dataShape.fields.length).fill(null);
|
|
294
|
+
|
|
295
|
+
if (this.previewComponent) {
|
|
296
|
+
this.getImplementingViews();
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
beforeUnmount() {
|
|
301
|
+
this.fieldInputRefs = [];
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
methods: {
|
|
305
|
+
async updateTags(newTags: string[]) {
|
|
306
|
+
console.log(`[DataInputForm] tags updated: ${JSON.stringify(newTags)}`);
|
|
307
|
+
this.tags = newTags;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
setFieldInputRef(el: FieldInputInstance, index: number) {
|
|
311
|
+
console.log(`[DataInputForm].setFieldInputRef: index: ${index}`);
|
|
312
|
+
// Ensure array is large enough
|
|
313
|
+
if (index >= this.fieldInputRefs.length) {
|
|
314
|
+
this.fieldInputRefs = this.fieldInputRefs.concat(new Array(index - this.fieldInputRefs.length + 1).fill(null));
|
|
315
|
+
}
|
|
316
|
+
// remove any null entries at the end
|
|
317
|
+
|
|
318
|
+
while (this.fieldInputRefs[this.fieldInputRefs.length - 1] === null) {
|
|
319
|
+
this.fieldInputRefs.pop();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.fieldInputRefs[index] = el;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async getCourseTags() {
|
|
326
|
+
const existingTags = await this.courseDB!.getCourseTagStubs();
|
|
327
|
+
this.autoCompleteSuggestions = existingTags.rows.map((tagDoc) => {
|
|
328
|
+
return tagDoc.doc!.name;
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
expectedValidations(): number {
|
|
333
|
+
return this.dataShape.fields.length;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
checkInput(): boolean {
|
|
337
|
+
return true;
|
|
338
|
+
// return this.fieldInputs.every((input) => input.validate());
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
convertInput() {},
|
|
342
|
+
|
|
343
|
+
inputContainsTranspositionFcns(): boolean {
|
|
344
|
+
this.convertInput();
|
|
345
|
+
for (const input in this.convertedInput) {
|
|
346
|
+
if (typeof this.convertedInput[input] === 'function') {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
objectContainsFunction(o: StringIndexable): boolean {
|
|
354
|
+
for (const key in o) {
|
|
355
|
+
if (typeof o[key] === 'function') {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
expandO(o: StringIndexable): StringIndexable[] {
|
|
363
|
+
let ret: StringIndexable[] = [];
|
|
364
|
+
|
|
365
|
+
if (this.objectContainsFunction(o)) {
|
|
366
|
+
for (const fKey in o) {
|
|
367
|
+
if (typeof o[fKey] === 'function') {
|
|
368
|
+
console.log(`[DataInputForm] Key ${fKey} is a function.`);
|
|
369
|
+
const replaced: StringIndexable[] = [];
|
|
370
|
+
|
|
371
|
+
(o[fKey]() as Array<unknown>).forEach((fcnOutput) => {
|
|
372
|
+
let copy: StringIndexable = {};
|
|
373
|
+
copy = _.cloneDeep(o);
|
|
374
|
+
copy[fKey] = fcnOutput;
|
|
375
|
+
|
|
376
|
+
console.log(`[DataInputForm] Replaced Copy: ${JSON.stringify(copy)}`);
|
|
377
|
+
|
|
378
|
+
replaced.push(copy);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
replaced.forEach((obj) => {
|
|
382
|
+
if (this.objectContainsFunction(obj)) {
|
|
383
|
+
console.log('[DataInputForm] 2nd pass...');
|
|
384
|
+
const recursiveExpansion = this.expandO(obj);
|
|
385
|
+
ret = ret.concat(recursiveExpansion);
|
|
386
|
+
} else {
|
|
387
|
+
ret.push(obj);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return ret;
|
|
393
|
+
} else {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
getTags(): string[] {
|
|
399
|
+
const dataShapeParsedTags: string[] = [];
|
|
400
|
+
|
|
401
|
+
this.fieldInputs.forEach((f) => {
|
|
402
|
+
if (f.generateTags) {
|
|
403
|
+
const fTags = f.generateTags();
|
|
404
|
+
dataShapeParsedTags.push(...fTags);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const manualTags = (this.$refs.tagsInput as unknown as TagsInputInstance).tags.map((t) => t.text);
|
|
409
|
+
|
|
410
|
+
return dataShapeParsedTags.concat(manualTags);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
getElo(): CourseElo | undefined {
|
|
414
|
+
for (const f of this.fieldInputs) {
|
|
415
|
+
if (f.generateELO) {
|
|
416
|
+
return f.generateELO();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return undefined;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
async submit() {
|
|
423
|
+
if (this.checkInput()) {
|
|
424
|
+
console.log(`[DataInputForm] Store: ${JSON.stringify(this.fieldStore.inputs)}`);
|
|
425
|
+
console.log(`[DataInputForm] ConvertedStore: ${JSON.stringify(this.convertedInput)}`);
|
|
426
|
+
this.uploading = true;
|
|
427
|
+
|
|
428
|
+
let inputs = [];
|
|
429
|
+
|
|
430
|
+
if (this.inputContainsTranspositionFcns()) {
|
|
431
|
+
console.log(`[DataInputForm] Expanded input: ${JSON.stringify(this.expandO(this.convertedInput))}`);
|
|
432
|
+
inputs = this.expandO(this.convertedInput);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`[DataInputForm] No Transposition fcn detected`);
|
|
435
|
+
inputs = [this.convertedInput];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = await Promise.all(
|
|
439
|
+
inputs.map(async (input) => {
|
|
440
|
+
return await this.courseDB!.addNote(
|
|
441
|
+
this.datashapeDescriptor.course,
|
|
442
|
+
this.dataShape,
|
|
443
|
+
input,
|
|
444
|
+
(await getCurrentUser()).getUsername(),
|
|
445
|
+
this.getTags(),
|
|
446
|
+
undefined,
|
|
447
|
+
this.getElo()
|
|
448
|
+
);
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (result[0].status === Status.ok) {
|
|
453
|
+
alertUser({
|
|
454
|
+
text: `Content added... Thank you!`,
|
|
455
|
+
status: Status.ok,
|
|
456
|
+
});
|
|
457
|
+
const ti = this.$refs.tagsInput as unknown as TagsInputInstance;
|
|
458
|
+
if (ti.tags.length) {
|
|
459
|
+
ti.updateAvailableCourseTags();
|
|
460
|
+
ti.tags = [];
|
|
461
|
+
}
|
|
462
|
+
this.reset();
|
|
463
|
+
} else {
|
|
464
|
+
alertUser({
|
|
465
|
+
text: `A problem occurred. Content has not been added.`,
|
|
466
|
+
status: Status.error,
|
|
467
|
+
});
|
|
468
|
+
console.error(`Error in DataInputForm.submit(). Result from addNote: ${JSON.stringify(result)}`);
|
|
469
|
+
this.uploading = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
reset() {
|
|
475
|
+
console.log(`[DataInputForm].reset()`);
|
|
476
|
+
this.uploading = false;
|
|
477
|
+
|
|
478
|
+
// Clear all field inputs
|
|
479
|
+
this.fieldInputs.forEach((input) => {
|
|
480
|
+
input.clearData();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Reset the field store
|
|
484
|
+
this.fieldStore.$reset();
|
|
485
|
+
|
|
486
|
+
// Focus the first input
|
|
487
|
+
this.fieldInputs[0].focus();
|
|
488
|
+
|
|
489
|
+
// Reinitialize converted inputs
|
|
490
|
+
this.convertInput();
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
getImplementingViews() {
|
|
494
|
+
if (this.previewComponent) {
|
|
495
|
+
console.log(`[DataInputForm] Getting previewComponent views`);
|
|
496
|
+
this.shapeViews = this.previewComponent.views;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const ds of this.courseCfg.dataShapes) {
|
|
501
|
+
const descriptor = NameSpacer.getDataShapeDescriptor(ds.name);
|
|
502
|
+
|
|
503
|
+
console.log('[DataInputForm] descriptor', descriptor);
|
|
504
|
+
console.log('[DataInputForm] this.dataShape', this.dataShape);
|
|
505
|
+
console.log('[DataInputForm] this.dataShape.name', this.dataShape.name);
|
|
506
|
+
|
|
507
|
+
if (descriptor.dataShape === this.dataShape.name) {
|
|
508
|
+
const crs = allCourses.getCourse(descriptor.course)!;
|
|
509
|
+
|
|
510
|
+
this.shapeViews = [];
|
|
511
|
+
|
|
512
|
+
crs.getBaseQTypes().forEach((qType) => {
|
|
513
|
+
if (qType.dataShapes[0].name === this.dataShape.name) {
|
|
514
|
+
this.shapeViews = this.shapeViews.concat(qType.views);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
for (const q of ds.questionTypes) {
|
|
519
|
+
const qDescriptor = NameSpacer.getQuestionDescriptor(q);
|
|
520
|
+
crs.getQuestion(qDescriptor.questionType)!.views.forEach((view) => {
|
|
521
|
+
this.shapeViews = this.shapeViews.concat(view);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
isFieldInput(component: unknown): component is FieldInputInstance {
|
|
529
|
+
return isFieldInput(component);
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
</script>
|