@windward/integrations 0.21.0 → 0.23.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/Content/Blocks/ActionPanel/TransformActivity.vue +386 -0
  3. package/components/Content/Blocks/ActionPanel/TransformBlock.vue +402 -59
  4. package/components/Content/Blocks/ExternalIntegration/LtiConsumer.vue +22 -2
  5. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +8 -2
  6. package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +4 -0
  7. package/components/ExternalIntegration/Driver/Lti1p3/ManageConsumer.vue +2 -2
  8. package/components/Integration/JobTable.vue +35 -3
  9. package/components/Integration/TranslateCourse.vue +574 -0
  10. package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +27 -0
  11. package/i18n/ar-SA/components/ai_agent/chat.ts +20 -0
  12. package/i18n/ar-SA/components/ai_agent/index.ts +5 -0
  13. package/i18n/ar-SA/components/content/blocks/action_panel/index.ts +5 -0
  14. package/i18n/ar-SA/components/content/blocks/action_panel/transform_block.ts +9 -0
  15. package/i18n/ar-SA/components/content/blocks/external_integration/index.ts +5 -0
  16. package/i18n/ar-SA/components/content/blocks/external_integration/lti_consumer.ts +17 -0
  17. package/i18n/ar-SA/components/content/blocks/index.ts +7 -0
  18. package/i18n/ar-SA/components/content/index.ts +5 -0
  19. package/i18n/ar-SA/components/external_integration/driver/lti1p1.ts +17 -0
  20. package/i18n/ar-SA/components/external_integration/driver/lti1p3.ts +25 -0
  21. package/i18n/ar-SA/components/external_integration/driver/scorm.ts +14 -0
  22. package/i18n/ar-SA/components/external_integration/index.ts +36 -0
  23. package/i18n/ar-SA/components/external_integration/provider_target.ts +9 -0
  24. package/i18n/ar-SA/components/file_import/index.ts +5 -0
  25. package/i18n/ar-SA/components/file_import/resourcespace.ts +4 -0
  26. package/i18n/ar-SA/components/index.ts +19 -0
  27. package/i18n/ar-SA/components/integration/driver.ts +67 -0
  28. package/i18n/ar-SA/components/integration/index.ts +9 -0
  29. package/i18n/ar-SA/components/integration/job.ts +25 -0
  30. package/i18n/ar-SA/components/integration/job_log.ts +24 -0
  31. package/i18n/ar-SA/components/llm/blooms.ts +15 -0
  32. package/i18n/ar-SA/components/llm/content_selector.ts +3 -0
  33. package/i18n/ar-SA/components/llm/generate_content/fake_text_stream.ts +62 -0
  34. package/i18n/ar-SA/components/llm/generate_content/generate_questions.ts +92 -0
  35. package/i18n/ar-SA/components/llm/generate_content/index.ts +8 -0
  36. package/i18n/ar-SA/components/llm/index.ts +10 -0
  37. package/i18n/ar-SA/components/navigation/index.ts +5 -0
  38. package/i18n/ar-SA/components/navigation/integrations.ts +9 -0
  39. package/i18n/ar-SA/components/settings/external_integration/index.ts +5 -0
  40. package/i18n/ar-SA/components/settings/external_integration/lti_consumer.ts +10 -0
  41. package/i18n/ar-SA/components/settings/index.ts +5 -0
  42. package/i18n/ar-SA/index.ts +16 -0
  43. package/i18n/ar-SA/modules/index.ts +5 -0
  44. package/i18n/ar-SA/pages/admin/index.ts +5 -0
  45. package/i18n/ar-SA/pages/admin/translateCourse.ts +68 -0
  46. package/i18n/ar-SA/pages/course/external_integration/index.ts +6 -0
  47. package/i18n/ar-SA/pages/course/index.ts +5 -0
  48. package/i18n/ar-SA/pages/importContent.ts +3 -0
  49. package/i18n/ar-SA/pages/importCourse.ts +13 -0
  50. package/i18n/ar-SA/pages/index.ts +15 -0
  51. package/i18n/ar-SA/pages/login/index.ts +9 -0
  52. package/i18n/ar-SA/pages/login/lti.ts +23 -0
  53. package/i18n/ar-SA/pages/login/saml.ts +7 -0
  54. package/i18n/ar-SA/pages/login/scorm.ts +28 -0
  55. package/i18n/ar-SA/pages/vendor.ts +11 -0
  56. package/i18n/ar-SA/shared/content_blocks.ts +9 -0
  57. package/i18n/ar-SA/shared/error.ts +9 -0
  58. package/i18n/ar-SA/shared/file.ts +5 -0
  59. package/i18n/ar-SA/shared/index.ts +17 -0
  60. package/i18n/ar-SA/shared/menu.ts +3 -0
  61. package/i18n/ar-SA/shared/notification.ts +14 -0
  62. package/i18n/ar-SA/shared/permission.ts +52 -0
  63. package/i18n/ar-SA/shared/settings.ts +9 -0
  64. package/i18n/en-US/components/content/blocks/action_panel/index.ts +2 -0
  65. package/i18n/en-US/components/content/blocks/action_panel/transform_activity.ts +8 -0
  66. package/i18n/en-US/components/content/blocks/action_panel/transform_block.ts +1 -1
  67. package/i18n/en-US/components/content/blocks/external_integration/lti_consumer.ts +1 -0
  68. package/i18n/en-US/components/integration/job.ts +2 -0
  69. package/i18n/en-US/components/navigation/integrations.ts +1 -0
  70. package/i18n/en-US/components/settings/external_integration/lti_consumer.ts +2 -0
  71. package/i18n/en-US/pages/admin/index.ts +5 -0
  72. package/i18n/en-US/pages/admin/translateCourse.ts +68 -0
  73. package/i18n/en-US/pages/index.ts +2 -0
  74. package/i18n/es-ES/components/content/blocks/action_panel/index.ts +2 -0
  75. package/i18n/es-ES/components/content/blocks/action_panel/transform_activity.ts +8 -0
  76. package/i18n/es-ES/components/content/blocks/action_panel/transform_block.ts +1 -1
  77. package/i18n/es-ES/components/content/blocks/external_integration/lti_consumer.ts +8 -7
  78. package/i18n/es-ES/components/integration/job.ts +2 -0
  79. package/i18n/es-ES/components/navigation/integrations.ts +1 -0
  80. package/i18n/es-ES/components/settings/external_integration/lti_consumer.ts +3 -0
  81. package/i18n/es-ES/pages/admin/index.ts +5 -0
  82. package/i18n/es-ES/pages/admin/translateCourse.ts +68 -0
  83. package/i18n/es-ES/pages/index.ts +2 -0
  84. package/i18n/fr-FR/components/ai_agent/chat.ts +20 -0
  85. package/i18n/fr-FR/components/ai_agent/index.ts +5 -0
  86. package/i18n/fr-FR/components/content/blocks/action_panel/index.ts +5 -0
  87. package/i18n/fr-FR/components/content/blocks/action_panel/transform_block.ts +9 -0
  88. package/i18n/fr-FR/components/content/blocks/external_integration/index.ts +5 -0
  89. package/i18n/fr-FR/components/content/blocks/external_integration/lti_consumer.ts +17 -0
  90. package/i18n/fr-FR/components/content/blocks/index.ts +7 -0
  91. package/i18n/fr-FR/components/content/index.ts +5 -0
  92. package/i18n/fr-FR/components/external_integration/driver/lti1p1.ts +17 -0
  93. package/i18n/fr-FR/components/external_integration/driver/lti1p3.ts +25 -0
  94. package/i18n/fr-FR/components/external_integration/driver/scorm.ts +14 -0
  95. package/i18n/fr-FR/components/external_integration/index.ts +36 -0
  96. package/i18n/fr-FR/components/external_integration/provider_target.ts +9 -0
  97. package/i18n/fr-FR/components/file_import/index.ts +5 -0
  98. package/i18n/fr-FR/components/file_import/resourcespace.ts +4 -0
  99. package/i18n/fr-FR/components/index.ts +19 -0
  100. package/i18n/fr-FR/components/integration/driver.ts +67 -0
  101. package/i18n/fr-FR/components/integration/index.ts +9 -0
  102. package/i18n/fr-FR/components/integration/job.ts +25 -0
  103. package/i18n/fr-FR/components/integration/job_log.ts +24 -0
  104. package/i18n/fr-FR/components/llm/blooms.ts +15 -0
  105. package/i18n/fr-FR/components/llm/content_selector.ts +3 -0
  106. package/i18n/fr-FR/components/llm/generate_content/fake_text_stream.ts +62 -0
  107. package/i18n/fr-FR/components/llm/generate_content/generate_questions.ts +92 -0
  108. package/i18n/fr-FR/components/llm/generate_content/index.ts +8 -0
  109. package/i18n/fr-FR/components/llm/index.ts +10 -0
  110. package/i18n/fr-FR/components/navigation/index.ts +5 -0
  111. package/i18n/fr-FR/components/navigation/integrations.ts +9 -0
  112. package/i18n/fr-FR/components/settings/external_integration/index.ts +5 -0
  113. package/i18n/fr-FR/components/settings/external_integration/lti_consumer.ts +10 -0
  114. package/i18n/fr-FR/components/settings/index.ts +5 -0
  115. package/i18n/fr-FR/index.ts +16 -0
  116. package/i18n/fr-FR/modules/index.ts +5 -0
  117. package/i18n/fr-FR/pages/admin/index.ts +5 -0
  118. package/i18n/fr-FR/pages/admin/translateCourse.ts +68 -0
  119. package/i18n/fr-FR/pages/course/external_integration/index.ts +6 -0
  120. package/i18n/fr-FR/pages/course/index.ts +5 -0
  121. package/i18n/fr-FR/pages/importContent.ts +3 -0
  122. package/i18n/fr-FR/pages/importCourse.ts +13 -0
  123. package/i18n/fr-FR/pages/index.ts +15 -0
  124. package/i18n/fr-FR/pages/login/index.ts +9 -0
  125. package/i18n/fr-FR/pages/login/lti.ts +23 -0
  126. package/i18n/fr-FR/pages/login/saml.ts +7 -0
  127. package/i18n/fr-FR/pages/login/scorm.ts +28 -0
  128. package/i18n/fr-FR/pages/vendor.ts +11 -0
  129. package/i18n/fr-FR/shared/content_blocks.ts +9 -0
  130. package/i18n/fr-FR/shared/error.ts +9 -0
  131. package/i18n/fr-FR/shared/file.ts +5 -0
  132. package/i18n/fr-FR/shared/index.ts +17 -0
  133. package/i18n/fr-FR/shared/menu.ts +3 -0
  134. package/i18n/fr-FR/shared/notification.ts +14 -0
  135. package/i18n/fr-FR/shared/permission.ts +52 -0
  136. package/i18n/fr-FR/shared/settings.ts +9 -0
  137. package/i18n/index.ts +4 -0
  138. package/i18n/sv-SE/components/content/blocks/action_panel/index.ts +2 -0
  139. package/i18n/sv-SE/components/content/blocks/action_panel/transform_activity.ts +8 -0
  140. package/i18n/sv-SE/components/content/blocks/action_panel/transform_block.ts +1 -1
  141. package/i18n/sv-SE/components/content/blocks/external_integration/lti_consumer.ts +9 -8
  142. package/i18n/sv-SE/components/integration/job.ts +6 -4
  143. package/i18n/sv-SE/components/navigation/integrations.ts +1 -0
  144. package/i18n/sv-SE/components/settings/external_integration/lti_consumer.ts +3 -0
  145. package/i18n/sv-SE/pages/admin/index.ts +5 -0
  146. package/i18n/sv-SE/pages/admin/translateCourse.ts +68 -0
  147. package/i18n/sv-SE/pages/index.ts +2 -0
  148. package/package.json +1 -1
  149. package/pages/admin/translateCourse.vue +81 -0
  150. package/plugin.js +32 -0
@@ -38,6 +38,9 @@ import { mapGetters } from 'vuex'
38
38
  import Organization from '~/models/Organization'
39
39
  import Course from '~/models/Course'
40
40
 
41
+ const MIN_SENTENCE_COUNT_FOR_TEXT_TRANSFORM = 4
42
+ const SENTENCES_PER_PARAGRAPH = 2
43
+
41
44
  export default {
42
45
  props: {
43
46
  // The block associated with this action panel
@@ -71,6 +74,7 @@ export default {
71
74
  ...mapGetters({
72
75
  organization: 'organization/get',
73
76
  course: 'course/get',
77
+ activeContentBlock: 'contentBlock/get',
74
78
  }),
75
79
  vModel: {
76
80
  get() {
@@ -80,28 +84,55 @@ export default {
80
84
  this.$emit('input', value)
81
85
  },
82
86
  },
87
+ sourceBlock() {
88
+ if (!this.value) {
89
+ return null
90
+ }
91
+
92
+ const active = this.activeContentBlock
93
+ if (
94
+ active &&
95
+ typeof active === 'object' &&
96
+ _.get(active, 'id', null) &&
97
+ _.get(active, 'id', null) === _.get(this.value, 'id', null)
98
+ ) {
99
+ return active
100
+ }
101
+
102
+ return this.value
103
+ },
83
104
  sourceKind() {
84
- return this.getBlockKindFromTag(_.get(this.value, 'tag', ''))
105
+ return this.getBlockKindFromTag(_.get(this.sourceBlock, 'tag', ''))
85
106
  },
86
- textParagraphCount() {
107
+ textSentenceCount() {
87
108
  if (this.sourceKind !== 'text') {
88
109
  return 0
89
110
  }
90
- return this.countTextParagraphs(_.get(this.value, 'body', ''))
111
+ return this.countTextSentences(
112
+ _.get(this.sourceBlock, 'body', ''),
113
+ this.$i18n?.locale
114
+ )
91
115
  },
92
116
  structuredItemCount() {
93
- if (!['accordion', 'tab', 'clickable_icons'].includes(this.sourceKind)) {
117
+ if (
118
+ !['accordion', 'tab', 'clickable_icons'].includes(
119
+ this.sourceKind
120
+ )
121
+ ) {
94
122
  return 0
95
123
  }
96
- return this.countStructuredItems(this.value)
124
+ return this.countStructuredItems(this.sourceBlock)
97
125
  },
98
126
  availableActions() {
99
- if (!this.value) {
127
+ if (!this.sourceBlock) {
100
128
  return []
101
129
  }
102
130
 
103
131
  if (this.sourceKind === 'text') {
104
- if (this.textParagraphCount < 2) {
132
+ if (
133
+ this.textSentenceCount <
134
+ MIN_SENTENCE_COUNT_FOR_TEXT_TRANSFORM
135
+ ) {
105
136
  return []
106
137
  }
107
138
  return [
@@ -127,9 +158,6 @@ export default {
127
158
  }
128
159
 
129
160
  if (this.sourceKind === 'accordion') {
130
- if (this.structuredItemCount < 2) {
131
- return []
132
- }
133
161
  const actions = [
134
162
  {
135
163
  tag: 'content-blocks-text',
@@ -158,9 +186,6 @@ export default {
158
186
  }
159
187
 
160
188
  if (this.sourceKind === 'tab') {
161
- if (this.structuredItemCount < 2) {
162
- return []
163
- }
164
189
  const actions = [
165
190
  {
166
191
  tag: 'content-blocks-text',
@@ -189,9 +214,6 @@ export default {
189
214
  }
190
215
 
191
216
  if (this.sourceKind === 'clickable_icons') {
192
- if (this.structuredItemCount < 2) {
193
- return []
194
- }
195
217
  const actions = [
196
218
  {
197
219
  tag: 'content-blocks-text',
@@ -251,26 +273,328 @@ export default {
251
273
 
252
274
  return 'unknown'
253
275
  },
254
- countTextParagraphs(html) {
276
+ extractTextFromHtml(html) {
277
+ if (!html || typeof html !== 'string') {
278
+ return ''
279
+ }
280
+
281
+ if (typeof DOMParser === 'undefined') {
282
+ return html.replace(/<[^>]+>/g, ' ')
283
+ }
284
+
285
+ try {
286
+ const doc = new DOMParser().parseFromString(html, 'text/html')
287
+ return (doc?.body?.textContent || '').trim()
288
+ } catch (_e) {
289
+ return html.replace(/<[^>]+>/g, ' ')
290
+ }
291
+ },
292
+ segmentSentences(text, locale) {
293
+ const input = typeof text === 'string' ? text : ''
294
+ if (!input.trim()) {
295
+ return []
296
+ }
297
+
298
+ try {
299
+ if (typeof Intl !== 'undefined' && Intl.Segmenter) {
300
+ const segmenter = new Intl.Segmenter(locale || undefined, {
301
+ granularity: 'sentence',
302
+ })
303
+ const rawSegments = Array.from(segmenter.segment(input))
304
+
305
+ return rawSegments
306
+ .map((seg, idx) => {
307
+ const start = Number(seg.index || 0)
308
+ const end =
309
+ idx + 1 < rawSegments.length
310
+ ? Number(rawSegments[idx + 1].index || 0)
311
+ : input.length
312
+ return {
313
+ start,
314
+ end,
315
+ text: input.slice(start, end),
316
+ }
317
+ })
318
+ .filter((seg) => seg.text.trim().length > 0)
319
+ }
320
+ } catch (_e) {
321
+ // Ignore and fall back to regex
322
+ }
323
+
324
+ // Fallback sentence splitter (best-effort)
325
+ const re = /[^.!?]+[.!?]+(?:\s+|$)|[^.!?]+$/g
326
+ const segments = []
327
+ let match = null
328
+ while ((match = re.exec(input)) !== null) {
329
+ const value = match[0] || ''
330
+ if (!value.trim()) {
331
+ continue
332
+ }
333
+ segments.push({
334
+ start: match.index,
335
+ end: match.index + value.length,
336
+ text: value,
337
+ })
338
+ }
339
+ return segments
340
+ },
341
+ countTextSentences(html, locale) {
342
+ const text = this.extractTextFromHtml(html)
343
+ return this.segmentSentences(text, locale).length
344
+ },
345
+ countTopLevelParagraphs(html) {
255
346
  if (!html || typeof html !== 'string') {
256
347
  return 0
257
348
  }
258
349
 
350
+ if (typeof DOMParser === 'undefined') {
351
+ return 0
352
+ }
353
+
259
354
  try {
260
355
  const doc = new DOMParser().parseFromString(html, 'text/html')
261
- const paragraphs = Array.from(doc.querySelectorAll('p, li'))
262
- return paragraphs.filter((el) => (el.textContent || '').trim())
263
- .length
356
+ const body = doc?.body
357
+ if (!body) {
358
+ return 0
359
+ }
360
+
361
+ const paragraphs = Array.from(body.children).filter((el) => {
362
+ const tag = (el.tagName || '').toLowerCase()
363
+ if (tag !== 'p' && tag !== 'li') {
364
+ return false
365
+ }
366
+ return (el.textContent || '').trim().length > 0
367
+ })
368
+
369
+ return paragraphs.length
264
370
  } catch (_e) {
265
- // Fallback: simple split on line breaks after stripping tags
266
- const normalized = html
267
- .replace(/<[^>]+>/g, '\n')
268
- .split(/\n+/)
269
- .map((p) => p.trim())
270
- .filter((p) => p.length > 0)
271
- return normalized.length
371
+ return 0
272
372
  }
273
373
  },
374
+ resolveTextOffset(textNodes, targetOffset) {
375
+ let offset = Math.max(0, Math.trunc(Number(targetOffset) || 0))
376
+
377
+ for (const node of textNodes) {
378
+ const len = (node.nodeValue || '').length
379
+ if (offset <= len) {
380
+ return { node, offset }
381
+ }
382
+ offset -= len
383
+ }
384
+
385
+ const last = textNodes[textNodes.length - 1]
386
+ return { node: last, offset: (last?.nodeValue || '').length }
387
+ },
388
+ splitHtmlIntoParagraphs(html, locale) {
389
+ if (!html || typeof html !== 'string') {
390
+ return null
391
+ }
392
+
393
+ if (typeof DOMParser === 'undefined') {
394
+ return null
395
+ }
396
+
397
+ const doc = new DOMParser().parseFromString(html, 'text/html')
398
+ const body = doc?.body
399
+ if (!body) {
400
+ return null
401
+ }
402
+
403
+ const hasMeaningfulTextOutsideElements = Array.from(
404
+ body.childNodes
405
+ ).some(
406
+ (node) =>
407
+ node.nodeType === Node.TEXT_NODE &&
408
+ (node.textContent || '').trim()
409
+ )
410
+ const elementChildren = Array.from(body.children)
411
+
412
+ let container = null
413
+ if (
414
+ elementChildren.length === 1 &&
415
+ !hasMeaningfulTextOutsideElements
416
+ ) {
417
+ container = elementChildren[0]
418
+ } else if (
419
+ elementChildren.length === 0 &&
420
+ (body.textContent || '').trim()
421
+ ) {
422
+ container = body
423
+ } else {
424
+ return null
425
+ }
426
+
427
+ const containerTag =
428
+ container === body
429
+ ? 'body'
430
+ : (container.tagName || '').toLowerCase()
431
+ const disallowedContainers = new Set([
432
+ 'ul',
433
+ 'ol',
434
+ 'table',
435
+ 'tbody',
436
+ 'thead',
437
+ 'tr',
438
+ 'td',
439
+ 'th',
440
+ ])
441
+ if (disallowedContainers.has(containerTag)) {
442
+ return null
443
+ }
444
+
445
+ // Avoid creating nested paragraphs by splitting inside containers that already have paragraphs
446
+ if (container !== body && containerTag !== 'p') {
447
+ if (
448
+ typeof container.querySelector === 'function' &&
449
+ container.querySelector('p')
450
+ ) {
451
+ return null
452
+ }
453
+ }
454
+
455
+ const text = container.textContent || ''
456
+ const sentences = this.segmentSentences(text, locale)
457
+ if (sentences.length < MIN_SENTENCE_COUNT_FOR_TEXT_TRANSFORM) {
458
+ return null
459
+ }
460
+
461
+ const ranges = []
462
+ for (
463
+ let i = 0;
464
+ i < sentences.length;
465
+ i += SENTENCES_PER_PARAGRAPH
466
+ ) {
467
+ const start = sentences[i].start
468
+ const end =
469
+ sentences[
470
+ Math.min(
471
+ i + SENTENCES_PER_PARAGRAPH - 1,
472
+ sentences.length - 1
473
+ )
474
+ ].end
475
+ ranges.push({ start, end })
476
+ }
477
+
478
+ const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT)
479
+ const textNodes = []
480
+ let current = walker.nextNode()
481
+ while (current) {
482
+ if ((current.nodeValue || '').length > 0) {
483
+ textNodes.push(current)
484
+ }
485
+ current = walker.nextNode()
486
+ }
487
+ if (textNodes.length === 0) {
488
+ return null
489
+ }
490
+
491
+ const paragraphs = []
492
+ for (const { start, end } of ranges) {
493
+ const startPos = this.resolveTextOffset(textNodes, start)
494
+ const endPos = this.resolveTextOffset(textNodes, end)
495
+ if (!startPos?.node || !endPos?.node) {
496
+ continue
497
+ }
498
+
499
+ const range = doc.createRange()
500
+ range.setStart(startPos.node, startPos.offset)
501
+ range.setEnd(endPos.node, endPos.offset)
502
+
503
+ const fragment = range.cloneContents()
504
+ const p = doc.createElement('p')
505
+ p.appendChild(fragment)
506
+
507
+ if ((p.textContent || '').trim().length > 0) {
508
+ paragraphs.push(p)
509
+ }
510
+ }
511
+
512
+ if (paragraphs.length < 2) {
513
+ return null
514
+ }
515
+
516
+ body.innerHTML = ''
517
+ paragraphs.forEach((p) => body.appendChild(p))
518
+ return body.innerHTML
519
+ },
520
+ prepareHtmlForBlockTransform(html, locale) {
521
+ if (!html || typeof html !== 'string') {
522
+ throw new Error('invalid_html')
523
+ }
524
+
525
+ const sentenceCount = this.countTextSentences(html, locale)
526
+ if (sentenceCount < MIN_SENTENCE_COUNT_FOR_TEXT_TRANSFORM) {
527
+ throw new Error('insufficient_content')
528
+ }
529
+
530
+ // If the microservice will see enough top-level paragraphs, don't touch it.
531
+ if (this.countTopLevelParagraphs(html) >= 2) {
532
+ return html
533
+ }
534
+
535
+ // Unwrap a single container (common when editors wrap content)
536
+ if (typeof DOMParser !== 'undefined') {
537
+ try {
538
+ let currentHtml = html
539
+ for (let i = 0; i < 2; i++) {
540
+ if (this.countTopLevelParagraphs(currentHtml) >= 2) {
541
+ return currentHtml
542
+ }
543
+
544
+ const doc = new DOMParser().parseFromString(
545
+ currentHtml,
546
+ 'text/html'
547
+ )
548
+ const body = doc?.body
549
+ if (!body) {
550
+ break
551
+ }
552
+
553
+ const hasMeaningfulTextOutsideElements = Array.from(
554
+ body.childNodes
555
+ ).some(
556
+ (node) =>
557
+ node.nodeType === Node.TEXT_NODE &&
558
+ (node.textContent || '').trim()
559
+ )
560
+ const elementChildren = Array.from(body.children)
561
+
562
+ if (
563
+ elementChildren.length === 1 &&
564
+ !hasMeaningfulTextOutsideElements
565
+ ) {
566
+ const container = elementChildren[0]
567
+ const tag = (container.tagName || '').toLowerCase()
568
+ if (
569
+ tag === 'div' ||
570
+ tag === 'section' ||
571
+ tag === 'article'
572
+ ) {
573
+ currentHtml = container.innerHTML || ''
574
+ continue
575
+ }
576
+ }
577
+
578
+ break
579
+ }
580
+
581
+ const splitHtml = this.splitHtmlIntoParagraphs(
582
+ currentHtml,
583
+ locale
584
+ )
585
+ if (
586
+ splitHtml &&
587
+ this.countTopLevelParagraphs(splitHtml) >= 2
588
+ ) {
589
+ return splitHtml
590
+ }
591
+ } catch (_e) {
592
+ // Ignore and fall through to error
593
+ }
594
+ }
595
+
596
+ throw new Error('insufficient_content')
597
+ },
274
598
  countStructuredItems(block) {
275
599
  const items = _.get(block, 'metadata.config.items', [])
276
600
  return Array.isArray(items) ? items.length : 0
@@ -321,6 +645,7 @@ export default {
321
645
  const normalizedOrder = Number.isFinite(baseOrder)
322
646
  ? Math.trunc(baseOrder)
323
647
  : 0
648
+
324
649
  const newBlock = {
325
650
  id: _.uniqueId('create_content_block_'),
326
651
  content_id: null,
@@ -354,7 +679,9 @@ export default {
354
679
 
355
680
  const title = typeof config.title === 'string' ? config.title : ''
356
681
  const instructions =
357
- typeof config.instructions === 'string' ? config.instructions : ''
682
+ typeof config.instructions === 'string'
683
+ ? config.instructions
684
+ : ''
358
685
  const displayTitle = _.isBoolean(config.display_title)
359
686
  ? config.display_title
360
687
  : true
@@ -375,7 +702,9 @@ export default {
375
702
  typeof rawItem.content === 'string'
376
703
  ? rawItem.content
377
704
  : '',
378
- imageConfig: this.normalizeImageConfig(rawItem.fileConfig),
705
+ imageConfig: this.normalizeImageConfig(
706
+ rawItem.fileConfig
707
+ ),
379
708
  }
380
709
  }
381
710
 
@@ -389,19 +718,27 @@ export default {
389
718
  typeof rawItem.content === 'string'
390
719
  ? rawItem.content
391
720
  : '',
392
- imageConfig: this.normalizeImageConfig(rawItem.imageAsset),
721
+ imageConfig: this.normalizeImageConfig(
722
+ rawItem.imageAsset
723
+ ),
393
724
  }
394
725
  }
395
726
 
396
727
  if (kind === 'clickable_icons') {
397
- const fileConfig = this.normalizeImageConfig(rawItem.fileConfig)
728
+ const fileConfig = this.normalizeImageConfig(
729
+ rawItem.fileConfig
730
+ )
398
731
  const iconImage = rawItem.iconImage === true
399
732
 
400
733
  return {
401
734
  title:
402
- typeof rawItem.title === 'string' ? rawItem.title : '',
735
+ typeof rawItem.title === 'string'
736
+ ? rawItem.title
737
+ : '',
403
738
  bodyHtml:
404
- typeof rawItem.body === 'string' ? rawItem.body : '',
739
+ typeof rawItem.body === 'string'
740
+ ? rawItem.body
741
+ : '',
405
742
  imageConfig:
406
743
  iconImage || this.hasAssetInImageConfig(fileConfig)
407
744
  ? fileConfig
@@ -432,7 +769,8 @@ export default {
432
769
  }
433
770
 
434
771
  structured.items.forEach((item) => {
435
- const itemTitle = typeof item.title === 'string' ? item.title : ''
772
+ const itemTitle =
773
+ typeof item.title === 'string' ? item.title : ''
436
774
  const bodyHtml =
437
775
  typeof item.bodyHtml === 'string' ? item.bodyHtml : ''
438
776
 
@@ -618,15 +956,15 @@ export default {
618
956
  'windward.core.components.settings.clickable_icon.information'
619
957
  ),
620
958
  display: {
621
- show_title: false,
622
- show_background: false,
959
+ show_title: true,
960
+ show_background: true,
623
961
  round_icon: false,
624
962
  italic_icon: false,
625
963
  large_icon: false,
626
964
  autocolor: true,
627
965
  },
628
966
  items: items.map((item) => ({
629
- icon: '',
967
+ icon: 'mdi-star',
630
968
  fileConfig: {},
631
969
  iconImage: false,
632
970
  title: item.title || '',
@@ -642,14 +980,11 @@ export default {
642
980
  return newBlock
643
981
  },
644
982
  async generateMultiItemBlockFromText(targetType) {
645
- const html = _.get(this.value, 'body', '')
646
- if (!html || typeof html !== 'string') {
647
- throw new Error('invalid_html')
648
- }
983
+ const sourceBlock = this.sourceBlock
984
+ const html = _.get(sourceBlock, 'body', '')
985
+ const locale = this.$i18n?.locale
649
986
 
650
- if (this.countTextParagraphs(html) < 2) {
651
- throw new Error('insufficient_paragraphs')
652
- }
987
+ const preparedHtml = this.prepareHtmlForBlockTransform(html, locale)
653
988
 
654
989
  const organizationId = _.get(this.organization, 'id', null)
655
990
  const courseId = _.get(this.course, 'id', null)
@@ -671,9 +1006,9 @@ export default {
671
1006
  }
672
1007
 
673
1008
  const payload = {
674
- html,
1009
+ html: preparedHtml,
675
1010
  target_tag: targetType,
676
- language: this.$i18n.locale,
1011
+ language: locale,
677
1012
  }
678
1013
 
679
1014
  const requestConfig = request._reqConfig(
@@ -692,7 +1027,7 @@ export default {
692
1027
  throw new Error('invalid_response')
693
1028
  }
694
1029
 
695
- return this.buildTransformedBlock(this.value, data, targetType)
1030
+ return this.buildTransformedBlock(sourceBlock, data, targetType)
696
1031
  },
697
1032
  async performBlockTransform(e, targetType) {
698
1033
  e.stopPropagation()
@@ -701,7 +1036,9 @@ export default {
701
1036
  return
702
1037
  }
703
1038
 
704
- if (!this.value || !this.value.tag) {
1039
+ const sourceBlock = this.sourceBlock
1040
+
1041
+ if (!sourceBlock || !sourceBlock.tag) {
705
1042
  return
706
1043
  }
707
1044
 
@@ -717,19 +1054,16 @@ export default {
717
1054
  newBlock = await this.generateMultiItemBlockFromText(
718
1055
  targetType
719
1056
  )
1057
+ } else if (targetType === 'content-blocks-text') {
1058
+ newBlock = this.buildTextBlockFromStructured(sourceBlock)
720
1059
  } else {
721
- if (this.countStructuredItems(this.value) < 2) {
1060
+ if (this.countStructuredItems(sourceBlock) < 2) {
722
1061
  throw new Error('insufficient_items')
723
1062
  }
724
-
725
- if (targetType === 'content-blocks-text') {
726
- newBlock = this.buildTextBlockFromStructured(this.value)
727
- } else {
728
- newBlock = this.buildStructuredBlockFromStructured(
729
- this.value,
730
- targetType
731
- )
732
- }
1063
+ newBlock = this.buildStructuredBlockFromStructured(
1064
+ sourceBlock,
1065
+ targetType
1066
+ )
733
1067
  }
734
1068
 
735
1069
  if (!newBlock) {
@@ -746,8 +1080,17 @@ export default {
746
1080
  // eslint-disable-next-line no-console
747
1081
  console.error('Block transform failed', e)
748
1082
 
1083
+ const reason = _.get(
1084
+ e,
1085
+ 'response.data.error.details.reason',
1086
+ ''
1087
+ )
1088
+
749
1089
  if (this.$toast) {
750
- if (String(e?.message) === 'insufficient_paragraphs') {
1090
+ if (
1091
+ String(e?.message) === 'insufficient_content' ||
1092
+ reason === 'NOT_ENOUGH_PARAGRAPHS'
1093
+ ) {
751
1094
  this.$toast.error(
752
1095
  this.$t(
753
1096
  'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_insufficient_paragraphs'
@@ -85,6 +85,15 @@
85
85
  :value="value"
86
86
  />
87
87
  </v-form>
88
+ <v-alert v-if="launchError" type="error">
89
+ <p>
90
+ {{
91
+ $t(
92
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.unknown_error'
93
+ )
94
+ }}
95
+ </p>
96
+ </v-alert>
88
97
  <iframe
89
98
  v-if="
90
99
  block.metadata.config.launch_type === 'inline' &&
@@ -112,14 +121,22 @@
112
121
  </template>
113
122
  <template #form="{ on, attrs }">
114
123
  <iframe
115
- v-if="launched"
124
+ v-if="!launchError && launched"
116
125
  v-bind="attrs"
117
126
  :name="frameId"
118
127
  class="launch-frame"
119
128
  v-on="on"
120
129
  ></iframe>
121
-
122
130
  <div v-else></div>
131
+ <v-alert v-if="launchError" type="error">
132
+ <p>
133
+ {{
134
+ $t(
135
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.unknown_error'
136
+ )
137
+ }}
138
+ </p>
139
+ </v-alert>
123
140
  </template>
124
141
  </DialogBox>
125
142
  </div>
@@ -184,6 +201,7 @@ export default {
184
201
  launched: false,
185
202
  launchTimeout: {},
186
203
  openModal: false,
204
+ launchError: false,
187
205
  }
188
206
  },
189
207
  async fetch() {
@@ -288,6 +306,7 @@ export default {
288
306
  }
289
307
  },
290
308
  async onLaunch() {
309
+ this.launchError = false
291
310
  try {
292
311
  // Clear the launch data in-case we're in inline mode
293
312
  // If the iframe isn't destroyed subsequent launches will be new windows instead of the target iframe
@@ -330,6 +349,7 @@ export default {
330
349
  )
331
350
  }
332
351
  } catch (e) {
352
+ this.launchError = true
333
353
  // eslint-disable-next-line no-console
334
354
  console.error('LTI Link Launch Fail', e)
335
355
  }