@windward/integrations 0.19.1 → 0.21.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 (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/components/Content/Blocks/ActionPanel/TransformBlock.vue +776 -0
  3. package/components/LLM/GenerateContent/AssessmentQuestionGenerateButton.vue +180 -9
  4. package/components/LLM/GenerateContent/BlockQuestionGenerateButton.vue +35 -13
  5. package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +18 -12
  6. package/helpers/ExternalIntegration/ScormHelper.ts +1 -1
  7. package/i18n/en-US/components/content/blocks/action_panel/index.ts +5 -0
  8. package/i18n/en-US/components/content/blocks/action_panel/transform_block.ts +9 -0
  9. package/i18n/en-US/components/content/blocks/external_integration/lti_consumer.ts +2 -2
  10. package/i18n/en-US/components/content/blocks/index.ts +2 -0
  11. package/i18n/en-US/components/llm/generate_content/generate_questions.ts +4 -0
  12. package/i18n/en-US/components/llm/generate_content/index.ts +1 -0
  13. package/i18n/en-US/shared/settings.ts +1 -2
  14. package/i18n/es-ES/components/content/blocks/action_panel/index.ts +5 -0
  15. package/i18n/es-ES/components/content/blocks/action_panel/transform_block.ts +9 -0
  16. package/i18n/es-ES/components/content/blocks/external_integration/lti_consumer.ts +2 -2
  17. package/i18n/es-ES/components/content/blocks/index.ts +2 -0
  18. package/i18n/es-ES/components/llm/generate_content/generate_questions.ts +5 -0
  19. package/i18n/es-ES/components/llm/generate_content/index.ts +1 -0
  20. package/i18n/es-ES/shared/settings.ts +1 -2
  21. package/i18n/sv-SE/components/content/blocks/action_panel/index.ts +5 -0
  22. package/i18n/sv-SE/components/content/blocks/action_panel/transform_block.ts +9 -0
  23. package/i18n/sv-SE/components/content/blocks/external_integration/lti_consumer.ts +2 -2
  24. package/i18n/sv-SE/components/content/blocks/index.ts +2 -0
  25. package/i18n/sv-SE/components/llm/generate_content/generate_questions.ts +5 -0
  26. package/i18n/sv-SE/components/llm/generate_content/index.ts +1 -0
  27. package/i18n/sv-SE/shared/settings.ts +1 -2
  28. package/package.json +1 -1
  29. package/plugin.js +21 -5
@@ -0,0 +1,776 @@
1
+ <template>
2
+ <v-list-item-group v-if="canTransformBlock" :disabled="isTransforming">
3
+ <v-list-group v-model="showMenu" @click="onClickShowTransforms">
4
+ <template #activator>
5
+ <v-list-item-icon>
6
+ <v-progress-circular
7
+ v-if="isTransforming"
8
+ indeterminate
9
+ ></v-progress-circular>
10
+ <v-icon v-if="!isTransforming">mdi-shape</v-icon>
11
+ </v-list-item-icon>
12
+ <v-list-item-title>
13
+ {{
14
+ $t(
15
+ 'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type'
16
+ )
17
+ }}
18
+ </v-list-item-title>
19
+ </template>
20
+
21
+ <v-list-item
22
+ v-for="action in availableActions"
23
+ :key="action.tag"
24
+ :disabled="isTransforming"
25
+ @click="performBlockTransform($event, action.tag)"
26
+ >
27
+ <v-list-item-title>
28
+ <v-icon>mdi-arrow-right</v-icon> {{ action.label }}
29
+ </v-list-item-title>
30
+ </v-list-item>
31
+ </v-list-group>
32
+ </v-list-item-group>
33
+ </template>
34
+
35
+ <script>
36
+ import _ from 'lodash'
37
+ import { mapGetters } from 'vuex'
38
+ import Organization from '~/models/Organization'
39
+ import Course from '~/models/Course'
40
+
41
+ export default {
42
+ props: {
43
+ // The block associated with this action panel
44
+ value: { type: Object, required: false, default: null },
45
+
46
+ // The blocks position relative to other blocks (Potentially not the same as value.order)
47
+ index: { type: Number, required: false, default: 0 },
48
+ // The total number of blocks on the content page
49
+ length: { type: Number, required: false, default: 0 },
50
+
51
+ // The content block html element
52
+ bindElement: { type: null, required: false, default: null },
53
+
54
+ // Button enable / disable props
55
+ draggable: { type: Boolean, required: false, default: true },
56
+ savable: { type: Boolean, required: false, default: true },
57
+ editable: { type: Boolean, required: false, default: true },
58
+ deletable: { type: Boolean, required: false, default: true },
59
+ editMode: { type: Boolean, required: false, default: null },
60
+ saving: { type: Boolean, required: false, default: null },
61
+ btnGroupId: { type: String, required: false, default: '' },
62
+ },
63
+
64
+ data() {
65
+ return {
66
+ showMenu: false,
67
+ isTransforming: false,
68
+ }
69
+ },
70
+ computed: {
71
+ ...mapGetters({
72
+ organization: 'organization/get',
73
+ course: 'course/get',
74
+ }),
75
+ vModel: {
76
+ get() {
77
+ return this.value
78
+ },
79
+ set(value) {
80
+ this.$emit('input', value)
81
+ },
82
+ },
83
+ sourceKind() {
84
+ return this.getBlockKindFromTag(_.get(this.value, 'tag', ''))
85
+ },
86
+ textParagraphCount() {
87
+ if (this.sourceKind !== 'text') {
88
+ return 0
89
+ }
90
+ return this.countTextParagraphs(_.get(this.value, 'body', ''))
91
+ },
92
+ structuredItemCount() {
93
+ if (!['accordion', 'tab', 'clickable_icons'].includes(this.sourceKind)) {
94
+ return 0
95
+ }
96
+ return this.countStructuredItems(this.value)
97
+ },
98
+ availableActions() {
99
+ if (!this.value) {
100
+ return []
101
+ }
102
+
103
+ if (this.sourceKind === 'text') {
104
+ if (this.textParagraphCount < 2) {
105
+ return []
106
+ }
107
+ return [
108
+ {
109
+ tag: 'plugin-core-accordion',
110
+ label: this.$t(
111
+ 'windward.core.shared.content_blocks.title.accordion'
112
+ ),
113
+ },
114
+ {
115
+ tag: 'plugin-core-tab',
116
+ label: this.$t(
117
+ 'windward.core.shared.content_blocks.title.tab'
118
+ ),
119
+ },
120
+ {
121
+ tag: 'plugin-core-clickable-icons',
122
+ label: this.$t(
123
+ 'windward.core.shared.content_blocks.title.clickable_icons'
124
+ ),
125
+ },
126
+ ]
127
+ }
128
+
129
+ if (this.sourceKind === 'accordion') {
130
+ if (this.structuredItemCount < 2) {
131
+ return []
132
+ }
133
+ const actions = [
134
+ {
135
+ tag: 'content-blocks-text',
136
+ label: this.$t(
137
+ 'components.content.blocks.text.block_title'
138
+ ),
139
+ },
140
+ ]
141
+ if (this.structuredItemCount >= 2) {
142
+ actions.push(
143
+ {
144
+ tag: 'plugin-core-tab',
145
+ label: this.$t(
146
+ 'windward.core.shared.content_blocks.title.tab'
147
+ ),
148
+ },
149
+ {
150
+ tag: 'plugin-core-clickable-icons',
151
+ label: this.$t(
152
+ 'windward.core.shared.content_blocks.title.clickable_icons'
153
+ ),
154
+ }
155
+ )
156
+ }
157
+ return actions
158
+ }
159
+
160
+ if (this.sourceKind === 'tab') {
161
+ if (this.structuredItemCount < 2) {
162
+ return []
163
+ }
164
+ const actions = [
165
+ {
166
+ tag: 'content-blocks-text',
167
+ label: this.$t(
168
+ 'components.content.blocks.text.block_title'
169
+ ),
170
+ },
171
+ ]
172
+ if (this.structuredItemCount >= 2) {
173
+ actions.push(
174
+ {
175
+ tag: 'plugin-core-accordion',
176
+ label: this.$t(
177
+ 'windward.core.shared.content_blocks.title.accordion'
178
+ ),
179
+ },
180
+ {
181
+ tag: 'plugin-core-clickable-icons',
182
+ label: this.$t(
183
+ 'windward.core.shared.content_blocks.title.clickable_icons'
184
+ ),
185
+ }
186
+ )
187
+ }
188
+ return actions
189
+ }
190
+
191
+ if (this.sourceKind === 'clickable_icons') {
192
+ if (this.structuredItemCount < 2) {
193
+ return []
194
+ }
195
+ const actions = [
196
+ {
197
+ tag: 'content-blocks-text',
198
+ label: this.$t(
199
+ 'components.content.blocks.text.block_title'
200
+ ),
201
+ },
202
+ ]
203
+ if (this.structuredItemCount >= 2) {
204
+ actions.push(
205
+ {
206
+ tag: 'plugin-core-accordion',
207
+ label: this.$t(
208
+ 'windward.core.shared.content_blocks.title.accordion'
209
+ ),
210
+ },
211
+ {
212
+ tag: 'plugin-core-tab',
213
+ label: this.$t(
214
+ 'windward.core.shared.content_blocks.title.tab'
215
+ ),
216
+ }
217
+ )
218
+ }
219
+ return actions
220
+ }
221
+
222
+ return []
223
+ },
224
+ canTransformBlock() {
225
+ return this.availableActions.length > 0
226
+ },
227
+ },
228
+ methods: {
229
+ onClickShowTransforms(e) {
230
+ e.stopPropagation()
231
+ },
232
+ getBlockKindFromTag(tag) {
233
+ if (!tag || typeof tag !== 'string') {
234
+ return 'unknown'
235
+ }
236
+
237
+ if (tag === 'content-blocks-text') {
238
+ return 'text'
239
+ }
240
+
241
+ const normalized = tag.replace(/^plugin-/, '')
242
+ if (normalized === 'core-accordion') {
243
+ return 'accordion'
244
+ }
245
+ if (normalized === 'core-tab') {
246
+ return 'tab'
247
+ }
248
+ if (normalized === 'core-clickable-icons') {
249
+ return 'clickable_icons'
250
+ }
251
+
252
+ return 'unknown'
253
+ },
254
+ countTextParagraphs(html) {
255
+ if (!html || typeof html !== 'string') {
256
+ return 0
257
+ }
258
+
259
+ try {
260
+ 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
264
+ } 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
272
+ }
273
+ },
274
+ countStructuredItems(block) {
275
+ const items = _.get(block, 'metadata.config.items', [])
276
+ return Array.isArray(items) ? items.length : 0
277
+ },
278
+ escapeHtml(text) {
279
+ return String(text == null ? '' : text)
280
+ .replace(/&/g, '&amp;')
281
+ .replace(/</g, '&lt;')
282
+ .replace(/>/g, '&gt;')
283
+ .replace(/"/g, '&quot;')
284
+ .replace(/'/g, '&#39;')
285
+ },
286
+ normalizeAssetReference(asset) {
287
+ if (!asset) {
288
+ return null
289
+ }
290
+
291
+ if (typeof asset === 'string') {
292
+ return { file_asset_id: asset }
293
+ }
294
+
295
+ if (typeof asset === 'object') {
296
+ return asset
297
+ }
298
+
299
+ return null
300
+ },
301
+ normalizeImageConfig(rawConfig) {
302
+ if (!rawConfig || typeof rawConfig !== 'object') {
303
+ return null
304
+ }
305
+
306
+ const config = _.cloneDeep(rawConfig)
307
+ if (typeof config.asset === 'string') {
308
+ config.asset = this.normalizeAssetReference(config.asset)
309
+ } else if (config.asset && typeof config.asset === 'object') {
310
+ config.asset = this.normalizeAssetReference(config.asset)
311
+ }
312
+
313
+ return config
314
+ },
315
+ hasAssetInImageConfig(imageConfig) {
316
+ return !!_.get(imageConfig, 'asset.file_asset_id', null)
317
+ },
318
+ buildNewBlockBase(sourceBlock) {
319
+ const baseOrderRaw = _.get(sourceBlock, 'order', 0)
320
+ const baseOrder = Number(baseOrderRaw)
321
+ const normalizedOrder = Number.isFinite(baseOrder)
322
+ ? Math.trunc(baseOrder)
323
+ : 0
324
+ const newBlock = {
325
+ id: _.uniqueId('create_content_block_'),
326
+ content_id: null,
327
+ tag: '',
328
+ body: '',
329
+ status: 'draft',
330
+ order: normalizedOrder,
331
+ type: {
332
+ save_state: false,
333
+ trackable: false,
334
+ completable: false,
335
+ },
336
+ assets: _.cloneDeep(_.get(sourceBlock, 'assets', [])),
337
+ metadata: {
338
+ config: {},
339
+ },
340
+ }
341
+
342
+ const display = _.cloneDeep(
343
+ _.get(sourceBlock, 'metadata.display', null)
344
+ )
345
+ if (display) {
346
+ newBlock.metadata.display = display
347
+ }
348
+
349
+ return newBlock
350
+ },
351
+ extractStructuredBlockData(sourceBlock) {
352
+ const kind = this.getBlockKindFromTag(_.get(sourceBlock, 'tag', ''))
353
+ const config = _.get(sourceBlock, 'metadata.config', {}) || {}
354
+
355
+ const title = typeof config.title === 'string' ? config.title : ''
356
+ const instructions =
357
+ typeof config.instructions === 'string' ? config.instructions : ''
358
+ const displayTitle = _.isBoolean(config.display_title)
359
+ ? config.display_title
360
+ : true
361
+
362
+ const rawItems = Array.isArray(config.items) ? config.items : []
363
+ const items = rawItems.map((rawItem) => {
364
+ if (!rawItem || typeof rawItem !== 'object') {
365
+ return { title: '', bodyHtml: '', imageConfig: null }
366
+ }
367
+
368
+ if (kind === 'accordion') {
369
+ return {
370
+ title:
371
+ typeof rawItem.header === 'string'
372
+ ? rawItem.header
373
+ : '',
374
+ bodyHtml:
375
+ typeof rawItem.content === 'string'
376
+ ? rawItem.content
377
+ : '',
378
+ imageConfig: this.normalizeImageConfig(rawItem.fileConfig),
379
+ }
380
+ }
381
+
382
+ if (kind === 'tab') {
383
+ return {
384
+ title:
385
+ typeof rawItem.tabHeader === 'string'
386
+ ? rawItem.tabHeader
387
+ : '',
388
+ bodyHtml:
389
+ typeof rawItem.content === 'string'
390
+ ? rawItem.content
391
+ : '',
392
+ imageConfig: this.normalizeImageConfig(rawItem.imageAsset),
393
+ }
394
+ }
395
+
396
+ if (kind === 'clickable_icons') {
397
+ const fileConfig = this.normalizeImageConfig(rawItem.fileConfig)
398
+ const iconImage = rawItem.iconImage === true
399
+
400
+ return {
401
+ title:
402
+ typeof rawItem.title === 'string' ? rawItem.title : '',
403
+ bodyHtml:
404
+ typeof rawItem.body === 'string' ? rawItem.body : '',
405
+ imageConfig:
406
+ iconImage || this.hasAssetInImageConfig(fileConfig)
407
+ ? fileConfig
408
+ : null,
409
+ }
410
+ }
411
+
412
+ return { title: '', bodyHtml: '', imageConfig: null }
413
+ })
414
+
415
+ return {
416
+ kind,
417
+ title,
418
+ instructions,
419
+ displayTitle,
420
+ items,
421
+ }
422
+ },
423
+ buildTextBlockFromStructured(sourceBlock) {
424
+ const structured = this.extractStructuredBlockData(sourceBlock)
425
+ const parts = []
426
+
427
+ if (structured.displayTitle && structured.title) {
428
+ parts.push(`<h2>${this.escapeHtml(structured.title)}</h2>`)
429
+ }
430
+ if (structured.instructions) {
431
+ parts.push(`<p>${this.escapeHtml(structured.instructions)}</p>`)
432
+ }
433
+
434
+ structured.items.forEach((item) => {
435
+ const itemTitle = typeof item.title === 'string' ? item.title : ''
436
+ const bodyHtml =
437
+ typeof item.bodyHtml === 'string' ? item.bodyHtml : ''
438
+
439
+ if (itemTitle) {
440
+ parts.push(`<h3>${this.escapeHtml(itemTitle)}</h3>`)
441
+ }
442
+ if (bodyHtml) {
443
+ parts.push(bodyHtml)
444
+ }
445
+ })
446
+
447
+ const newBlock = this.buildNewBlockBase(sourceBlock)
448
+ newBlock.tag = 'content-blocks-text'
449
+ newBlock.body = parts.join('')
450
+ newBlock.metadata.config = { expand: false }
451
+ return newBlock
452
+ },
453
+ buildStructuredBlockFromStructured(sourceBlock, targetType) {
454
+ const structured = this.extractStructuredBlockData(sourceBlock)
455
+ const newBlock = this.buildNewBlockBase(sourceBlock)
456
+
457
+ if (targetType === 'plugin-core-accordion') {
458
+ newBlock.tag = 'plugin-core-accordion'
459
+ newBlock.body = this.$t(
460
+ 'windward.core.shared.content_blocks.title.accordion'
461
+ )
462
+ newBlock.metadata.config = {
463
+ title: structured.title,
464
+ display_title: structured.displayTitle,
465
+ instructions: structured.instructions,
466
+ items: structured.items.map((item) => ({
467
+ header: item.title || '',
468
+ expand: false,
469
+ content: item.bodyHtml || '',
470
+ fileConfig: item.imageConfig || {
471
+ display: {
472
+ width: 100,
473
+ margin: '',
474
+ padding: '',
475
+ },
476
+ hideBackground: true,
477
+ },
478
+ })),
479
+ }
480
+ return newBlock
481
+ }
482
+
483
+ if (targetType === 'plugin-core-tab') {
484
+ newBlock.tag = 'plugin-core-tab'
485
+ newBlock.body = this.$t(
486
+ 'windward.core.shared.content_blocks.title.tab'
487
+ )
488
+ newBlock.metadata.config = {
489
+ title: structured.title,
490
+ display_title: structured.displayTitle,
491
+ instructions: structured.instructions,
492
+ currentTab: 0,
493
+ items: structured.items.map((item) => ({
494
+ tabHeader: item.title || '',
495
+ expand: false,
496
+ content: item.bodyHtml || '',
497
+ imageAsset: item.imageConfig || {
498
+ display: {
499
+ width: 100,
500
+ margin: '',
501
+ padding: '',
502
+ },
503
+ hideBackground: true,
504
+ },
505
+ })),
506
+ }
507
+ return newBlock
508
+ }
509
+
510
+ if (targetType === 'plugin-core-clickable-icons') {
511
+ newBlock.tag = 'plugin-core-clickable-icons'
512
+ newBlock.body = this.$t(
513
+ 'windward.core.shared.content_blocks.title.clickable_icons'
514
+ )
515
+ newBlock.metadata.config = {
516
+ title: structured.title,
517
+ display_title: structured.displayTitle,
518
+ instructions: structured.instructions,
519
+ description: this.$t(
520
+ 'windward.core.components.settings.clickable_icon.information'
521
+ ),
522
+ display: {
523
+ show_title: false,
524
+ show_background: false,
525
+ round_icon: false,
526
+ italic_icon: false,
527
+ large_icon: false,
528
+ autocolor: true,
529
+ },
530
+ items: structured.items.map((item) => {
531
+ const imageConfig = item.imageConfig
532
+ const hasAsset = this.hasAssetInImageConfig(imageConfig)
533
+
534
+ return {
535
+ icon: '',
536
+ fileConfig: hasAsset ? imageConfig : {},
537
+ iconImage: hasAsset,
538
+ title: item.title || '',
539
+ body: item.bodyHtml || '',
540
+ color: {
541
+ class: '',
542
+ },
543
+ active: false,
544
+ }
545
+ }),
546
+ }
547
+ return newBlock
548
+ }
549
+
550
+ throw new Error(`Unsupported targetType: ${targetType}`)
551
+ },
552
+ buildTransformedBlock(sourceBlock, data, targetType) {
553
+ const newBlock = this.buildNewBlockBase(sourceBlock)
554
+
555
+ const blockTitle = data.block_title || ''
556
+ const items = Array.isArray(data.items) ? data.items : []
557
+
558
+ if (targetType === 'plugin-core-accordion') {
559
+ newBlock.tag = 'plugin-core-accordion'
560
+ newBlock.body = this.$t(
561
+ 'windward.core.shared.content_blocks.title.accordion'
562
+ )
563
+ newBlock.metadata.config = {
564
+ title: blockTitle,
565
+ display_title: true,
566
+ instructions: this.$t(
567
+ 'windward.core.components.settings.accordion.instructions'
568
+ ),
569
+ items: items.map((item) => ({
570
+ header: item.title || '',
571
+ expand: false,
572
+ content: item.body_html || '',
573
+ fileConfig: {
574
+ display: {
575
+ width: 100,
576
+ margin: '',
577
+ padding: '',
578
+ },
579
+ hideBackground: true,
580
+ },
581
+ })),
582
+ }
583
+ } else if (targetType === 'plugin-core-tab') {
584
+ newBlock.tag = 'plugin-core-tab'
585
+ newBlock.body = this.$t(
586
+ 'windward.core.shared.content_blocks.title.tab'
587
+ )
588
+ newBlock.metadata.config = {
589
+ title: blockTitle,
590
+ display_title: true,
591
+ instructions: this.$t(
592
+ 'windward.core.components.settings.tab.instructions'
593
+ ),
594
+ currentTab: 0,
595
+ items: items.map((item) => ({
596
+ tabHeader: item.title || '',
597
+ expand: false,
598
+ content: item.body_html || '',
599
+ imageAsset: {
600
+ display: {
601
+ width: 100,
602
+ margin: '',
603
+ padding: '',
604
+ },
605
+ hideBackground: true,
606
+ },
607
+ })),
608
+ }
609
+ } else if (targetType === 'plugin-core-clickable-icons') {
610
+ newBlock.tag = 'plugin-core-clickable-icons'
611
+ newBlock.body = this.$t(
612
+ 'windward.core.shared.content_blocks.title.clickable_icons'
613
+ )
614
+ newBlock.metadata.config = {
615
+ title: blockTitle,
616
+ display_title: true,
617
+ description: this.$t(
618
+ 'windward.core.components.settings.clickable_icon.information'
619
+ ),
620
+ display: {
621
+ show_title: false,
622
+ show_background: false,
623
+ round_icon: false,
624
+ italic_icon: false,
625
+ large_icon: false,
626
+ autocolor: true,
627
+ },
628
+ items: items.map((item) => ({
629
+ icon: '',
630
+ fileConfig: {},
631
+ iconImage: false,
632
+ title: item.title || '',
633
+ body: item.body_html || '',
634
+ color: {
635
+ class: '',
636
+ },
637
+ active: false,
638
+ })),
639
+ }
640
+ }
641
+
642
+ return newBlock
643
+ },
644
+ async generateMultiItemBlockFromText(targetType) {
645
+ const html = _.get(this.value, 'body', '')
646
+ if (!html || typeof html !== 'string') {
647
+ throw new Error('invalid_html')
648
+ }
649
+
650
+ if (this.countTextParagraphs(html) < 2) {
651
+ throw new Error('insufficient_paragraphs')
652
+ }
653
+
654
+ const organizationId = _.get(this.organization, 'id', null)
655
+ const courseId = _.get(this.course, 'id', null)
656
+
657
+ if (!organizationId || !courseId) {
658
+ throw new Error('missing_context')
659
+ }
660
+
661
+ const request = new Course()
662
+ request.custom(
663
+ new Organization({ id: organizationId }),
664
+ new Course({ id: courseId }),
665
+ 'llm-block-transform'
666
+ )
667
+
668
+ const resourcePath = request._customResource
669
+ if (!resourcePath) {
670
+ throw new Error('missing_resource')
671
+ }
672
+
673
+ const payload = {
674
+ html,
675
+ target_tag: targetType,
676
+ language: this.$i18n.locale,
677
+ }
678
+
679
+ const requestConfig = request._reqConfig(
680
+ {
681
+ method: 'POST',
682
+ url: `${request.baseURL()}/${resourcePath}`,
683
+ data: payload,
684
+ },
685
+ { forceMethod: true }
686
+ )
687
+
688
+ const response = await request.request(requestConfig)
689
+ const data = response?.data || response
690
+
691
+ if (!data || !Array.isArray(data.items) || data.items.length < 2) {
692
+ throw new Error('invalid_response')
693
+ }
694
+
695
+ return this.buildTransformedBlock(this.value, data, targetType)
696
+ },
697
+ async performBlockTransform(e, targetType) {
698
+ e.stopPropagation()
699
+
700
+ if (this.isTransforming) {
701
+ return
702
+ }
703
+
704
+ if (!this.value || !this.value.tag) {
705
+ return
706
+ }
707
+
708
+ this.isTransforming = true
709
+
710
+ try {
711
+ let newBlock = null
712
+
713
+ if (this.sourceKind === 'text') {
714
+ if (targetType === 'content-blocks-text') {
715
+ return
716
+ }
717
+ newBlock = await this.generateMultiItemBlockFromText(
718
+ targetType
719
+ )
720
+ } else {
721
+ if (this.countStructuredItems(this.value) < 2) {
722
+ throw new Error('insufficient_items')
723
+ }
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
+ }
733
+ }
734
+
735
+ if (!newBlock) {
736
+ throw new Error('invalid_new_block')
737
+ }
738
+
739
+ this.$eb.$emit('create:content-block', newBlock)
740
+
741
+ const activeBlock = _.cloneDeep(newBlock)
742
+ this.$ContentService.setContentBlock(activeBlock)
743
+ this.$eb.$emit('block:focus', activeBlock)
744
+ } catch (e) {
745
+ // Surface errors instead of failing silently
746
+ // eslint-disable-next-line no-console
747
+ console.error('Block transform failed', e)
748
+
749
+ if (this.$toast) {
750
+ if (String(e?.message) === 'insufficient_paragraphs') {
751
+ this.$toast.error(
752
+ this.$t(
753
+ 'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_insufficient_paragraphs'
754
+ )
755
+ )
756
+ } else if (String(e?.message) === 'insufficient_items') {
757
+ this.$toast.error(
758
+ this.$t(
759
+ 'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_insufficient_items'
760
+ )
761
+ )
762
+ } else {
763
+ this.$toast.error(
764
+ this.$t(
765
+ 'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_error'
766
+ )
767
+ )
768
+ }
769
+ }
770
+ } finally {
771
+ this.isTransforming = false
772
+ }
773
+ },
774
+ },
775
+ }
776
+ </script>