@windward/core 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.30.0] - 2026-03-26
4
+
5
+ * Merged in feature/LE-2286-file-submission-block-submit-but (pull request #498)
6
+ * Merged in feature/LE-2277/generate-case-studies (pull request #484)
7
+ * Merged in bugfix/LE-2332-tab-text-color-in-dark-mode-with (pull request #497)
8
+ * Merged in feature/LE-2286-file-submission-block-submit-but (pull request #492)
9
+ * Merged in feature/LE-2321-increase-glossary-term-character (pull request #494)
10
+ * Merged in bugfix/LE-2332-tab-text-color-in-dark-mode-with (pull request #493)
11
+ * Merge remote-tracking branch 'origin/release/0.30.0' into feature/LE-2277/generate-case-studies
12
+ * Merged release/0.29.0 into feature/LE-2286-file-submission-block-submit-but
13
+ * Merged release/0.29.0 into feature/LE-2286-file-submission-block-submit-but
14
+
15
+
3
16
  ## Release [0.29.0] - 2026-03-11
4
17
 
5
18
  * Merged in feature/LE-2319-preselect-cc-transcript-language (pull request #491)
@@ -37,11 +37,13 @@
37
37
  v-for="(tab, tabIndex) in block.metadata.config.items"
38
38
  :key="tabIndex"
39
39
  >
40
- {{
41
- tab.tabHeader === '' || tab.tabHeader === null
42
- ? 'Item ' + (tabIndex + 1)
43
- : tab.tabHeader
44
- }}
40
+ <div class="tab-headers">
41
+ {{
42
+ tab.tabHeader === '' || tab.tabHeader === null
43
+ ? 'Item ' + (tabIndex + 1)
44
+ : tab.tabHeader
45
+ }}
46
+ </div>
45
47
  </v-tab>
46
48
  <v-tab-item
47
49
  v-for="(tabContent, tabContentIndex) in block.metadata
@@ -55,6 +57,7 @@
55
57
  <TextViewer
56
58
  v-if="!tabContent.expand"
57
59
  v-model="tabContent.content"
60
+ class="text-viewer"
58
61
  text-viewer
59
62
  ></TextViewer>
60
63
  <TextEditor
@@ -183,6 +186,9 @@ export default {
183
186
 
184
187
  <style scoped>
185
188
  .text-viewer {
186
- color: var(--v-primary-base);
189
+ color: var(--v-primary-base) !important;
190
+ }
191
+ .tab-headers {
192
+ color: white !important;
187
193
  }
188
194
  </style>
@@ -3,18 +3,16 @@
3
3
  <v-card-title
4
4
  >{{
5
5
  $t(
6
- 'windward.core.components.content.blocks.user_upload.user_uploads'
6
+ 'windward.core.components.content.blocks.user_upload.review_current_uploads'
7
7
  )
8
8
  }}
9
- -
10
- {{ $d(new Date(userFileAsset.created_at), 'long') }}
11
9
  </v-card-title>
12
10
  <v-card-text>
13
- <v-simple-table>
11
+ <v-simple-table class="simple-table" elevation="1">
14
12
  <template #default>
15
13
  <thead>
16
14
  <tr>
17
- <th>
15
+ <th style="width: 60%">
18
16
  {{ $t('shared.file.name') }}
19
17
  </th>
20
18
  <th>
@@ -73,14 +71,24 @@
73
71
  </tbody>
74
72
  </template>
75
73
  </v-simple-table>
74
+ <v-col class="d-flex justify-center">
75
+ <v-btn
76
+ color="success"
77
+ elevation="0"
78
+ class="text-center"
79
+ @click="onSubmit"
80
+ >
81
+ {{ $t('shared.forms.submit') }}
82
+ </v-btn>
83
+ </v-col>
76
84
  </v-card-text>
77
85
  </v-card>
78
86
  </template>
79
87
 
80
88
  <script>
89
+ import UserFileAsset from '../../../../models/UserFileAsset'
81
90
  import Download from '~/helpers/Download'
82
91
 
83
- import UserFileAsset from '../../../../models/UserFileAsset'
84
92
  import FileAsset from '~/models/FileAsset'
85
93
 
86
94
  import ContentBlock from '~/models/ContentBlock'
@@ -114,6 +122,9 @@ export default {
114
122
  this.userFileAsset = this.value
115
123
  },
116
124
  methods: {
125
+ onSubmit() {
126
+ this.$emit('submit')
127
+ },
117
128
  onConfirmDelete(file) {
118
129
  const self = this
119
130
  this.$toast.info(this.$t('shared.forms.confirm_delete_text'), {
@@ -0,0 +1,161 @@
1
+ <template>
2
+ <div v-if="userFileAsset.length">
3
+ <h5 class="mb-2">
4
+ {{
5
+ $t(
6
+ 'windward.core.components.content.blocks.user_upload.submission_history'
7
+ )
8
+ }}
9
+ </h5>
10
+ <v-card v-for="(fileSubmission, index) in userFileAsset" :key="index">
11
+ <v-card-title class="pb-0">
12
+ <p class="mb-0">
13
+ {{
14
+ $t(
15
+ 'windward.core.components.content.blocks.user_upload.submission'
16
+ )
17
+ }}
18
+ -
19
+ {{ $d(new Date(fileSubmission.created_at), 'long') }}
20
+ </p>
21
+ </v-card-title>
22
+ <v-card-text>
23
+ <v-simple-table>
24
+ <thead>
25
+ <tr>
26
+ <th style="width: 60%">
27
+ {{
28
+ $t(
29
+ 'windward.core.components.content.blocks.user_upload.filename'
30
+ )
31
+ }}
32
+ </th>
33
+ <th style="width: 20%">
34
+ {{
35
+ $t(
36
+ 'windward.core.components.content.blocks.user_upload.file_size'
37
+ )
38
+ }}
39
+ </th>
40
+ <th style="width: 20%">
41
+ {{
42
+ $t(
43
+ 'windward.core.components.content.blocks.user_upload.download'
44
+ )
45
+ }}
46
+ </th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr
51
+ v-for="file in fileSubmission.file_assets"
52
+ :key="file.id"
53
+ >
54
+ <td>{{ file.name }}</td>
55
+ <td>
56
+ {{ file.asset.metadata.size | humanFilesize }}
57
+ </td>
58
+ <td>
59
+ <v-btn
60
+ link
61
+ elevation="0"
62
+ @click="
63
+ Download.url(
64
+ file.asset.public_url,
65
+ file.name
66
+ )
67
+ "
68
+ >
69
+ <v-icon>mdi-download</v-icon>
70
+ <span class="sr-only">
71
+ {{ $t('shared.file.download') }}
72
+ </span>
73
+ </v-btn>
74
+ </td>
75
+ </tr>
76
+ </tbody>
77
+ </v-simple-table>
78
+ </v-card-text>
79
+ </v-card>
80
+ </div>
81
+ </template>
82
+
83
+ <script>
84
+ import UserFileAsset from '../../../../models/UserFileAsset'
85
+ import Download from '~/helpers/Download'
86
+
87
+ import FileAsset from '~/models/FileAsset'
88
+
89
+ import ContentBlock from '~/models/ContentBlock'
90
+ import Enrollment from '~/models/Enrollment'
91
+
92
+ export default {
93
+ name: 'DisplayUserFilesTable',
94
+ components: {},
95
+ props: {
96
+ value: {
97
+ type: Array,
98
+ required: false,
99
+ default: () => {
100
+ return []
101
+ },
102
+ },
103
+ },
104
+ data() {
105
+ return {
106
+ Download,
107
+ userFileAsset: {},
108
+ }
109
+ },
110
+ watch: {
111
+ value(newVal) {
112
+ this.userFileAsset = newVal
113
+ },
114
+ },
115
+ mounted() {
116
+ this.userFileAsset = this.value
117
+ },
118
+ methods: {
119
+ onConfirmDelete(file) {
120
+ const self = this
121
+ this.$toast.info(this.$t('shared.forms.confirm_delete_text'), {
122
+ duration: null,
123
+ action: [
124
+ {
125
+ text: this.$t('shared.forms.cancel'),
126
+ onClick: (e, toastObject) => {
127
+ toastObject.goAway(0)
128
+ },
129
+ },
130
+ {
131
+ text: this.$t('shared.forms.confirm'),
132
+ onClick: (e, toastObject) => {
133
+ toastObject.goAway(0)
134
+ self.deleteFile(file)
135
+ },
136
+ },
137
+ ],
138
+ })
139
+ },
140
+ async deleteFile(file) {
141
+ await new FileAsset({ id: file.file_asset_id })
142
+ .for(
143
+ new Enrollment(this.enrollment),
144
+ new ContentBlock({
145
+ id: this.userFileAsset.content_block_id,
146
+ }),
147
+ new UserFileAsset({ id: this.userFileAsset.id })
148
+ )
149
+ .delete()
150
+
151
+ // Remove the deleted file from the array for display reasons
152
+ this.userFileAsset.file_assets =
153
+ this.userFileAsset.file_assets.filter(function (f) {
154
+ return f.file_asset_id !== file.file_asset_id
155
+ })
156
+
157
+ this.$emit('input', this.userFileAsset)
158
+ },
159
+ },
160
+ }
161
+ </script>
@@ -1,109 +1,104 @@
1
1
  <template>
2
- <v-container :class="['pa-0', blockDirectionClasses]" :dir="blockTextDirection">
3
- <div>
4
- <h2
5
- v-if="
6
- block.metadata.config.title &&
7
- block.metadata.config.display_title
8
- "
9
- tabindex="0"
10
- >
11
- {{ block.metadata.config.title }}
12
- </h2>
13
- <v-row>
14
- <v-col v-if="block.metadata.config.instructions" cols="12">
15
- <p tabindex="0" class="pt-3">
16
- {{ block.metadata.config.instructions }}
2
+ <v-container
3
+ :class="['pa-0', blockDirectionClasses]"
4
+ :dir="blockTextDirection"
5
+ >
6
+ <h2
7
+ v-if="
8
+ block.metadata.config.title &&
9
+ block.metadata.config.display_title
10
+ "
11
+ tabindex="0"
12
+ >
13
+ {{ block.metadata.config.title }}
14
+ </h2>
15
+ <v-row>
16
+ <v-col v-if="block.metadata.config.instructions" cols="12">
17
+ <p tabindex="0" class="pt-3">
18
+ {{ block.metadata.config.instructions }}
19
+ </p>
20
+ </v-col>
21
+ <v-col v-if="!blockExists" cols="12">
22
+ <v-alert type="warning">
23
+ <p>
24
+ {{
25
+ $t(
26
+ 'windward.core.components.content.blocks.user_upload.must_save'
27
+ )
28
+ }}
17
29
  </p>
18
- </v-col>
19
- <v-col v-if="!blockExists" cols="12">
20
- <v-alert type="warning">
21
- <p>
22
- {{
23
- $t(
24
- 'windward.core.components.content.blocks.user_upload.must_save'
25
- )
26
- }}
27
- </p>
28
- </v-alert>
29
- </v-col>
30
- </v-row>
31
- <v-row v-if="render">
32
- <v-col cols="12">
33
- <div class="upload-container" :class="uploadContainerClass">
34
- <DisplayUserFilesTable
35
- v-model="userFileAsset"
36
- :enrollment="enrollment"
37
- ></DisplayUserFilesTable>
38
- <div v-if="showUpload">
39
- <div v-if="blockExists">
40
- <v-form
41
- ref="form"
42
- v-model="valid"
43
- lazy-validation
44
- class="pb-0"
45
- >
46
- <FileDropZone
47
- v-model="uploadFiles"
48
- :accept="
49
- block.metadata.config.uploadSettings
50
- .accept
51
- "
52
- :multiple="
53
- block.metadata.config.uploadSettings
54
- .multiple
55
- "
56
- ></FileDropZone>
57
-
58
- <v-container class="text-center">
59
- <v-btn
60
- :disabled="!canUpload || loading"
61
- color="primary"
62
- elevation="0"
63
- class="text-center"
64
- @click="handleUpload"
65
- >
66
- {{ $t('shared.forms.upload') }}
67
- </v-btn>
68
- </v-container>
69
- </v-form>
70
- </div>
71
- </div>
72
- </div>
73
- </v-col>
74
- </v-row>
75
- </div>
30
+ </v-alert>
31
+ </v-col>
32
+ </v-row>
33
+ <v-row v-if="render">
34
+ <v-col v-if="blockExists" cols="12">
35
+ <div class="upload-container" :class="uploadContainerClass">
36
+ <v-col>
37
+ <FileDropZone
38
+ v-model="uploadFiles"
39
+ :accept="
40
+ block.metadata.config.uploadSettings.accept
41
+ "
42
+ :multiple="
43
+ block.metadata.config.uploadSettings.multiple
44
+ "
45
+ ></FileDropZone>
46
+ <v-col class="d-flex justify-center">
47
+ <v-btn
48
+ :disabled="!canUpload || loading"
49
+ color="primary"
50
+ elevation="0"
51
+ class="text-center"
52
+ @click="handleUpload"
53
+ >
54
+ {{ $t('shared.forms.upload') }}
55
+ </v-btn>
56
+ </v-col>
57
+ </v-col>
58
+ <DisplayUserFilesTable
59
+ v-model="uploadedUserFileAsset"
60
+ :enrollment="enrollment"
61
+ @submit="handleSubmit"
62
+ ></DisplayUserFilesTable>
63
+ <v-divider class="my-4"></v-divider>
64
+ <SubmittedDisplayUserFilesTable
65
+ v-model="submittedUserFileAssets"
66
+ ></SubmittedDisplayUserFilesTable>
67
+ </div>
68
+ </v-col>
69
+ </v-row>
76
70
  </v-container>
77
71
  </template>
78
72
 
79
73
  <script>
80
74
  import _ from 'lodash'
81
75
  import { mapGetters } from 'vuex'
76
+ import UserFileAsset from '../../../models/UserFileAsset'
77
+ import DisplayUserFilesTable from './UserUpload/DisplayUserFilesTable.vue'
78
+ import SubmittedDisplayUserFilesTable from './UserUpload/SubmittedDisplayUserFilesTable.vue'
82
79
  import Uuid from '~/helpers/Uuid'
83
80
  import Download from '~/helpers/Download'
84
81
  import Enrollment from '~/models/Enrollment'
85
82
  import ContentBlock from '~/models/ContentBlock'
86
83
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
87
84
  import FileDropZone from '~/components/Core/FileDropZone.vue'
88
- import UserFileAsset from '../../../models/UserFileAsset'
89
-
90
- import DisplayUserFilesTable from './UserUpload/DisplayUserFilesTable.vue'
91
85
 
92
86
  export default {
93
87
  name: 'UserUpload',
94
88
  components: {
95
89
  FileDropZone,
96
90
  DisplayUserFilesTable,
91
+ SubmittedDisplayUserFilesTable,
97
92
  },
98
93
  extends: BaseContentBlock,
99
94
  data() {
100
95
  return {
101
96
  Download,
102
97
  saveState: false, // Override the base block to disable state saving
103
- valid: true,
104
98
  loading: false,
105
99
  uploadFiles: [],
106
- userFileAsset: {},
100
+ uploadedUserFileAsset: {},
101
+ submittedUserFileAssets: [],
107
102
  studentUpload: null,
108
103
  maxFileLimit: 10,
109
104
  }
@@ -121,44 +116,48 @@ export default {
121
116
  return Uuid.test(this.block.id)
122
117
  },
123
118
  canUpload() {
124
- if (this.valid) {
125
- const currentFiles = _.get(
126
- this.userFileAsset,
127
- 'file_assets',
128
- []
129
- )
119
+ const currentFiles = _.get(
120
+ this.uploadedUserFileAsset,
121
+ 'file_assets',
122
+ []
123
+ )
130
124
 
131
- // Multiple disabled, single file selected
132
- if (
133
- !Array.isArray(this.uploadFiles) &&
134
- !this.block.metadata.config.uploadSettings.multiple &&
135
- this.uploadFiles &&
136
- currentFiles.length === 0
137
- ) {
138
- return true
139
- } else if (
140
- // Multi enabled, one or more files selected
141
- this.block.metadata.config.uploadSettings.multiple &&
142
- Array.isArray(this.uploadFiles) &&
143
- this.uploadFiles.length > 0 &&
144
- this.uploadFiles.length + currentFiles.length <=
145
- this.maxFileLimit
146
- ) {
147
- return true
148
- } else {
149
- // No files
150
- return false
151
- }
125
+ // Multiple disabled, single file selected
126
+ if (
127
+ !Array.isArray(this.uploadFiles) &&
128
+ !this.block.metadata.config.uploadSettings.multiple &&
129
+ this.uploadFiles &&
130
+ currentFiles.length === 0
131
+ ) {
132
+ return true
133
+ } else if (
134
+ // Multi enabled, one or more files selected
135
+ this.block.metadata.config.uploadSettings.multiple &&
136
+ Array.isArray(this.uploadFiles) &&
137
+ this.uploadFiles.length > 0 &&
138
+ this.uploadFiles.length + currentFiles.length <=
139
+ this.maxFileLimit
140
+ ) {
141
+ return true
142
+ } else {
143
+ // No files
144
+ return false
152
145
  }
153
- return false
146
+ },
147
+ canSubmit() {
148
+ return !this.canUpload && !_.isEmpty(this.uploadedUserFileAsset)
154
149
  },
155
150
  showUpload() {
156
- const currentFiles = _.get(this.userFileAsset, 'file_assets', [])
157
-
151
+ const currentFiles = _.get(
152
+ this.uploadedUserFileAsset,
153
+ 'file_assets',
154
+ []
155
+ )
158
156
  // Multiple disabled, confirm there's no current files
159
157
  if (
160
158
  !this.block.metadata.config.uploadSettings.multiple &&
161
- currentFiles.length === 0
159
+ currentFiles.length === 0 &&
160
+ this.submittedUserFileAssets.length === 0
162
161
  ) {
163
162
  return true
164
163
  } else if (
@@ -223,7 +222,6 @@ export default {
223
222
  ) {
224
223
  uploadFiles = [uploadFiles]
225
224
  }
226
-
227
225
  // Create a new UserFileAsset if we don't have an instance yet
228
226
  let userFileAssetRequest = new UserFileAsset({
229
227
  file: uploadFiles,
@@ -233,31 +231,66 @@ export default {
233
231
  )
234
232
 
235
233
  // Apply the existing id if we already have an instance
236
- if (Uuid.test(this.userFileAsset.id)) {
237
- userFileAssetRequest.id = this.userFileAsset.id
234
+ if (Uuid.test(this.uploadedUserFileAsset.id)) {
235
+ userFileAssetRequest.id = this.uploadedUserFileAsset.id
238
236
  }
239
237
 
240
238
  // Add our files to upload
241
239
  userFileAssetRequest.file = uploadFiles
242
240
 
243
241
  try {
244
- // Apply the response back to the reactive this.userFileAsset
245
- this.userFileAsset = await userFileAssetRequest.save()
242
+ // Apply the response back to the reactive this.uploadedUserFileAsset
243
+ this.uploadedUserFileAsset = await userFileAssetRequest.save()
246
244
  } catch (e) {
247
245
  this.$toast.error(this.$t('shared.forms.errors.unknown'))
248
- console.log(e)
246
+ console.error(e)
249
247
  }
250
248
  this.loading = false
251
249
  this.uploadFiles = []
252
250
  },
251
+ async handleSubmit() {
252
+ if (this.canSubmit) {
253
+ this.loading = true
254
+
255
+ // Create an update request for the existing UserFileAsset
256
+ const userFileAssetRequest = new UserFileAsset({
257
+ status: 'submitted',
258
+ }).for(
259
+ new Enrollment(this.enrollment),
260
+ new ContentBlock({ id: this.block.id })
261
+ )
262
+
263
+ // Apply the existing id to update the specific record
264
+ if (Uuid.test(this.uploadedUserFileAsset.id)) {
265
+ userFileAssetRequest.id = this.uploadedUserFileAsset.id
266
+ }
267
+ try {
268
+ // Save the update
269
+ const response = await userFileAssetRequest.save()
270
+ this.submittedUserFileAssets.unshift(response)
271
+ this.uploadedUserFileAsset = {}
272
+ } catch (e) {
273
+ this.$toast.error(this.$t('shared.forms.errors.unknown'))
274
+ console.error(e)
275
+ }
276
+ this.loading = false
277
+ }
278
+ },
253
279
  async loadUserUploads() {
254
280
  if (this.enrollment && this.block.id) {
255
- this.userFileAsset = await new UserFileAsset()
281
+ const result = await new UserFileAsset()
256
282
  .for(
257
283
  new Enrollment(this.enrollment),
258
284
  new ContentBlock({ id: this.block.id })
259
285
  )
260
- .first()
286
+ .get()
287
+ result.forEach((file) => {
288
+ if (file.status === 'submitted') {
289
+ this.submittedUserFileAssets.push(file)
290
+ } else {
291
+ this.uploadedUserFileAsset = file
292
+ }
293
+ })
261
294
  }
262
295
  },
263
296
  },
@@ -49,13 +49,63 @@
49
49
  v-bind="attrs"
50
50
  type=" table-row-divider, list-item, divider, list-item, divider, image,image"
51
51
  ></v-skeleton-loader>
52
+
53
+ <div v-if="isTextBlock" class="case-study-controls mt-2">
54
+ <div class="text-caption mb-2">
55
+ {{
56
+ $t(
57
+ 'windward.core.components.settings.text_editor.case_study_help'
58
+ )
59
+ }}
60
+ </div>
61
+ <v-autocomplete
62
+ v-model="selectedCaseStudyPages"
63
+ class="mb-2 case-study-page-selector"
64
+ :items="flattenedContent"
65
+ outlined
66
+ hide-details
67
+ multiple
68
+ chips
69
+ small-chips
70
+ deletable-chips
71
+ :disabled="render || isGeneratingCaseStudy"
72
+ :label="
73
+ $t(
74
+ 'windward.core.components.settings.text_editor.case_study_selected_pages'
75
+ )
76
+ "
77
+ item-text="content.name"
78
+ return-object
79
+ ></v-autocomplete>
80
+ <v-btn
81
+ elevation="0"
82
+ color="secondary"
83
+ block
84
+ :loading="isGeneratingCaseStudy"
85
+ :disabled="render || isGeneratingCaseStudy"
86
+ @click="onGenerateCaseStudy"
87
+ >
88
+ <v-icon v-if="!isGeneratingCaseStudy" class="pr-1">
89
+ mdi-magic-staff
90
+ </v-icon>
91
+ <span v-if="!isGeneratingCaseStudy">{{
92
+ $t(
93
+ 'windward.core.components.settings.text_editor.generate_case_study'
94
+ )
95
+ }}</span>
96
+ </v-btn>
97
+ </div>
52
98
  </div>
53
99
  </template>
54
100
 
55
101
  <script>
102
+ import _ from 'lodash'
103
+ import { mapGetters } from 'vuex'
56
104
  import Crypto from '~/helpers/Crypto'
57
105
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
58
106
  import TextEditor from '~/components/Text/TextEditor'
107
+ import Course from '~/models/Course'
108
+ import Organization from '~/models/Organization'
59
109
  export default {
60
110
  name: 'TextEditorSettings',
61
111
  components: {
@@ -76,10 +126,42 @@ export default {
76
126
  },
77
127
  hideTextEditor: false,
78
128
  updateKey: Crypto.id(),
129
+ isGeneratingCaseStudy: false,
130
+ selectedCaseStudyPages: [],
79
131
  }
80
132
  },
133
+ computed: {
134
+ ...mapGetters({
135
+ organization: 'organization/get',
136
+ course: 'course/get',
137
+ currentContent: 'content/get',
138
+ }),
139
+ isTextBlock() {
140
+ return _.get(this.block, 'tag', '') === 'content-blocks-text'
141
+ },
142
+ flattenedContent() {
143
+ const flatTree = this.$ContentService.getFlatTree()
144
+
145
+ const homepage = this.$ContentService.getHomepage()
146
+ if (!_.isEmpty(homepage)) {
147
+ flatTree.unshift(homepage)
148
+ }
149
+
150
+ return flatTree
151
+ },
152
+ },
81
153
  mounted() {
82
154
  this.setConfig({ expand: false })
155
+
156
+ // Default selected pages to the current page
157
+ if (
158
+ this.isTextBlock &&
159
+ Array.isArray(this.selectedCaseStudyPages) &&
160
+ this.selectedCaseStudyPages.length === 0 &&
161
+ !_.isEmpty(this.currentContent)
162
+ ) {
163
+ this.selectedCaseStudyPages = [_.cloneDeep(this.currentContent)]
164
+ }
83
165
  },
84
166
  methods: {
85
167
  onExpand() {
@@ -87,8 +169,169 @@ export default {
87
169
  this.block.metadata.config.expand = this.hideTextEditor
88
170
  this.updateKey = Crypto.id()
89
171
  },
172
+ blockHasMeaningfulText() {
173
+ const html = _.get(this.block, 'body', '') || ''
174
+ if (!html) {
175
+ return false
176
+ }
177
+ // Strip tags and check for meaningful length
178
+ const text = String(html).replace(/<[^>]*>/g, '').trim()
179
+ return text.length > 0
180
+ },
181
+ async requestCaseStudyGeneration(contentIds) {
182
+ const organizationId = _.get(this.organization, 'id', null)
183
+ const courseId = _.get(this.course, 'id', null)
184
+
185
+ if (!organizationId || !courseId) {
186
+ throw new Error('missing_context')
187
+ }
188
+
189
+ const request = new Course()
190
+ request.custom(
191
+ new Organization({ id: organizationId }),
192
+ new Course({ id: courseId }),
193
+ 'llm-case-study'
194
+ )
195
+
196
+ const resourcePath = request._customResource
197
+ if (!resourcePath) {
198
+ throw new Error('missing_resource')
199
+ }
200
+
201
+ const payload = {
202
+ content_ids: contentIds,
203
+ language: this.$i18n?.locale,
204
+ }
205
+
206
+ const requestConfig = request._reqConfig(
207
+ {
208
+ method: 'POST',
209
+ url: `${request.baseURL()}/${resourcePath}`,
210
+ data: payload,
211
+ },
212
+ { forceMethod: true }
213
+ )
214
+
215
+ const response = await request.request(requestConfig)
216
+ return response?.data ?? response
217
+ },
218
+ onGenerateCaseStudy(force = false) {
219
+ if (this.isGeneratingCaseStudy || this.render) {
220
+ return
221
+ }
222
+
223
+ const contentIds = (this.selectedCaseStudyPages || [])
224
+ .map((c) => _.get(c, 'id', null))
225
+ .filter((id) => typeof id === 'string' && id.length > 0)
226
+
227
+ if (contentIds.length === 0) {
228
+ this.$toast?.error(
229
+ this.$t(
230
+ 'windward.core.components.settings.text_editor.case_study_select_pages_error'
231
+ )
232
+ )
233
+ return
234
+ }
235
+
236
+ if (!force && this.blockHasMeaningfulText()) {
237
+ const confirmText = this.$t(
238
+ 'windward.core.components.settings.text_editor.case_study_replace_confirm'
239
+ )
240
+ this.$dialog.show(confirmText, {
241
+ icon: 'mdi-alert-outline',
242
+ duration: null,
243
+ action: [
244
+ {
245
+ text: this.$t('shared.forms.cancel'),
246
+ onClick: (_e, toastObject) => {
247
+ toastObject.goAway(0)
248
+ },
249
+ },
250
+ {
251
+ text: this.$t('shared.forms.confirm'),
252
+ onClick: (_e, toastObject) => {
253
+ toastObject.goAway(0)
254
+ this.onGenerateCaseStudy(true)
255
+ },
256
+ },
257
+ ],
258
+ })
259
+ return
260
+ }
261
+
262
+ this.isGeneratingCaseStudy = true
263
+
264
+ this.requestCaseStudyGeneration(contentIds)
265
+ .then((data) => {
266
+ if (!data || typeof data.html !== 'string') {
267
+ throw new Error('invalid_response')
268
+ }
269
+
270
+ this.block.body = data.html
271
+ this.updateKey = Crypto.id()
272
+
273
+ this.$toast?.success(
274
+ this.$t(
275
+ 'windward.core.components.settings.text_editor.case_study_generated'
276
+ )
277
+ )
278
+ })
279
+ .catch((error) => {
280
+ // eslint-disable-next-line no-console
281
+ console.error('Case study generation failed', error)
282
+
283
+ const messageKey = _.get(
284
+ error,
285
+ 'response.data.error.message',
286
+ ''
287
+ )
288
+ const details = _.get(
289
+ error,
290
+ 'response.data.error.details',
291
+ {}
292
+ )
293
+
294
+ const errorSubtype = _.get(details, 'error_subtype', '')
295
+ const errorType = String(messageKey).split('.').pop()
296
+
297
+ if (errorSubtype === 'CHARACTER_LIMIT_EXCEEDED') {
298
+ this.$toast?.error(
299
+ this.$t(
300
+ 'windward.core.components.settings.text_editor.case_study_character_limit'
301
+ )
302
+ )
303
+ } else if (
304
+ _.get(details, 'error_type', '') ===
305
+ 'INSUFFICIENT_CONTENT' ||
306
+ errorType === 'insufficient_content'
307
+ ) {
308
+ this.$toast?.error(
309
+ this.$t(
310
+ 'windward.core.components.settings.text_editor.case_study_insufficient_content'
311
+ )
312
+ )
313
+ } else {
314
+ this.$toast?.error(
315
+ this.$t(
316
+ 'windward.core.components.settings.text_editor.case_study_error'
317
+ )
318
+ )
319
+ }
320
+ })
321
+ .finally(() => {
322
+ this.isGeneratingCaseStudy = false
323
+ })
324
+ },
90
325
  },
91
326
  }
92
327
  </script>
93
328
 
94
- <style scoped></style>
329
+ <style scoped>
330
+ .case-study-controls {
331
+ max-width: 100%;
332
+ }
333
+
334
+ .case-study-page-selector >>> .v-select__selections {
335
+ padding-top: 8px;
336
+ }
337
+ </style>
@@ -180,7 +180,7 @@ export default {
180
180
  !Uuid.test(this.block.id)
181
181
  ) {
182
182
  this.block.metadata.config.instructions = this.$t(
183
- 'windward.core.components.settings.user_upload.instructions'
183
+ 'windward.core.components.content.blocks.user_upload.instructions'
184
184
  )
185
185
  }
186
186
  if (_.isEmpty(this.block.metadata.config.uploadSettings)) {
@@ -3,7 +3,7 @@
3
3
  <v-text-field
4
4
  id="glossary-form-term"
5
5
  v-model="selectedTerm.term"
6
- :counter="50"
6
+ :counter="75"
7
7
  :label="$t('windward.core.pages.glossary.term')"
8
8
  required
9
9
  :rules="validation.termRules"
@@ -80,8 +80,8 @@ export default {
80
80
  termRules: [
81
81
  (v) => !!v || this.$t('shared.forms.errors.required'),
82
82
  (v) =>
83
- (v && v.length <= 50) ||
84
- this.$t('shared.forms.errors.text_lt', [50]),
83
+ (v && v.length <= 75) ||
84
+ this.$t('shared.forms.errors.text_lt', [75]),
85
85
  (v) =>
86
86
  (v && this.termIsUnique(v)) ||
87
87
  this.$t('shared.forms.errors.field_unique'),
@@ -2,10 +2,16 @@ export default {
2
2
  title: 'File Submission',
3
3
  user_uploads: 'User Uploads',
4
4
  dialog_view: 'View Uploads',
5
- instructions_none: 'None',
5
+ instructions:
6
+ 'Upload your files below. Then, review your current uploads, and select Submit when you are ready.',
6
7
  must_save: 'You must save this block before users can make uploads.',
7
8
  uploaded: 'Uploaded',
8
9
  name: 'Name',
9
10
  size: 'Size',
10
11
  download: 'Download',
12
+ review_current_uploads: 'Review Current Uploads',
13
+ submission_history: 'Submission History',
14
+ submission: 'Submission',
15
+ filename: 'Filename',
16
+ file_size: 'File Size',
11
17
  }
@@ -7,4 +7,18 @@ export default {
7
7
  click_to_expand: 'Click to expand editor',
8
8
  click_to_hide_editor: 'Click to hide editor',
9
9
  click_to_hide_glossary: 'Click to hide glossary',
10
+ case_study_help:
11
+ 'Select one or more pages to generate a fictional case study scenario based on their content.',
12
+ case_study_selected_pages: 'Source pages',
13
+ generate_case_study: 'Generate Case Study',
14
+ case_study_select_pages_error: 'Select at least one page to generate a case study.',
15
+ case_study_replace_confirm:
16
+ 'This will replace the current text in this block. Continue?',
17
+ case_study_generated: 'Case study generated.',
18
+ case_study_insufficient_content:
19
+ 'Not enough content on the selected pages. Add more content or select additional pages.',
20
+ case_study_character_limit:
21
+ 'The selected pages contain too much content. Select fewer pages and try again.',
22
+ case_study_error:
23
+ 'Unable to generate case study. Try selecting fewer pages or edit manually.',
10
24
  }
@@ -1,5 +1,5 @@
1
1
  export default {
2
- accept_multiple: 'Multiple Files',
2
+ accept_multiple: 'Allow submission of multiple files.',
3
3
  accept_types: 'Allowed File Types',
4
4
  types: {
5
5
  all: 'All File Types',
@@ -2,11 +2,17 @@ export default {
2
2
  title: 'Envío de archivos',
3
3
  user_uploads: 'Cargas de usuarios',
4
4
  dialog_view: 'Ver cargas',
5
- instructions_none: 'Ninguno',
5
+ instructions:
6
+ 'Sube tus archivos a continuación. Luego, revisa tus archivos actuales y selecciona "Enviar" cuando estés listo.',
6
7
  must_save:
7
8
  'Debes guardar este bloque antes de que los usuarios puedan realizar cargas.',
8
9
  uploaded: 'Subido',
9
10
  name: 'Nombre',
10
11
  size: 'Tamaño',
11
12
  download: 'Descargar',
13
+ review_current_uploads: 'Revisar las cargas actuales',
14
+ submission_history: 'Historial de envíos',
15
+ submission: 'Envío',
16
+ filename: 'Nombre del archivo',
17
+ file_size: 'Tamaño de archivo',
12
18
  }
@@ -7,4 +7,19 @@ export default {
7
7
  click_to_expand: 'Haga Click ampliar el editor',
8
8
  click_to_hide_editor: 'Haga click para esconder el editor',
9
9
  click_to_hide_glossary: 'Haga click para esconder el glosario',
10
+ case_study_help:
11
+ 'Seleccione una o más páginas para generar un caso práctico ficticio basado en su contenido.',
12
+ case_study_selected_pages: 'Páginas de origen',
13
+ generate_case_study: 'Generar caso práctico',
14
+ case_study_select_pages_error:
15
+ 'Seleccione al menos una página para generar un caso práctico.',
16
+ case_study_replace_confirm:
17
+ 'Esto reemplazará el texto actual de este bloque. ¿Continuar?',
18
+ case_study_generated: 'Caso práctico generado.',
19
+ case_study_insufficient_content:
20
+ 'No hay suficiente contenido en las páginas seleccionadas. Agregue más contenido o seleccione páginas adicionales.',
21
+ case_study_character_limit:
22
+ 'Las páginas seleccionadas contienen demasiado contenido. Seleccione menos páginas e inténtelo de nuevo.',
23
+ case_study_error:
24
+ 'No se pudo generar el caso práctico. Intente seleccionar menos páginas o edite manualmente.',
10
25
  }
@@ -1,5 +1,5 @@
1
1
  export default {
2
- accept_multiple: 'Archivos Múltiples',
2
+ accept_multiple: 'Permitir el envío de múltiples archivos.',
3
3
  accept_types: 'Tipos de archivos permitidos ',
4
4
  types: {
5
5
  all: 'Todos los tipos de archivos',
@@ -2,11 +2,17 @@ export default {
2
2
  title: 'Filinlämning',
3
3
  user_uploads: 'Användaruppladdningar',
4
4
  dialog_view: 'Visa uppladdningar',
5
- instructions_none: 'Ingen',
5
+ instructions:
6
+ 'Ladda upp dina filer nedan. Granska sedan dina aktuella uppladdningar och välj Skicka när du är redo.',
6
7
  must_save:
7
8
  'Du måste spara detta block innan användare kan göra uppladdningar.',
8
9
  uploaded: 'Uppladdat',
9
10
  name: 'Namn',
10
11
  size: 'Storlek',
11
12
  download: 'Ladda ner',
13
+ review_current_uploads: 'Granska aktuella uppladdningar',
14
+ submission_history: 'Inlämningshistorik',
15
+ submission: 'Underkastelse',
16
+ filename: 'Filnamn',
17
+ file_size: 'Fil-storlek',
12
18
  }
@@ -7,4 +7,19 @@ export default {
7
7
  click_to_expand: 'klicka för att förbruka',
8
8
  click_to_hide_editor: 'klicka för att dölja editorn',
9
9
  click_to_hide_glossary: 'klicka för att dölja ordlistan',
10
+ case_study_help:
11
+ 'Välj en eller flera sidor för att generera ett fiktivt fallstudiescenario baserat på deras innehåll.',
12
+ case_study_selected_pages: 'Källsidor',
13
+ generate_case_study: 'Generera fallstudie',
14
+ case_study_select_pages_error:
15
+ 'Välj minst en sida för att generera en fallstudie.',
16
+ case_study_replace_confirm:
17
+ 'Detta kommer att ersätta den nuvarande texten i detta block. Fortsätt?',
18
+ case_study_generated: 'Fallstudie genererad.',
19
+ case_study_insufficient_content:
20
+ 'Inte tillräckligt med innehåll på de valda sidorna. Lägg till mer innehåll eller välj fler sidor.',
21
+ case_study_character_limit:
22
+ 'De valda sidorna innehåller för mycket innehåll. Välj färre sidor och försök igen.',
23
+ case_study_error:
24
+ 'Kunde inte generera fallstudie. Försök välja färre sidor eller redigera manuellt.',
10
25
  }
@@ -1,5 +1,5 @@
1
1
  export default {
2
- accept_multiple: 'Flera filer',
2
+ accept_multiple: 'Tillåt inlämning av flera filer.',
3
3
  accept_types: 'Tillåtna filtyper ',
4
4
  types: {
5
5
  all: 'Alla filtyper',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/core",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {