@windward/games 0.23.0 → 0.24.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.24.0] - 2025-10-23
4
+
5
+ * Merged in feature/LE-2034/generate-crossword (pull request #262)
6
+ * Merged in bugfix/LE-2020-the-quiz-show-manager-panel-does (pull request #263)
7
+
8
+
3
9
  ## Release [0.23.0] - 2025-09-16
4
10
 
5
11
  * Merged in feature/LE-2035/seven-strikes (pull request #259)
@@ -31,10 +31,10 @@ export default {
31
31
  icons: {
32
32
  type: Object,
33
33
  required: false,
34
- default: {
34
+ default: () => ({
35
35
  success: 'mdi-check-circle-outline',
36
36
  error: 'mdi-close-circle-outline',
37
- },
37
+ }),
38
38
  },
39
39
  },
40
40
  }
@@ -90,6 +90,24 @@
90
90
  </v-row>
91
91
  </v-container>
92
92
  </v-container>
93
+ <v-container class="pa-4 mb-6">
94
+ <v-row>
95
+ <v-col cols="12">
96
+ <PluginRef
97
+ target="contentBlockSettingTool"
98
+ :attrs="{
99
+ value: block,
100
+ course: course,
101
+ content: currentContent,
102
+ }"
103
+ :on="{
104
+ input: onPluginSetBlock,
105
+ append: onPluginAppendBlock,
106
+ }"
107
+ ></PluginRef>
108
+ </v-col>
109
+ </v-row>
110
+ </v-container>
93
111
  <div v-if="loading" class="text-center">
94
112
  <v-progress-circular
95
113
  :size="70"
@@ -103,15 +121,17 @@
103
121
 
104
122
  <script>
105
123
  import _ from 'lodash'
124
+ import { mapGetters } from 'vuex'
106
125
  import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
107
126
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
108
127
  import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue'
128
+ import PluginRef from '~/components/Core/PluginRef.vue'
109
129
  import Uuid from '~/helpers/Uuid'
110
130
 
111
131
  export default {
112
132
  name: 'CrosswordPuzzleSettingsManager',
113
133
  extends: BaseContentSettings,
114
- components: { SortableExpansionPanel, BaseContentBlockSettings },
134
+ components: { SortableExpansionPanel, BaseContentBlockSettings, PluginRef },
115
135
  beforeMount() {
116
136
  if (_.isEmpty(this.block)) {
117
137
  this.block = {}
@@ -186,6 +206,12 @@ export default {
186
206
  },
187
207
  }
188
208
  },
209
+ computed: {
210
+ ...mapGetters({
211
+ course: 'course/get',
212
+ currentContent: 'content/get',
213
+ }),
214
+ },
189
215
  methods: {
190
216
  onAddElement() {
191
217
  // pushes in new crossword object
@@ -207,6 +233,227 @@ export default {
207
233
  this.onAddElement()
208
234
  }
209
235
  },
236
+ onPluginSetBlock(activityData) {
237
+ this.loading = true
238
+ try {
239
+ const result = this.processGeneratedCrossword(activityData)
240
+ if (!result) {
241
+ this.showInvalidResponseToast()
242
+ return
243
+ }
244
+
245
+ this.$set(
246
+ this.block.metadata,
247
+ 'config',
248
+ this.block.metadata.config || {}
249
+ )
250
+
251
+ this.$set(
252
+ this.block.metadata.config,
253
+ 'words',
254
+ result.words.map((item, index) => ({
255
+ id: index,
256
+ word: item.word,
257
+ clue: item.clue,
258
+ expand: false,
259
+ }))
260
+ )
261
+
262
+ if (result.title) {
263
+ this.block.metadata.config.title = result.title
264
+ }
265
+
266
+ this.$toast.success(
267
+ this.$t(
268
+ 'windward.games.components.settings.crossword.replaced_successfully'
269
+ ),
270
+ { duration: 3000 }
271
+ )
272
+ } catch (error) {
273
+ console.error('Failed to process generated crossword items', error)
274
+ this.$toast.error(
275
+ this.$t(
276
+ 'windward.games.components.settings.crossword.failed_to_process'
277
+ ),
278
+ { duration: 5000 }
279
+ )
280
+ } finally {
281
+ this.loading = false
282
+ }
283
+ },
284
+ onPluginAppendBlock(activityData) {
285
+ this.loading = true
286
+ try {
287
+ const result = this.processGeneratedCrossword(activityData)
288
+ if (!result) {
289
+ this.showInvalidResponseToast()
290
+ return
291
+ }
292
+
293
+ if (!Array.isArray(this.block.metadata.config.words)) {
294
+ this.$set(this.block.metadata.config, 'words', [])
295
+ }
296
+
297
+ const startingIndex = this.block.metadata.config.words.length
298
+
299
+ result.words.forEach((item, index) => {
300
+ this.block.metadata.config.words.push({
301
+ id: startingIndex + index,
302
+ word: item.word,
303
+ clue: item.clue,
304
+ expand: false,
305
+ })
306
+ })
307
+
308
+ this.renumberWordIds()
309
+
310
+ if (result.title) {
311
+ const defaultTitle = this.$t(
312
+ 'windward.games.components.content.blocks.crossword.crossword'
313
+ )
314
+ if (
315
+ !this.block.metadata.config.title ||
316
+ this.block.metadata.config.title === defaultTitle
317
+ ) {
318
+ this.block.metadata.config.title = result.title
319
+ }
320
+ }
321
+
322
+ this.$toast.success(
323
+ this.$t(
324
+ 'windward.games.components.settings.crossword.added_successfully'
325
+ ),
326
+ { duration: 3000 }
327
+ )
328
+ } catch (error) {
329
+ console.error('Failed to append generated crossword items', error)
330
+ this.$toast.error(
331
+ this.$t(
332
+ 'windward.games.components.settings.crossword.failed_to_process'
333
+ ),
334
+ { duration: 5000 }
335
+ )
336
+ } finally {
337
+ this.loading = false
338
+ }
339
+ },
340
+ processGeneratedCrossword(activityData) {
341
+ const config = _.get(activityData, 'metadata.config', null)
342
+ const words = _.get(config, 'words', [])
343
+
344
+ if (!Array.isArray(words) || words.length === 0) {
345
+ return null
346
+ }
347
+
348
+ const sanitizedWords = words
349
+ .map((item) => {
350
+ const sanitizedWord = this.normalizeWord(item?.word)
351
+ const sanitizedClue = this.normalizeClue(item?.clue)
352
+
353
+ if (!sanitizedWord || !sanitizedClue) {
354
+ return null
355
+ }
356
+
357
+ return {
358
+ word: sanitizedWord,
359
+ clue: sanitizedClue,
360
+ expand: false,
361
+ }
362
+ })
363
+ .filter((item) => item !== null)
364
+
365
+ if (sanitizedWords.length < 2) {
366
+ return null
367
+ }
368
+
369
+ const limitedWords = sanitizedWords.slice(0, 10)
370
+
371
+ const normalizedTitle = this.normalizeTitle(_.get(config, 'title', ''))
372
+
373
+ return {
374
+ title: normalizedTitle,
375
+ words: limitedWords,
376
+ }
377
+ },
378
+ normalizeWord(word) {
379
+ if (!word) {
380
+ return ''
381
+ }
382
+
383
+ const formatted = word
384
+ .toString()
385
+ .normalize('NFKD')
386
+ .replace(/[^A-Za-z]/g, '')
387
+ .toUpperCase()
388
+ .slice(0, 20)
389
+
390
+ return formatted
391
+ },
392
+ normalizeClue(clue) {
393
+ if (!clue) {
394
+ return ''
395
+ }
396
+
397
+ let normalized = clue
398
+ .toString()
399
+ .replace(/\s+/g, ' ')
400
+ .trim()
401
+
402
+ if (!normalized) {
403
+ return ''
404
+ }
405
+
406
+ if (normalized.endsWith('?')) {
407
+ normalized = normalized.slice(0, -1).trim()
408
+ }
409
+
410
+ const maxLength = 155
411
+ if (normalized.length > maxLength) {
412
+ normalized = normalized.slice(0, maxLength).trim()
413
+ }
414
+
415
+ if (normalized && !/[.!]$/.test(normalized)) {
416
+ if (normalized.length >= maxLength) {
417
+ normalized = normalized.slice(0, maxLength - 1).trim()
418
+ }
419
+ normalized += '.'
420
+ }
421
+
422
+ return normalized
423
+ },
424
+ normalizeTitle(title) {
425
+ if (!title) {
426
+ return ''
427
+ }
428
+
429
+ let normalized = title
430
+ .toString()
431
+ .replace(/\s+/g, ' ')
432
+ .trim()
433
+
434
+ if (!normalized) {
435
+ return ''
436
+ }
437
+
438
+ if (normalized.length > 100) {
439
+ normalized = normalized.slice(0, 100).trim()
440
+ }
441
+
442
+ return normalized
443
+ },
444
+ renumberWordIds() {
445
+ this.block.metadata.config.words.forEach((item, index) => {
446
+ item.id = index
447
+ })
448
+ },
449
+ showInvalidResponseToast() {
450
+ this.$toast.error(
451
+ this.$t(
452
+ 'windward.games.components.settings.crossword.invalid_response'
453
+ ),
454
+ { duration: 5000 }
455
+ )
456
+ },
210
457
  },
211
458
  }
212
459
  </script>
@@ -267,7 +267,8 @@ export default {
267
267
  },
268
268
  computed: {},
269
269
  beforeMount() {
270
- if (_.isEmpty(this.block.metadata.config)) {
270
+ // since display_title is already set so we'll look at a quiz show property to set settings for first mount
271
+ if (_.isEmpty(this.block.metadata.config.title)) {
271
272
  this.block.metadata.config = _.cloneDeep(this.quizShowSettings)
272
273
  }
273
274
  if (!_.isBoolean(this.block.metadata.config.display_title)) {
@@ -5,4 +5,8 @@ export default {
5
5
  min_length: 'Cannot have less than two words',
6
6
  alert: 'Alert',
7
7
  instructions: 'Complete the crossword by clicking on each square to type the appropriate letter. Use the tab key to move left to right across the grid. Click on the clue to highlight the corresponding word in the puzzle.',
8
+ replaced_successfully: 'Crossword items replaced successfully',
9
+ added_successfully: 'Crossword items added successfully',
10
+ failed_to_process: 'Failed to process generated crossword items',
11
+ invalid_response: 'Unable to process generated crossword items',
8
12
  }
@@ -6,4 +6,8 @@ export default {
6
6
  alert: 'Alerta',
7
7
  instructions:
8
8
  'Complete el crucigrama haciendo clic en cada cuadrado para escribir la letra correspondiente. Utilice la tecla de tabulación para moverse de izquierda a derecha a través de la cuadrícula. Haga clic en la pista para resaltar la palabra correspondiente en el rompecabezas.',
9
+ replaced_successfully: 'Elementos del crucigrama reemplazados correctamente',
10
+ added_successfully: 'Elementos del crucigrama agregados correctamente',
11
+ failed_to_process: 'Error al procesar los elementos generados del crucigrama',
12
+ invalid_response: 'No se pudieron procesar los elementos generados del crucigrama',
9
13
  }
@@ -5,4 +5,8 @@ export default {
5
5
  min_length: 'Kan inte ha mindre än två ord',
6
6
  alert: 'Alert',
7
7
  instructions: 'Fyll i korsordet genom att klicka på varje ruta för att skriva rätt bokstav. Använd tabbtangenten för att flytta från vänster till höger över rutnätet. Klicka på ledtråden för att markera motsvarande ord i pusslet.',
8
+ replaced_successfully: 'Korsordsobjekt ersattes',
9
+ added_successfully: 'Korsordsobjekt lades till',
10
+ failed_to_process: 'Det gick inte att bearbeta genererade korsordsobjekt',
11
+ invalid_response: 'Det gick inte att behandla genererade korsordsobjekt',
8
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/games",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Windward UI Plugin Games",
5
5
  "main": "plugin.js",
6
6
  "scripts": {