@blokkli/editor 2.0.0-alpha.49 → 2.0.0-alpha.50

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 (36) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +2 -1
  3. package/dist/modules/agent/runtime/app/features/agent/types.d.ts +0 -4
  4. package/dist/modules/agent/runtime/app/tools/add_paragraphs/index.js +29 -10
  5. package/dist/modules/agent/runtime/app/tools/get_bundle_info/index.js +7 -2
  6. package/dist/modules/agent/runtime/app/tools/get_paragraph_context/index.js +1 -1
  7. package/dist/modules/agent/runtime/app/tools/get_paragraph_options/index.js +1 -1
  8. package/dist/modules/agent/runtime/app/tools/set_paragraph_options/index.js +1 -1
  9. package/dist/modules/agent/runtime/app/tools/swap_paragraphs/index.js +9 -48
  10. package/dist/modules/charts/runtime/blokkli/tools/chart_schemas.js +1 -1
  11. package/dist/modules/charts/runtime/blokkli/tools/get_chart_data/index.js +1 -1
  12. package/dist/modules/charts/runtime/blokkli/tools/update_chart/index.js +1 -1
  13. package/dist/runtime/editor/components/EditProvider.vue +2 -1
  14. package/dist/runtime/editor/components/ItemIcon/index.vue +1 -1
  15. package/dist/runtime/editor/composables/useEditableFieldOverride.js +7 -2
  16. package/dist/runtime/editor/features/changelog/changelog.json +9 -1
  17. package/dist/runtime/editor/features/dragging-overlay/index.vue +2 -1
  18. package/dist/runtime/editor/features/edit/index.vue +35 -19
  19. package/dist/runtime/editor/features/swap/index.d.vue.ts +3 -0
  20. package/dist/runtime/editor/features/swap/index.vue +57 -0
  21. package/dist/runtime/editor/features/swap/index.vue.d.ts +3 -0
  22. package/dist/runtime/editor/features/swap/types.d.ts +8 -0
  23. package/dist/runtime/editor/features/swap/types.js +0 -0
  24. package/dist/runtime/editor/helpers/swap.d.ts +13 -0
  25. package/dist/runtime/editor/helpers/swap.js +31 -0
  26. package/dist/runtime/editor/plugins/ItemAction/index.d.vue.ts +6 -0
  27. package/dist/runtime/editor/plugins/ItemAction/index.vue +2 -0
  28. package/dist/runtime/editor/plugins/ItemAction/index.vue.d.ts +6 -0
  29. package/dist/runtime/editor/providers/definition.d.ts +1 -1
  30. package/dist/runtime/editor/providers/fieldValue.d.ts +2 -1
  31. package/dist/runtime/editor/providers/fieldValue.js +7 -2
  32. package/dist/runtime/editor/translations/de.json +21 -1
  33. package/dist/runtime/editor/translations/fr.json +21 -1
  34. package/dist/runtime/editor/translations/gsw_CH.json +21 -1
  35. package/dist/runtime/editor/translations/it.json +21 -1
  36. package/package.json +12 -12
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blokkli/editor",
3
3
  "configKey": "blokkli",
4
- "version": "2.0.0-alpha.49",
4
+ "version": "2.0.0-alpha.50",
5
5
  "compatibility": {
6
6
  "nuxt": ">=3.15.0"
7
7
  },
package/dist/module.mjs CHANGED
@@ -18,7 +18,7 @@ import 'typescript';
18
18
  import 'oxc-walker';
19
19
 
20
20
  const name = "@blokkli/editor";
21
- const version = "2.0.0-alpha.49";
21
+ const version = "2.0.0-alpha.50";
22
22
 
23
23
  function validateOption(optionKey, option, icons) {
24
24
  const errors = [];
@@ -1554,6 +1554,7 @@ const USED_MATERIAL_ICONS = [
1554
1554
  "bk_mdi_stream",
1555
1555
  "bk_mdi_swap_horiz",
1556
1556
  "bk_mdi_swap_horizontal_circle",
1557
+ "bk_mdi_swap_vert",
1557
1558
  "bk_mdi_text_compare",
1558
1559
  "bk_mdi_text_select_end",
1559
1560
  "bk_mdi_texture",
@@ -18,10 +18,6 @@ declare module '#blokkli/editor/types/permissions' {
18
18
  }
19
19
  declare module '#blokkli/editor/adapter' {
20
20
  interface BlokkliAdapter<T> {
21
- /**
22
- * Swap two blocks.
23
- */
24
- swapBlocks?: (first: string, second: string) => Promise<MutationResponseLike<T>>;
25
21
  /**
26
22
  * Rearrange blocks within a field by specifying the desired order.
27
23
  *
@@ -78,10 +78,14 @@ function validateContentFields(ctx, block, path) {
78
78
  }
79
79
  return void 0;
80
80
  }
81
- function validateBlockOptions(ctx, block, path) {
81
+ function validateBlockOptions(ctx, block, path, parentBundle) {
82
82
  if (!block.options) return void 0;
83
83
  const { definitions } = ctx.app;
84
- const definition = definitions.getBlockDefinition(block.bundle, "default");
84
+ const definition = definitions.getBlockDefinition(
85
+ block.bundle,
86
+ "default",
87
+ parentBundle
88
+ );
85
89
  if (!definition) {
86
90
  return `${path}: Paragraph definition not found for bundle "${block.bundle}".`;
87
91
  }
@@ -103,7 +107,7 @@ function validateBlockOptions(ctx, block, path) {
103
107
  }
104
108
  return void 0;
105
109
  }
106
- function validateBlockTree(ctx, blocks, allowedBundles, fieldLabel, pathPrefix) {
110
+ function validateBlockTree(ctx, blocks, allowedBundles, fieldLabel, pathPrefix, parentBundle) {
107
111
  const { types } = ctx.app;
108
112
  for (let i = 0; i < blocks.length; i++) {
109
113
  const block = blocks[i];
@@ -120,7 +124,7 @@ function validateBlockTree(ctx, blocks, allowedBundles, fieldLabel, pathPrefix)
120
124
  }
121
125
  const contentError = validateContentFields(ctx, block, path);
122
126
  if (contentError) return contentError;
123
- const optionsError = validateBlockOptions(ctx, block, path);
127
+ const optionsError = validateBlockOptions(ctx, block, path, parentBundle);
124
128
  if (optionsError) return optionsError;
125
129
  if (block.children) {
126
130
  for (const [childFieldName, childBlocks] of Object.entries(
@@ -141,7 +145,8 @@ function validateBlockTree(ctx, blocks, allowedBundles, fieldLabel, pathPrefix)
141
145
  childBlocks,
142
146
  childFieldConfig.allowedBundles,
143
147
  childFieldName,
144
- `${path} > ${childFieldName} > `
148
+ `${path} > ${childFieldName} > `,
149
+ block.bundle
145
150
  );
146
151
  if (childError) return childError;
147
152
  }
@@ -149,7 +154,7 @@ function validateBlockTree(ctx, blocks, allowedBundles, fieldLabel, pathPrefix)
149
154
  }
150
155
  return void 0;
151
156
  }
152
- function buildEventBlocks(ctx, blocks) {
157
+ function buildEventBlocks(ctx, blocks, parentBundle) {
153
158
  const { definitions } = ctx.app;
154
159
  return blocks.map((block) => {
155
160
  const blockUuid = generateUUID();
@@ -159,7 +164,11 @@ function buildEventBlocks(ctx, blocks) {
159
164
  })) : void 0;
160
165
  let options;
161
166
  if (block.options) {
162
- const definition = definitions.getBlockDefinition(block.bundle, "default");
167
+ const definition = definitions.getBlockDefinition(
168
+ block.bundle,
169
+ "default",
170
+ parentBundle
171
+ );
163
172
  if (definition) {
164
173
  const availableOptions = getAvailableOptions(
165
174
  definition.options,
@@ -180,7 +189,11 @@ function buildEventBlocks(ctx, blocks) {
180
189
  children = {};
181
190
  for (const [fieldName, childBlocks] of Object.entries(block.children)) {
182
191
  if (childBlocks.length) {
183
- children[fieldName] = buildEventBlocks(ctx, childBlocks);
192
+ children[fieldName] = buildEventBlocks(
193
+ ctx,
194
+ childBlocks,
195
+ block.bundle
196
+ );
184
197
  }
185
198
  }
186
199
  }
@@ -253,17 +266,23 @@ export default defineBlokkliAgentTool({
253
266
  };
254
267
  }
255
268
  }
269
+ const topLevelParentBundle = isRootEntity ? null : bundle;
256
270
  const validationError = validateBlockTree(
257
271
  ctx,
258
272
  params.paragraphs,
259
273
  field.allowedBundles,
260
274
  params.parent.field,
261
- ""
275
+ "",
276
+ topLevelParentBundle
262
277
  );
263
278
  if (validationError) {
264
279
  return { error: validationError };
265
280
  }
266
- const eventBlocks = buildEventBlocks(ctx, params.paragraphs);
281
+ const eventBlocks = buildEventBlocks(
282
+ ctx,
283
+ params.paragraphs,
284
+ topLevelParentBundle
285
+ );
267
286
  const totalCount = countBlocks(eventBlocks);
268
287
  const blockUuids = collectAllUuids(eventBlocks);
269
288
  const { $t } = ctx.app;
@@ -58,7 +58,7 @@ export default defineBlokkliAgentTool({
58
58
  paramsSchema,
59
59
  resultSchema,
60
60
  execute(ctx, params) {
61
- const { fields, types, state, definitions, $t } = ctx.app;
61
+ const { fields, types, state, definitions, $t, context, blocks } = ctx.app;
62
62
  const label = $t(
63
63
  "aiAgentGetBundleInfoDone",
64
64
  "Got bundle info for @field"
@@ -79,6 +79,7 @@ export default defineBlokkliAgentTool({
79
79
  const currentCount = state.getFieldBlockCount(fieldKey);
80
80
  const allowedBundles = params.bundles ? field.allowedBundles.filter((b) => params.bundles.includes(b)) : field.allowedBundles;
81
81
  const includeOptions = allowedBundles.length < 3;
82
+ const parentBundle = params.parentUuid === context.value.entityUuid ? null : blocks.getBlock(params.parentUuid)?.bundle ?? null;
82
83
  const bundles = allowedBundles.map((bundle) => {
83
84
  const bundleDefinition = types.getBlockBundleDefinition(bundle);
84
85
  const contentFields = {};
@@ -106,7 +107,11 @@ export default defineBlokkliAgentTool({
106
107
  }
107
108
  let options;
108
109
  if (includeOptions) {
109
- const definition = definitions.getBlockDefinition(bundle, "default");
110
+ const definition = definitions.getBlockDefinition(
111
+ bundle,
112
+ "default",
113
+ parentBundle
114
+ );
110
115
  if (definition) {
111
116
  const availableOptions = getAvailableOptions(
112
117
  definition.options,
@@ -241,7 +241,7 @@ export default defineBlokkliAgentTool({
241
241
  const definition = definitions.getBlockDefinition(
242
242
  block.bundle,
243
243
  selectionItem?.fieldListType ?? "default",
244
- selectionItem?.parentBlockBundle
244
+ selectionItem?.parentBlockBundle ?? null
245
245
  );
246
246
  if (definition) {
247
247
  const availableOptions = getAvailableOptions(
@@ -35,7 +35,7 @@ export default defineBlokkliAgentTool({
35
35
  const definition = definitions.getBlockDefinition(
36
36
  bundle,
37
37
  selectionItem?.fieldListType ?? "default",
38
- selectionItem?.parentBlockBundle
38
+ selectionItem?.parentBlockBundle ?? null
39
39
  );
40
40
  if (!definition) continue;
41
41
  const availableOptions = getAvailableOptions(
@@ -60,7 +60,7 @@ export default defineBlokkliAgentTool({
60
60
  const definition = definitions.getBlockDefinition(
61
61
  bundle,
62
62
  selectionItem?.fieldListType ?? "default",
63
- selectionItem?.parentBlockBundle
63
+ selectionItem?.parentBlockBundle ?? null
64
64
  );
65
65
  if (!definition) {
66
66
  return {
@@ -1,10 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { defineBlokkliAgentTool } from "#blokkli/agent/app/composables";
3
3
  import { mutationResultSchema } from "../schemas.js";
4
- import {
5
- requireBundlePermission,
6
- requireNoRestrictedAncestor
7
- } from "../../helpers/validation.js";
4
+ import { getSwapDisabledReason } from "#blokkli/editor/helpers/swap";
8
5
  const paramsSchema = z.object({
9
6
  uuidA: z.string().describe("First paragraph UUID"),
10
7
  uuidB: z.string().describe("Second paragraph UUID")
@@ -23,10 +20,7 @@ export default defineBlokkliAgentTool({
23
20
  resultSchema: mutationResultSchema,
24
21
  requiredAdapterMethods: ["swapBlocks"],
25
22
  execute(ctx, params) {
26
- const { blocks, types, $t } = ctx.app;
27
- if (params.uuidA === params.uuidB) {
28
- return { error: "Both UUIDs refer to the same paragraph" };
29
- }
23
+ const { blocks, types, $t, permissions } = ctx.app;
30
24
  const blockA = blocks.getBlock(params.uuidA);
31
25
  if (!blockA) {
32
26
  return { error: `Paragraph not found: ${params.uuidA}` };
@@ -35,46 +29,13 @@ export default defineBlokkliAgentTool({
35
29
  if (!blockB) {
36
30
  return { error: `Paragraph not found: ${params.uuidB}` };
37
31
  }
38
- const denied = requireBundlePermission(
39
- ctx.app,
40
- [blockA.bundle, blockB.bundle],
41
- "edit"
42
- );
43
- if (denied) return denied;
44
- const ancestorDenied = requireNoRestrictedAncestor(ctx.app, [
45
- params.uuidA,
46
- params.uuidB
47
- ]);
48
- if (ancestorDenied) return ancestorDenied;
49
- const fieldConfigA = types.getFieldConfig(
50
- blockA.host.type,
51
- blockA.host.bundle,
52
- blockA.host.fieldName
53
- );
54
- if (!fieldConfigA) {
55
- return {
56
- error: `Could not find field config for paragraph A's parent field`
57
- };
58
- }
59
- const fieldConfigB = types.getFieldConfig(
60
- blockB.host.type,
61
- blockB.host.bundle,
62
- blockB.host.fieldName
63
- );
64
- if (!fieldConfigB) {
65
- return {
66
- error: `Could not find field config for paragraph B's parent field`
67
- };
68
- }
69
- if (!fieldConfigB.allowedBundles.includes(blockA.bundle)) {
70
- return {
71
- error: `Cannot swap: bundle "${blockA.bundle}" (uuid: ${params.uuidA}) is not allowed in field "${blockB.host.fieldName}" (parent: ${blockB.host.uuid}). Allowed bundles: ${fieldConfigB.allowedBundles.join(", ")}`
72
- };
73
- }
74
- if (!fieldConfigA.allowedBundles.includes(blockB.bundle)) {
75
- return {
76
- error: `Cannot swap: bundle "${blockB.bundle}" (uuid: ${params.uuidB}) is not allowed in field "${blockA.host.fieldName}" (parent: ${blockA.host.uuid}). Allowed bundles: ${fieldConfigA.allowedBundles.join(", ")}`
77
- };
32
+ const reason = getSwapDisabledReason(blockA, blockB, {
33
+ getFieldConfig: types.getFieldConfig,
34
+ checkBlockBundlePermission: permissions.checkBlockBundlePermission,
35
+ blockHasRestrictedAncestor: permissions.blockHasRestrictedAncestor
36
+ });
37
+ if (reason) {
38
+ return { error: reason };
78
39
  }
79
40
  const labelA = types.getBlockLabel(blockA.bundle);
80
41
  const labelB = types.getBlockLabel(blockB.bundle);
@@ -105,7 +105,7 @@ export function findChartOptionKey(ctx, uuid) {
105
105
  const definition = definitions.getBlockDefinition(
106
106
  bundle,
107
107
  selectionItem?.fieldListType ?? "default",
108
- selectionItem?.parentBlockBundle
108
+ selectionItem?.parentBlockBundle ?? null
109
109
  );
110
110
  if (!definition?.options) {
111
111
  return { error: `Paragraph "${uuid}" (${bundle}) has no chart option.` };
@@ -38,7 +38,7 @@ export default defineBlokkliAgentTool({
38
38
  const chartOption = findChartOptionKey(ctx, params.uuid);
39
39
  if ("error" in chartOption) return chartOption;
40
40
  const item = state.getFieldListItem(params.uuid);
41
- const rawData = item?.options?.[chartOption.key];
41
+ const rawData = state.mutatedOptions[params.uuid]?.[chartOption.key] || item?.options?.[chartOption.key];
42
42
  let data;
43
43
  if (rawData) {
44
44
  try {
@@ -47,7 +47,7 @@ export default defineBlokkliAgentTool({
47
47
  if ("error" in chartOption) return chartOption;
48
48
  let current;
49
49
  const item = state.getFieldListItem(params.uuid);
50
- const rawData = item?.options?.[chartOption.key];
50
+ const rawData = state.mutatedOptions[params.uuid]?.[chartOption.key] || item?.options?.[chartOption.key];
51
51
  if (rawData) {
52
52
  try {
53
53
  current = JSON.parse(rawData);
@@ -223,7 +223,8 @@ const fieldValue = fieldValueProviderFn(
223
223
  directive,
224
224
  state,
225
225
  types,
226
- definitions
226
+ definitions,
227
+ blocks
227
228
  );
228
229
  const readability = readabilityProviderFn(
229
230
  adapters,
@@ -24,7 +24,7 @@ const iconName = computed(() => {
24
24
  if (props.bundle === fromLibraryBlockBundle) {
25
25
  return "reusable";
26
26
  } else if (props.bundle) {
27
- const name = definitions.getBlockDefinition(props.bundle, "default")?.editor?.icon;
27
+ const name = definitions.getBlockDefinition(props.bundle, "default", null)?.editor?.icon;
28
28
  if (name) {
29
29
  return name;
30
30
  }
@@ -12,7 +12,7 @@ const NOOP_OVERRIDE = {
12
12
  }
13
13
  };
14
14
  export function useEditableFieldOverride(fieldName, host) {
15
- const { eventBus, state, types, definitions, directive, fieldValue } = useBlokkli();
15
+ const { eventBus, state, types, definitions, directive, fieldValue, blocks } = useBlokkli();
16
16
  const el = directive.findEditableElement(fieldName, host);
17
17
  if (!el) {
18
18
  return NOOP_OVERRIDE;
@@ -42,7 +42,12 @@ export function useEditableFieldOverride(fieldName, host) {
42
42
  );
43
43
  let matchingProp = null;
44
44
  if (host.type === itemEntityType) {
45
- const defintion = definitions.getBlockDefinition(host.bundle, "default");
45
+ const block = blocks.getBlock(host.uuid);
46
+ const defintion = definitions.getBlockDefinition(
47
+ host.bundle,
48
+ block?.fieldListType ?? "default",
49
+ block?.parentBlockBundle ?? null
50
+ );
46
51
  if (defintion?.propsFieldMapping) {
47
52
  matchingProp = findMatchingProp(defintion.propsFieldMapping);
48
53
  }
@@ -1,10 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "2.0.0-alpha.50",
4
+ "date": "2026-03-24",
5
+ "body": {
6
+ "en": "<h3>New Features</h3>\n<h4>Swap block positions</h4>\n<p>Select two blocks and use the new &quot;Swap block positions&quot; action to exchange\ntheir positions. This works across different fields, as long as both block types\nare allowed in each other&#39;s field.</p>\n<h3>Improvements</h3>\n<ul>\n<li>Blocks with an editor option (e.g. chart data) now show the edit button even\nwhen regular editing is disabled. Clicking it opens the option editor directly.</li>\n</ul>\n",
7
+ "de": "<h3>Neue Funktionen</h3>\n<h4>Blockpositionen tauschen</h4>\n<p>Zwei Blöcke auswählen und über die neue Aktion «Blockpositionen tauschen» ihre\nPositionen vertauschen. Dies funktioniert auch feldübergreifend, sofern beide\nBlocktypen im jeweiligen Feld erlaubt sind.</p>\n<h3>Verbesserungen</h3>\n<ul>\n<li>Blöcke mit einer Option mit Editor (z.B. Diagrammdaten) zeigen jetzt den\nBearbeiten-Button an, auch wenn die reguläre Bearbeitung deaktiviert ist. Ein\nKlick öffnet direkt den Options-Editor.</li>\n</ul>\n"
8
+ }
9
+ },
2
10
  {
3
11
  "version": "2.0.0-alpha.48",
4
12
  "date": "2026-03-20",
5
13
  "body": {
6
14
  "en": "<h3>Improvements</h3>\n<ul>\n<li>The media library filters now use custom dropdown selects with keyboard\nnavigation and a search field for long lists, replacing the native browser\nselects.</li>\n</ul>\n",
7
- "de": "<h3>Verbesserungen</h3>\n<ul>\n<li>Die Filter in der Medienbibliothek verwenden jetzt eigene Dropdown-Auswahlen\nmit Tastaturnavigation und einem Suchfeld bei langen Listen anstelle der\nnativen Browser-Auswahlen.</li>\n</ul>\n"
15
+ "de": "<h3>Verbesserungen</h3>\n<ul>\n<li>Die Filter in der Medienbibliothek verwenden jetzt eigene Dropdowns mit\nTastaturnavigation und einem Suchfeld bei langen Listen.</li>\n</ul>\n"
8
16
  }
9
17
  },
10
18
  {
@@ -230,7 +230,8 @@ onBlokkliEvent("state:reloaded", async function() {
230
230
  eventBus.emit("select", allSelected);
231
231
  const definition = definitions.getBlockDefinition(
232
232
  newBlock.bundle,
233
- newBlock.fieldListType
233
+ newBlock.fieldListType,
234
+ newBlock.parentBlockBundle
234
235
  );
235
236
  const addBehaviour = definition?.editor?.addBehaviour;
236
237
  if (addBehaviour?.startsWith("complex-option:")) {
@@ -29,6 +29,24 @@ const { eventBus, selection, state, $t, adapter, definitions, permissions } = us
29
29
  const userCanEditLibraryItems = computed(
30
30
  () => permissions.hasPermission("edit_library_item")
31
31
  );
32
+ function getComplexOption(item) {
33
+ const definition = definitions.getBlockDefinition(
34
+ item.bundle,
35
+ item.fieldListType,
36
+ item.parentBlockBundle
37
+ );
38
+ if (!definition?.options) {
39
+ return;
40
+ }
41
+ const keys = Object.keys(definition.options);
42
+ for (let i = 0; i < keys.length; i++) {
43
+ const key = keys[i];
44
+ if (!key) continue;
45
+ const option = definition.options[key];
46
+ if (option?.type !== "json" || !option.dataType) continue;
47
+ return { key, dataType: option.dataType };
48
+ }
49
+ }
32
50
  function isFragment(item) {
33
51
  return item.bundle === fragmentBlockBundle;
34
52
  }
@@ -48,7 +66,7 @@ const editDisabledReason = computed(() => {
48
66
  item.fieldListType,
49
67
  item.parentBlockBundle
50
68
  );
51
- if (definition?.editor?.disableEdit) {
69
+ if (definition?.editor?.disableEdit && !getComplexOption(item)) {
52
70
  return $t(
53
71
  "editDisabledByDefinition",
54
72
  "Editing is disabled for this block type."
@@ -107,32 +125,30 @@ function onClick(items) {
107
125
  }
108
126
  return;
109
127
  }
128
+ const complexOption = getComplexOption(item);
129
+ if (complexOption) {
130
+ eventBus.emit("option:edit-complex", {
131
+ uuid: item.uuid,
132
+ key: complexOption.key,
133
+ dataType: complexOption.dataType
134
+ });
135
+ return;
136
+ }
110
137
  eventBus.emit("item:edit", {
111
138
  uuid: item.uuid,
112
139
  bundle: item.bundle
113
140
  });
114
141
  }
115
142
  onBlokkliEvent("item:doubleClick", function(block) {
116
- const definition = definitions.getBlockDefinition(block);
117
- if (!definition) {
143
+ const complexOption = getComplexOption(block);
144
+ if (complexOption) {
145
+ eventBus.emit("option:edit-complex", {
146
+ uuid: block.uuid,
147
+ key: complexOption.key,
148
+ dataType: complexOption.dataType
149
+ });
118
150
  return;
119
151
  }
120
- const options = definition.options;
121
- if (options) {
122
- const keys = Object.keys(options);
123
- for (let i = 0; i < keys.length; i++) {
124
- const key = keys[i];
125
- if (!key) continue;
126
- const option = options[key];
127
- if (option?.type !== "json" || !option.dataType) continue;
128
- eventBus.emit("option:edit-complex", {
129
- uuid: block.uuid,
130
- key,
131
- dataType: option.dataType
132
- });
133
- return;
134
- }
135
- }
136
152
  onClick([block]);
137
153
  });
138
154
  </script>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <PluginItemAction
3
+ id="swap"
4
+ edit-only
5
+ :title="$t('swapButton', 'Swap block positions')"
6
+ :disabled="swapDisabledReason"
7
+ :hidden="selection.items.value.length !== 2"
8
+ multiple
9
+ icon="bk_mdi_swap_vert"
10
+ :weight="7e3"
11
+ @click="onClick"
12
+ />
13
+ </template>
14
+
15
+ <script setup>
16
+ import { useBlokkli, defineBlokkliFeature, computed } from "#imports";
17
+ import { PluginItemAction } from "#blokkli/editor/plugins";
18
+ import { getSwapDisabledReason } from "#blokkli/editor/helpers/swap";
19
+ const { state, $t, selection, types, permissions } = useBlokkli();
20
+ const { adapter } = defineBlokkliFeature({
21
+ id: "swap",
22
+ icon: "bk_mdi_swap_vert",
23
+ label: "Swap",
24
+ description: "Provides an action to swap two selected blocks.",
25
+ requiredAdapterMethods: ["swapBlocks"]
26
+ });
27
+ const swapDisabledReason = computed(() => {
28
+ const items = selection.items.value;
29
+ if (items.length !== 2) {
30
+ return false;
31
+ }
32
+ const reason = getSwapDisabledReason(items[0], items[1], {
33
+ getFieldConfig: types.getFieldConfig,
34
+ checkBlockBundlePermission: permissions.checkBlockBundlePermission,
35
+ blockHasRestrictedAncestor: permissions.blockHasRestrictedAncestor
36
+ });
37
+ if (reason) {
38
+ return $t("swapNotPossible", "Block positions cannot be swapped.");
39
+ }
40
+ return false;
41
+ });
42
+ async function onClick(items) {
43
+ if (items.length !== 2) {
44
+ return;
45
+ }
46
+ await state.mutateWithLoadingState(
47
+ () => adapter.swapBlocks(items[0].uuid, items[1].uuid),
48
+ $t("swapError", "The blocks could not be swapped.")
49
+ );
50
+ }
51
+ </script>
52
+
53
+ <script>
54
+ export default {
55
+ name: "Swap"
56
+ };
57
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,8 @@
1
+ declare module '#blokkli/editor/adapter' {
2
+ interface BlokkliAdapter<T> {
3
+ /**
4
+ * Swap the positions of two blocks.
5
+ */
6
+ swapBlocks?: (first: string, second: string) => Promise<MutationResponseLike<T>>;
7
+ }
8
+ }
File without changes
@@ -0,0 +1,13 @@
1
+ import type { RenderedFieldListItem } from '../types/field.js';
2
+ import type { FieldConfig } from '../types/definitions.js';
3
+ type SwapValidationContext = {
4
+ getFieldConfig: (entityType: string, entityBundle: string, fieldName: string) => FieldConfig | undefined;
5
+ checkBlockBundlePermission: (bundle: string, operation: 'edit') => boolean;
6
+ blockHasRestrictedAncestor: (uuid: string) => boolean;
7
+ };
8
+ /**
9
+ * Check whether two blocks can be swapped.
10
+ * Returns null if swap is possible, or a reason string if not.
11
+ */
12
+ export declare function getSwapDisabledReason(blockA: RenderedFieldListItem, blockB: RenderedFieldListItem, ctx: SwapValidationContext): string | null;
13
+ export {};
@@ -0,0 +1,31 @@
1
+ export function getSwapDisabledReason(blockA, blockB, ctx) {
2
+ if (blockA.uuid === blockB.uuid) {
3
+ return "Cannot swap a block with itself.";
4
+ }
5
+ if (!ctx.checkBlockBundlePermission(blockA.bundle, "edit") || !ctx.checkBlockBundlePermission(blockB.bundle, "edit")) {
6
+ return "You do not have permission to edit one or both blocks.";
7
+ }
8
+ if (ctx.blockHasRestrictedAncestor(blockA.uuid) || ctx.blockHasRestrictedAncestor(blockB.uuid)) {
9
+ return "One or both blocks are inside a parent with restricted editing permissions.";
10
+ }
11
+ const fieldConfigA = ctx.getFieldConfig(
12
+ blockA.host.type,
13
+ blockA.host.bundle,
14
+ blockA.host.fieldName
15
+ );
16
+ const fieldConfigB = ctx.getFieldConfig(
17
+ blockB.host.type,
18
+ blockB.host.bundle,
19
+ blockB.host.fieldName
20
+ );
21
+ if (!fieldConfigA || !fieldConfigB) {
22
+ return "Could not determine field configuration for one or both blocks.";
23
+ }
24
+ if (fieldConfigA.allowedBundles.length && !fieldConfigA.allowedBundles.includes(blockB.bundle)) {
25
+ return `Bundle "${blockB.bundle}" is not allowed in field "${blockA.host.fieldName}".`;
26
+ }
27
+ if (fieldConfigB.allowedBundles.length && !fieldConfigB.allowedBundles.includes(blockA.bundle)) {
28
+ return `Bundle "${blockA.bundle}" is not allowed in field "${blockB.host.fieldName}".`;
29
+ }
30
+ return null;
31
+ }
@@ -53,6 +53,12 @@ type __VLS_Props = {
53
53
  * Lower weights appear first. Use 'last' to always position at the end.
54
54
  */
55
55
  weight?: number | string | 'last';
56
+ /**
57
+ * Whether the action should be hidden.
58
+ *
59
+ * Unlike disabled, this completely hides the button via v-show.
60
+ */
61
+ hidden?: boolean;
56
62
  /**
57
63
  * Optional icon to display in the button.
58
64
  */
@@ -2,6 +2,7 @@
2
2
  <Teleport to="#bk-blokkli-item-actions">
3
3
  <button
4
4
  v-if="shouldRender"
5
+ v-show="!hidden"
5
6
  ref="el"
6
7
  :disabled="isDisabled"
7
8
  class="bk-item-action"
@@ -51,6 +52,7 @@ const props = defineProps({
51
52
  multiple: { type: Boolean, required: false },
52
53
  editOnly: { type: Boolean, required: false },
53
54
  weight: { type: [Number, String], required: false },
55
+ hidden: { type: Boolean, required: false },
54
56
  icon: { type: null, required: false },
55
57
  tourText: { type: String, required: false }
56
58
  });
@@ -53,6 +53,12 @@ type __VLS_Props = {
53
53
  * Lower weights appear first. Use 'last' to always position at the end.
54
54
  */
55
55
  weight?: number | string | 'last';
56
+ /**
57
+ * Whether the action should be hidden.
58
+ *
59
+ * Unlike disabled, this completely hides the button via v-show.
60
+ */
61
+ hidden?: boolean;
56
62
  /**
57
63
  * Optional icon to display in the button.
58
64
  */
@@ -19,7 +19,7 @@ export type DefinitionProvider = {
19
19
  * @param parentBundle - Optional parent block bundle for nested blocks
20
20
  * @returns The block definition, or undefined if not found
21
21
  */
22
- getBlockDefinition: (bundleOrBlock: string | RenderedFieldListItem, fieldListType?: ValidFieldListTypes | null, parentBundle?: BlockBundleWithNested | null) => BlockDefinition | undefined;
22
+ getBlockDefinition: (bundleOrBlock: string | RenderedFieldListItem, fieldListType: ValidFieldListTypes | null, parentBundle: BlockBundleWithNested | null) => BlockDefinition | undefined;
23
23
  /**
24
24
  * Get the default block definition for a bundle.
25
25
  *
@@ -4,6 +4,7 @@ import type { DefinitionProvider } from './definition.js';
4
4
  import type { DirectiveProvider } from './directive.js';
5
5
  import type { StateProvider } from './state.js';
6
6
  import type { BlockDefinitionProvider } from './types.js';
7
+ import type { BlocksProvider } from './blocks.js';
7
8
  import './fieldValueAdapterTypes.js';
8
9
  /**
9
10
  * Simplified field type for editable fields.
@@ -51,4 +52,4 @@ export type FieldValueProvider = {
51
52
  */
52
53
  getTextFieldValues: () => Promise<TextFieldValue[]>;
53
54
  };
54
- export default function fieldValueProvider(adapters: AdaptersProvider, directive: DirectiveProvider, state: StateProvider, types: BlockDefinitionProvider, definitions: DefinitionProvider): FieldValueProvider;
55
+ export default function fieldValueProvider(adapters: AdaptersProvider, directive: DirectiveProvider, state: StateProvider, types: BlockDefinitionProvider, definitions: DefinitionProvider, blocks: BlocksProvider): FieldValueProvider;
@@ -1,6 +1,6 @@
1
1
  import { itemEntityType } from "#blokkli-build/config";
2
2
  import "./fieldValueAdapterTypes";
3
- export default function fieldValueProvider(adapters, directive, state, types, definitions) {
3
+ export default function fieldValueProvider(adapters, directive, state, types, definitions, blocks) {
4
4
  function resolveFieldType(entityType, bundle, fieldName) {
5
5
  const config = types.editableFieldConfig.forName(
6
6
  entityType,
@@ -40,7 +40,12 @@ export default function fieldValueProvider(adapters, directive, state, types, de
40
40
  );
41
41
  let matchingProp = null;
42
42
  if (host.type === itemEntityType) {
43
- const definition = definitions.getBlockDefinition(host.bundle, "default");
43
+ const block = blocks.getBlock(host.uuid);
44
+ const definition = definitions.getBlockDefinition(
45
+ host.bundle,
46
+ block?.fieldListType ?? "default",
47
+ block?.parentBlockBundle ?? null
48
+ );
44
49
  if (definition?.propsFieldMapping) {
45
50
  matchingProp = findMatchingProp(definition.propsFieldMapping, fieldName);
46
51
  }
@@ -1636,7 +1636,7 @@
1636
1636
  "translation": "Dieser Blocktyp ist in diesem Feld nicht erlaubt."
1637
1637
  },
1638
1638
  "edit": {
1639
- "source": "Edit",
1639
+ "source": "Edit...",
1640
1640
  "translation": "Bearbeiten..."
1641
1641
  },
1642
1642
  "editDisabledByDefinition": {
@@ -2199,6 +2199,14 @@
2199
2199
  "source": "Structure",
2200
2200
  "translation": "Struktur"
2201
2201
  },
2202
+ "feature_swap_description": {
2203
+ "source": "Provides an action to swap two selected blocks.",
2204
+ "translation": ""
2205
+ },
2206
+ "feature_swap_label": {
2207
+ "source": "Swap",
2208
+ "translation": ""
2209
+ },
2202
2210
  "feature_templates_description": {
2203
2211
  "source": "Add blocks from templates.",
2204
2212
  "translation": "Blöcke aus Vorlagen hinzufügen."
@@ -3043,6 +3051,18 @@
3043
3051
  "source": "Shows a structured list of all blocks on the current page. Click on any block to quickly jump to it.",
3044
3052
  "translation": "Zeigt eine strukturierte Liste aller Blöcke auf der aktuellen Seite. Klicken Sie auf einen Block, um ihn auf der Seite anzuzeigen."
3045
3053
  },
3054
+ "swapButton": {
3055
+ "source": "Swap block positions",
3056
+ "translation": "Blockpositionen tauschen"
3057
+ },
3058
+ "swapError": {
3059
+ "source": "The blocks could not be swapped.",
3060
+ "translation": "Die Blöcke konnten nicht getauscht werden."
3061
+ },
3062
+ "swapNotPossible": {
3063
+ "source": "Block positions cannot be swapped.",
3064
+ "translation": "Blockpositionen können nicht getauscht werden."
3065
+ },
3046
3066
  "systemRequirementsDialogButton": {
3047
3067
  "source": "Continue anyway",
3048
3068
  "translation": "Trotzdem fortfahren"
@@ -1636,7 +1636,7 @@
1636
1636
  "translation": ""
1637
1637
  },
1638
1638
  "edit": {
1639
- "source": "Edit",
1639
+ "source": "Edit...",
1640
1640
  "translation": "Modifier"
1641
1641
  },
1642
1642
  "editDisabledByDefinition": {
@@ -2199,6 +2199,14 @@
2199
2199
  "source": "Structure",
2200
2200
  "translation": "Structure"
2201
2201
  },
2202
+ "feature_swap_description": {
2203
+ "source": "Provides an action to swap two selected blocks.",
2204
+ "translation": ""
2205
+ },
2206
+ "feature_swap_label": {
2207
+ "source": "Swap",
2208
+ "translation": ""
2209
+ },
2202
2210
  "feature_templates_description": {
2203
2211
  "source": "Add blocks from templates.",
2204
2212
  "translation": ""
@@ -3043,6 +3051,18 @@
3043
3051
  "source": "Shows a structured list of all blocks on the current page. Click on any block to quickly jump to it.",
3044
3052
  "translation": ""
3045
3053
  },
3054
+ "swapButton": {
3055
+ "source": "Swap block positions",
3056
+ "translation": ""
3057
+ },
3058
+ "swapError": {
3059
+ "source": "The blocks could not be swapped.",
3060
+ "translation": ""
3061
+ },
3062
+ "swapNotPossible": {
3063
+ "source": "Block positions cannot be swapped.",
3064
+ "translation": ""
3065
+ },
3046
3066
  "systemRequirementsDialogButton": {
3047
3067
  "source": "Continue anyway",
3048
3068
  "translation": ""
@@ -1636,7 +1636,7 @@
1636
1636
  "translation": "Dr Blocktyp isch in dem Fäld nid erlaubt."
1637
1637
  },
1638
1638
  "edit": {
1639
- "source": "Edit",
1639
+ "source": "Edit...",
1640
1640
  "translation": "Bearbeite"
1641
1641
  },
1642
1642
  "editDisabledByDefinition": {
@@ -2199,6 +2199,14 @@
2199
2199
  "source": "Structure",
2200
2200
  "translation": "Struktur"
2201
2201
  },
2202
+ "feature_swap_description": {
2203
+ "source": "Provides an action to swap two selected blocks.",
2204
+ "translation": ""
2205
+ },
2206
+ "feature_swap_label": {
2207
+ "source": "Swap",
2208
+ "translation": ""
2209
+ },
2202
2210
  "feature_templates_description": {
2203
2211
  "source": "Add blocks from templates.",
2204
2212
  "translation": "Blöck us Vorlage drzuefüege."
@@ -3043,6 +3051,18 @@
3043
3051
  "source": "Shows a structured list of all blocks on the current page. Click on any block to quickly jump to it.",
3044
3052
  "translation": "Zeigt e strukturierti Lischt vo allne Blöck uf dr aktuälle Sitte aa. Klick uf e belibige Block zum schnäll dohii z'springe."
3045
3053
  },
3054
+ "swapButton": {
3055
+ "source": "Swap block positions",
3056
+ "translation": ""
3057
+ },
3058
+ "swapError": {
3059
+ "source": "The blocks could not be swapped.",
3060
+ "translation": ""
3061
+ },
3062
+ "swapNotPossible": {
3063
+ "source": "Block positions cannot be swapped.",
3064
+ "translation": ""
3065
+ },
3046
3066
  "systemRequirementsDialogButton": {
3047
3067
  "source": "Continue anyway",
3048
3068
  "translation": "Trotzdem wiiterfahre"
@@ -1636,7 +1636,7 @@
1636
1636
  "translation": ""
1637
1637
  },
1638
1638
  "edit": {
1639
- "source": "Edit",
1639
+ "source": "Edit...",
1640
1640
  "translation": "Modifica"
1641
1641
  },
1642
1642
  "editDisabledByDefinition": {
@@ -2199,6 +2199,14 @@
2199
2199
  "source": "Structure",
2200
2200
  "translation": "Struttura"
2201
2201
  },
2202
+ "feature_swap_description": {
2203
+ "source": "Provides an action to swap two selected blocks.",
2204
+ "translation": ""
2205
+ },
2206
+ "feature_swap_label": {
2207
+ "source": "Swap",
2208
+ "translation": ""
2209
+ },
2202
2210
  "feature_templates_description": {
2203
2211
  "source": "Add blocks from templates.",
2204
2212
  "translation": ""
@@ -3043,6 +3051,18 @@
3043
3051
  "source": "Shows a structured list of all blocks on the current page. Click on any block to quickly jump to it.",
3044
3052
  "translation": ""
3045
3053
  },
3054
+ "swapButton": {
3055
+ "source": "Swap block positions",
3056
+ "translation": ""
3057
+ },
3058
+ "swapError": {
3059
+ "source": "The blocks could not be swapped.",
3060
+ "translation": ""
3061
+ },
3062
+ "swapNotPossible": {
3063
+ "source": "Block positions cannot be swapped.",
3064
+ "translation": ""
3065
+ },
3046
3066
  "systemRequirementsDialogButton": {
3047
3067
  "source": "Continue anyway",
3048
3068
  "translation": ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blokkli/editor",
3
- "version": "2.0.0-alpha.49",
3
+ "version": "2.0.0-alpha.50",
4
4
  "description": "Interactive page building experience for Nuxt",
5
5
  "keywords": [
6
6
  "cms",
@@ -186,21 +186,21 @@
186
186
  "vue-tsc": "^3.1.8"
187
187
  },
188
188
  "peerDependencies": {
189
- "@anthropic-ai/sdk": "^0.71.2",
190
- "@lunarisapp/readability": "^1.0.2",
191
- "@thedutchcoder/postcss-rem-to-px": "^0.0.2",
189
+ "@anthropic-ai/sdk": "*",
190
+ "@lunarisapp/readability": "*",
191
+ "@thedutchcoder/postcss-rem-to-px": "*",
192
192
  "acorn": "^8.0.0",
193
- "axe-core": "^4.10.0",
193
+ "axe-core": "*",
194
194
  "esbuild": ">=0.20.0",
195
- "magic-string": "^0.30.0",
195
+ "magic-string": ">=0.30.0",
196
196
  "mammoth": "^1.11.0",
197
197
  "micromatch": "^4.0.0",
198
- "openai": "^4.0.0",
199
- "oxc-walker": "^0.5.0",
200
- "postcss": "^8.4.0",
201
- "postcss-import": "^16.0.0",
202
- "postcss-replace": "^2.0.0",
203
- "tailwindcss": "^3.4.0",
198
+ "openai": "*",
199
+ "oxc-walker": ">=0.5.0",
200
+ "postcss": "*",
201
+ "postcss-import": "*",
202
+ "postcss-replace": "*",
203
+ "tailwindcss": "*",
204
204
  "turndown": "^7.2.2",
205
205
  "typescript": "^5.0.0"
206
206
  },