@blokkli/editor 2.0.0-alpha.46 → 2.0.0-alpha.47

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 (74) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +2 -1
  3. package/dist/modules/agent/runtime/app/helpers/validation.d.ts +13 -0
  4. package/dist/modules/agent/runtime/app/helpers/validation.js +22 -0
  5. package/dist/modules/agent/runtime/app/tools/add_content_search_paragraph/index.js +12 -0
  6. package/dist/modules/agent/runtime/app/tools/add_fragment/index.js +12 -1
  7. package/dist/modules/agent/runtime/app/tools/add_media_paragraph/index.js +12 -0
  8. package/dist/modules/agent/runtime/app/tools/add_paragraphs/index.js +10 -0
  9. package/dist/modules/agent/runtime/app/tools/add_reusable_paragraph/index.js +12 -0
  10. package/dist/modules/agent/runtime/app/tools/add_template/index.js +5 -0
  11. package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/index.js +15 -0
  12. package/dist/modules/agent/runtime/app/tools/delete_paragraphs/index.js +12 -0
  13. package/dist/modules/agent/runtime/app/tools/detach_reusable_paragraph/index.js +12 -0
  14. package/dist/modules/agent/runtime/app/tools/duplicate_paragraphs/index.js +16 -1
  15. package/dist/modules/agent/runtime/app/tools/move_paragraphs/index.js +17 -0
  16. package/dist/modules/agent/runtime/app/tools/rearrange_paragraphs/index.js +11 -0
  17. package/dist/modules/agent/runtime/app/tools/replace_content_search_item/index.js +8 -0
  18. package/dist/modules/agent/runtime/app/tools/replace_media_field/index.js +10 -0
  19. package/dist/modules/agent/runtime/app/tools/set_paragraph_options/index.js +10 -0
  20. package/dist/modules/agent/runtime/app/tools/swap_paragraphs/index.js +15 -0
  21. package/dist/modules/agent/runtime/app/tools/update_text_fields/index.js +21 -1
  22. package/dist/modules/agent/runtime/app/types/index.d.ts +6 -6
  23. package/dist/modules/drupal/index.mjs +2 -1
  24. package/dist/modules/drupal/runtime/adapter/index.js +15 -3
  25. package/dist/runtime/editor/components/Actions/index.vue +47 -2
  26. package/dist/runtime/editor/components/AnimationCanvas/index.vue +6 -3
  27. package/dist/runtime/editor/components/BundleSelector/index.d.vue.ts +8 -4
  28. package/dist/runtime/editor/components/BundleSelector/index.vue +111 -13
  29. package/dist/runtime/editor/components/BundleSelector/index.vue.d.ts +8 -4
  30. package/dist/runtime/editor/components/EditProvider.vue +2 -2
  31. package/dist/runtime/editor/components/FlexTextarea/index.vue +8 -1
  32. package/dist/runtime/editor/css/output.css +1 -1
  33. package/dist/runtime/editor/features/add-list/Blocks/index.vue +6 -3
  34. package/dist/runtime/editor/features/analyze/Renderer/index.vue +1 -1
  35. package/dist/runtime/editor/features/block-scheduler/index.vue +7 -1
  36. package/dist/runtime/editor/features/changelog/Dialog/index.vue +1 -1
  37. package/dist/runtime/editor/features/changelog/changelog.json +18 -10
  38. package/dist/runtime/editor/features/clipboard/index.vue +6 -1
  39. package/dist/runtime/editor/features/comments/AddForm/index.d.vue.ts +2 -2
  40. package/dist/runtime/editor/features/comments/AddForm/index.vue.d.ts +2 -2
  41. package/dist/runtime/editor/features/delete/index.vue +17 -2
  42. package/dist/runtime/editor/features/dragging-overlay/Renderer/index.vue +12 -2
  43. package/dist/runtime/editor/features/dragging-overlay/index.vue +5 -2
  44. package/dist/runtime/editor/features/duplicate/index.vue +23 -7
  45. package/dist/runtime/editor/features/edit/index.vue +29 -8
  46. package/dist/runtime/editor/features/editable-field/index.vue +15 -1
  47. package/dist/runtime/editor/features/fragments/index.vue +5 -2
  48. package/dist/runtime/editor/features/hover/Renderer/index.vue +19 -6
  49. package/dist/runtime/editor/features/hover/Renderer/vertex.glsl +5 -2
  50. package/dist/runtime/editor/features/library/index.vue +52 -8
  51. package/dist/runtime/editor/features/media-library/index.vue +7 -2
  52. package/dist/runtime/editor/features/multi-select/Renderer/index.vue +4 -1
  53. package/dist/runtime/editor/features/search/index.vue +7 -2
  54. package/dist/runtime/editor/features/selection/AddButtons/Renderer/index.vue +1 -1
  55. package/dist/runtime/editor/features/selection/AddButtons/index.vue +26 -2
  56. package/dist/runtime/editor/features/selection/Renderer/index.vue +23 -5
  57. package/dist/runtime/editor/features/selection/Renderer/vertex.glsl +5 -2
  58. package/dist/runtime/editor/features/selection/index.vue +17 -5
  59. package/dist/runtime/editor/features/translations/index.vue +17 -11
  60. package/dist/runtime/editor/helpers/dropTargets/index.d.ts +1 -1
  61. package/dist/runtime/editor/helpers/dropTargets/index.js +2 -2
  62. package/dist/runtime/editor/plugins/ItemAction/index.d.vue.ts +4 -1
  63. package/dist/runtime/editor/plugins/ItemAction/index.vue +9 -3
  64. package/dist/runtime/editor/plugins/ItemAction/index.vue.d.ts +4 -1
  65. package/dist/runtime/editor/providers/permissions.d.ts +22 -1
  66. package/dist/runtime/editor/providers/permissions.js +99 -3
  67. package/dist/runtime/editor/providers/selection.d.ts +2 -1
  68. package/dist/runtime/editor/providers/selection.js +10 -5
  69. package/dist/runtime/editor/translations/de.json +89 -1
  70. package/dist/runtime/editor/translations/fr.json +89 -1
  71. package/dist/runtime/editor/translations/gsw_CH.json +89 -1
  72. package/dist/runtime/editor/translations/it.json +89 -1
  73. package/dist/runtime/editor/types/definitions.d.ts +2 -0
  74. package/package.json +1 -1
@@ -31,6 +31,7 @@ uniform vec3 u_color_accent;
31
31
  uniform vec3 u_color_teal;
32
32
  uniform vec3 u_color_white;
33
33
  uniform vec3 u_color_lime;
34
+ uniform vec3 u_color_yellow;
34
35
 
35
36
  // The transformed quad for the fragment shader.
36
37
  out vec4 v_quad;
@@ -140,8 +141,10 @@ void main() {
140
141
  v_dash_cycle = 14.0 - u_scale * 1.0;
141
142
  v_rect_size_artboard = vec2(hoverPos.z, hoverPos.w);
142
143
 
143
- // Select color based on type: 0 = mono, 1 = accent, 2 = teal, 3 = white (inverted), 4 = lime (library)
144
- if (hoverType > 3.5) {
144
+ // Select color based on type: 0 = mono, 1 = accent, 2 = teal, 3 = white (inverted), 4 = lime (library), 5 = yellow (restricted)
145
+ if (hoverType > 4.5) {
146
+ v_color = u_color_yellow;
147
+ } else if (hoverType > 3.5) {
145
148
  v_color = u_color_lime;
146
149
  } else if (hoverType > 2.5) {
147
150
  v_color = u_color_white;
@@ -3,6 +3,7 @@
3
3
  v-if="isReusable"
4
4
  id="library_detach"
5
5
  :title="$t('libraryDetach', 'Detach from library')"
6
+ :disabled="detachDisabledReason"
6
7
  icon="reusable-detach"
7
8
  edit-only
8
9
  multiple
@@ -13,7 +14,7 @@
13
14
  v-else-if="!isReusable"
14
15
  id="library_make_reusable"
15
16
  :title="$t('libraryAdd', 'Add to library...')"
16
- :disabled="!canMakeReusable"
17
+ :disabled="makeReusableDisabledReason"
17
18
  edit-only
18
19
  icon="reusable"
19
20
  :weight="-70"
@@ -82,6 +83,12 @@ const showReusableDialog = useDialog("library-reusable", "center");
82
83
  const userCanCreateLibraryItem = computed(
83
84
  () => permissions.hasPermission("create_library_item")
84
85
  );
86
+ const canAddFromLibrary = computed(
87
+ () => permissions.checkBlockBundlePermission(fromLibraryBlockBundle, "add")
88
+ );
89
+ const canEditFromLibrary = computed(
90
+ () => permissions.checkBlockBundlePermission(fromLibraryBlockBundle, "edit")
91
+ );
85
92
  async function selectNewlyAdded(cb) {
86
93
  const uuidsBefore = state.getAllUuids();
87
94
  await cb();
@@ -93,7 +100,7 @@ async function selectNewlyAdded(cb) {
93
100
  eventBus.emit("select", newUuid);
94
101
  }
95
102
  const onDetach = async () => {
96
- if (!adapter.detachReusableBlock || !selection.uuids.value.length) {
103
+ if (!adapter.detachReusableBlock || !selection.uuids.value.length || detachDisabledReason.value) {
97
104
  return;
98
105
  }
99
106
  await selectNewlyAdded(
@@ -106,7 +113,7 @@ const onDetach = async () => {
106
113
  };
107
114
  const placedAction = ref(null);
108
115
  const onAddLibraryItem = async (uuid) => {
109
- if (!placedAction.value || !adapter.addLibraryItem) {
116
+ if (!placedAction.value || !adapter.addLibraryItem || !canAddFromLibrary.value) {
110
117
  return;
111
118
  }
112
119
  await state.mutateWithLoadingState(
@@ -166,9 +173,46 @@ const fromLibraryAllowedInList = computed(() => {
166
173
  }
167
174
  return types.allowedTypesInList.value.includes(fromLibraryBlockBundle);
168
175
  });
169
- const canMakeReusable = computed(
170
- () => !isReusable.value && itemBundle?.value?.allowReusable && fromLibraryAllowedInList.value && userCanCreateLibraryItem.value
171
- );
176
+ const detachDisabledReason = computed(() => {
177
+ if (!canEditFromLibrary.value) {
178
+ return $t(
179
+ "libraryDetachNoPermission",
180
+ "You do not have permission to detach this block."
181
+ );
182
+ }
183
+ return false;
184
+ });
185
+ const makeReusableDisabledReason = computed(() => {
186
+ if (isReusable.value) {
187
+ return false;
188
+ }
189
+ if (!userCanCreateLibraryItem.value) {
190
+ return $t(
191
+ "libraryAddNoPermission",
192
+ "You do not have permission to create library items."
193
+ );
194
+ }
195
+ const item = selection.item.value;
196
+ if (item && !permissions.checkBlockBundlePermission(item.bundle, "edit")) {
197
+ return $t(
198
+ "libraryAddNoEditPermission",
199
+ "You do not have permission to edit this block."
200
+ );
201
+ }
202
+ if (!itemBundle?.value?.allowReusable) {
203
+ return $t(
204
+ "libraryAddNotSupported",
205
+ "This block type cannot be made reusable."
206
+ );
207
+ }
208
+ if (!fromLibraryAllowedInList.value) {
209
+ return $t(
210
+ "libraryAddNotAllowedInField",
211
+ "Reusable blocks are not allowed in this field."
212
+ );
213
+ }
214
+ return false;
215
+ });
172
216
  const editingLibraryItem = ref(null);
173
217
  onBlokkliEvent("library:edit-item", function(e) {
174
218
  editingLibraryItem.value = {
@@ -186,7 +230,7 @@ function onSubmitLibraryItem() {
186
230
  }
187
231
  defineDropHandler("reusable", {
188
232
  async execute({ items, host, afterUuid }) {
189
- if (adapter.addLibraryItem) {
233
+ if (adapter.addLibraryItem && canAddFromLibrary.value) {
190
234
  await state.mutateWithLoadingState(
191
235
  () => adapter.addLibraryItem({
192
236
  libraryItemUuid: items[0].libraryItemUuid,
@@ -198,7 +242,7 @@ defineDropHandler("reusable", {
198
242
  }
199
243
  });
200
244
  defineAddAction(() => {
201
- if (!adapter.addLibraryItem || !adapter.getLibraryItems || !isSupportedOnEntity.value) {
245
+ if (!adapter.addLibraryItem || !adapter.getLibraryItems || !isSupportedOnEntity.value || !canAddFromLibrary.value) {
202
246
  return;
203
247
  }
204
248
  return {
@@ -30,7 +30,7 @@ defineBlokkliFeature({
30
30
  description: "Implements a media library to easily drag and drop media like images or videos.",
31
31
  requiredAdapterMethods: ["mediaLibraryGetResults", "mediaLibraryAddBlock"]
32
32
  });
33
- const { $t, adapter, state, types, directive } = useBlokkli();
33
+ const { $t, adapter, state, types, directive, permissions } = useBlokkli();
34
34
  const ERROR_MESSAGE = $t(
35
35
  "mediaLibraryReplaceFailed",
36
36
  "Failed to replace media."
@@ -55,6 +55,9 @@ defineDropAreas((dragItems) => {
55
55
  return;
56
56
  }
57
57
  const isBlock = field.type === itemEntityType;
58
+ if (isBlock && (!permissions.checkBlockBundlePermission(field.bundle, "edit") || permissions.blockHasRestrictedAncestor(field.uuid))) {
59
+ return;
60
+ }
58
61
  const draggableHost = {
59
62
  uuid: field.uuid,
60
63
  type: field.type,
@@ -106,7 +109,9 @@ defineDropHandler("media_library", {
106
109
  return [];
107
110
  }
108
111
  const item = items[0];
109
- return field.allowedBundles.filter((b) => item.itemBundles.includes(b));
112
+ return field.allowedBundles.filter(
113
+ (b) => item.itemBundles.includes(b) && permissions.checkBlockBundlePermission(b, "add")
114
+ );
110
115
  },
111
116
  async execute({ items, host, afterUuid, bundle }) {
112
117
  if (adapter.mediaLibraryAddBlock && items.length === 1) {
@@ -15,7 +15,7 @@ import {
15
15
  } from "twgl.js";
16
16
  import { RectangleBufferCollector } from "#blokkli/editor/helpers/webgl";
17
17
  import { defineRenderer, useDebugLogger } from "#blokkli/editor/composables";
18
- const { eventBus, dom, theme, animation, ui, blocks } = useBlokkli();
18
+ const { eventBus, dom, theme, animation, ui, blocks, permissions } = useBlokkli();
19
19
  const logger = useDebugLogger();
20
20
  const props = defineProps({
21
21
  startX: { type: Number, required: true },
@@ -43,6 +43,9 @@ class MultiSelectRectangleBufferCollector extends RectangleBufferCollector {
43
43
  if (!block) {
44
44
  continue;
45
45
  }
46
+ if (permissions.blockHasRestrictedAncestor(uuid)) {
47
+ continue;
48
+ }
46
49
  const el = dom.getDragElement(block);
47
50
  if (!el) {
48
51
  continue;
@@ -58,7 +58,7 @@ defineBlokkliFeature({
58
58
  label: "Search",
59
59
  description: "Provides an overlay with shortcut to search for blocks on the current page or existing content to add as blocks."
60
60
  });
61
- const { $t, selection, ui, adapter, state, types, directive } = useBlokkli();
61
+ const { $t, selection, ui, adapter, state, types, directive, permissions } = useBlokkli();
62
62
  const ERROR_MESSAGE = $t(
63
63
  "searchContentReplaceFailed",
64
64
  "Failed to replace content."
@@ -79,6 +79,9 @@ defineDropAreas((dragItems) => {
79
79
  if (field.type !== itemEntityType) {
80
80
  return;
81
81
  }
82
+ if (!permissions.checkBlockBundlePermission(field.bundle, "edit") || permissions.blockHasRestrictedAncestor(field.uuid)) {
83
+ return;
84
+ }
82
85
  const config = types.getDroppableFieldConfig(field.fieldName, field);
83
86
  const allowedBundles = config.allowed.find(
84
87
  (v) => v.type === searchItem.entityType
@@ -115,7 +118,9 @@ defineDropAreas((dragItems) => {
115
118
  defineDropHandler("search_content", {
116
119
  resolveBundles({ items, field }) {
117
120
  const item = items[0];
118
- return field.allowedBundles.filter((b) => item.itemBundles.includes(b));
121
+ return field.allowedBundles.filter(
122
+ (b) => item.itemBundles.includes(b) && permissions.checkBlockBundlePermission(b, "add")
123
+ );
119
124
  },
120
125
  async execute({ items, host, afterUuid, bundle }) {
121
126
  if (!adapter.addContentSearchItem) {
@@ -52,7 +52,7 @@ const currentUuid = ref("");
52
52
  const currentBundleLabel = ref("");
53
53
  const currentSingleAllowedBundleLabel = ref(null);
54
54
  const tooltipData = computed(() => {
55
- if (hoveredCircle.value < 0) {
55
+ if (hoveredCircle.value < 0 || ui.openTooltip.value) {
56
56
  return null;
57
57
  }
58
58
  const index = hoveredCircle.value;
@@ -8,9 +8,11 @@
8
8
  :anchor-el="addData.anchorEl"
9
9
  :anchor-coordinates="addData.anchorCoordinates"
10
10
  :label="addData.label"
11
+ :field="addData.field"
11
12
  @select="onSelectBundle"
12
13
  @close="closeOverlay"
13
14
  @action="onSelectAction"
15
+ @fragment="onSelectFragment"
14
16
  />
15
17
  </BlokkliTransition>
16
18
  </Teleport>
@@ -59,7 +61,9 @@ const {
59
61
  fields,
60
62
  animation,
61
63
  context,
62
- selection
64
+ selection,
65
+ permissions,
66
+ adapter
63
67
  } = useBlokkli();
64
68
  const isLocked = ref(false);
65
69
  const shouldRender = computed(() => {
@@ -258,6 +262,21 @@ function onSelectAction(action) {
258
262
  });
259
263
  closeOverlay();
260
264
  }
265
+ async function onSelectFragment(name) {
266
+ const fragmentsAddBlock = adapter.fragmentsAddBlock;
267
+ if (!addData.value || !fragmentsAddBlock) {
268
+ return;
269
+ }
270
+ const { host, preceedingUuid } = addData.value;
271
+ closeOverlay();
272
+ await state.mutateWithLoadingState(
273
+ () => fragmentsAddBlock({
274
+ name,
275
+ host: { ...host },
276
+ preceedingUuid
277
+ })
278
+ );
279
+ }
261
280
  const cache = /* @__PURE__ */ new Map();
262
281
  function clearAllCache() {
263
282
  cache.clear();
@@ -349,7 +368,12 @@ onBlokkliEvent("state:reloaded", () => {
349
368
  }
350
369
  });
351
370
  function setAddData(key, field, label, preceedingUuid, anchorEl, anchorCoordinates) {
352
- const allowedBundles = field.allowedBundles;
371
+ if (field.hostEntityType === itemEntityType && (!permissions.checkBlockBundlePermission(field.hostEntityBundle, "edit") || permissions.blockHasRestrictedAncestor(field.hostEntityUuid))) {
372
+ return;
373
+ }
374
+ const allowedBundles = field.allowedBundles.filter(
375
+ (v) => permissions.checkBlockBundlePermission(v, "add")
376
+ );
353
377
  if (allowedBundles.length === 0) {
354
378
  return;
355
379
  }
@@ -22,7 +22,7 @@ const props = defineProps({
22
22
  blocks: { type: Array, required: true },
23
23
  hasHostSelected: { type: Boolean, required: true }
24
24
  });
25
- const { animation, theme, dom, ui, state } = useBlokkli();
25
+ const { animation, theme, dom, ui, state, permissions } = useBlokkli();
26
26
  class SelectionRectangleBufferCollector extends RectangleBufferCollector {
27
27
  uuids = [];
28
28
  lastCount = 0;
@@ -50,7 +50,8 @@ class SelectionRectangleBufferCollector extends RectangleBufferCollector {
50
50
  y: 0,
51
51
  radius: [0, 0, 0, 0],
52
52
  isInverted: false,
53
- isFromLibrary: false
53
+ isFromLibrary: false,
54
+ isRestricted: false
54
55
  },
55
56
  3
56
57
  // Type 3 = host selection
@@ -70,8 +71,11 @@ class SelectionRectangleBufferCollector extends RectangleBufferCollector {
70
71
  }
71
72
  const style = ui.lowPerformanceMode.value ? null : theme.getDraggableStyle(el);
72
73
  const isFromLibrary = state.fromLibraryUuids.value.includes(block.uuid);
74
+ const isRestricted = !permissions.checkBlockBundlePermission(block.bundle, "edit") || !permissions.checkBlockBundlePermission(block.bundle, "delete") || !permissions.checkBlockBundlePermission(block.bundle, "add");
73
75
  let type = 0;
74
- if (isFromLibrary) {
76
+ if (isRestricted) {
77
+ type = 4;
78
+ } else if (isFromLibrary) {
75
79
  type = 2;
76
80
  } else if (style?.isInverted) {
77
81
  type = 1;
@@ -85,7 +89,8 @@ class SelectionRectangleBufferCollector extends RectangleBufferCollector {
85
89
  y: rect.y,
86
90
  radius: style?.radius ?? [0, 0, 0, 0],
87
91
  isInverted: !!style?.isInverted,
88
- isFromLibrary
92
+ isFromLibrary,
93
+ isRestricted
89
94
  },
90
95
  type
91
96
  );
@@ -141,6 +146,15 @@ const getColorLibrary = useTransitionedValue(() => {
141
146
  }
142
147
  return theme.lime.value.normal;
143
148
  });
149
+ const getColorRestricted = useTransitionedValue(() => {
150
+ if (selectionColorOverride.value) {
151
+ return selectionColorOverride.value;
152
+ }
153
+ if (hasTransformingStyle.value) {
154
+ return theme.orange.value.normal;
155
+ }
156
+ return theme.yellow.value.normal;
157
+ });
144
158
  const getColorHost = useTransitionedValue(() => {
145
159
  return theme.mono.value[700];
146
160
  });
@@ -161,6 +175,7 @@ const { collector } = defineRenderer("selection-overlay", {
161
175
  u_color_default: toShaderColor(getColorDefault()),
162
176
  u_color_inverted: toShaderColor(getColorInverted()),
163
177
  u_color_library: toShaderColor(getColorLibrary()),
178
+ u_color_restricted: toShaderColor(getColorRestricted()),
164
179
  u_color_host: toShaderColor(getColorHost()),
165
180
  u_artboard_size: [
166
181
  ui.artboardSize.value.width,
@@ -187,6 +202,7 @@ const { collector } = defineRenderer("selection-overlay", {
187
202
  const colorDefault = rgbaToCss(getColorDefault());
188
203
  const colorInverted = rgbaToCss(getColorInverted());
189
204
  const colorLibrary = rgbaToCss(getColorLibrary());
205
+ const colorRestricted = rgbaToCss(getColorRestricted());
190
206
  const colorHost = rgbaToCss(getColorHost());
191
207
  const smoothstepValue = Math.max(
192
208
  0,
@@ -196,7 +212,9 @@ const { collector } = defineRenderer("selection-overlay", {
196
212
  for (let i = 0; i < rects.length; i++) {
197
213
  const rect = rects[i];
198
214
  let strokeColor = colorDefault;
199
- if (rect.isFromLibrary) {
215
+ if (rect.isRestricted) {
216
+ strokeColor = colorRestricted;
217
+ } else if (rect.isFromLibrary) {
200
218
  strokeColor = colorLibrary;
201
219
  } else if (rect.isInverted) {
202
220
  strokeColor = colorInverted;
@@ -21,6 +21,7 @@ uniform vec2 u_resolution;
21
21
  uniform vec3 u_color_default;
22
22
  uniform vec3 u_color_inverted;
23
23
  uniform vec3 u_color_library;
24
+ uniform vec3 u_color_restricted;
24
25
  uniform vec3 u_color_host;
25
26
 
26
27
  // The transformed quad for the fragment shader.
@@ -86,9 +87,11 @@ void main() {
86
87
 
87
88
  v_rect_width = adjusted_quad.x;
88
89
 
89
- // Set color based on type: 0=default, 1=inverted, 2=library, 3=host
90
+ // Set color based on type: 0=default, 1=inverted, 2=library, 3=host, 4=restricted
90
91
  v_color = u_color_default;
91
- if (a_rect_type > 2.5) {
92
+ if (a_rect_type > 3.5) {
93
+ v_color = u_color_restricted;
94
+ } else if (a_rect_type > 2.5) {
92
95
  v_color = u_color_host;
93
96
  } else if (a_rect_type > 1.5) {
94
97
  v_color = u_color_library;
@@ -52,7 +52,8 @@ const {
52
52
  types,
53
53
  state,
54
54
  blocks,
55
- element
55
+ element,
56
+ permissions
56
57
  } = useBlokkli();
57
58
  const originatesFromTextInput = (e) => e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
58
59
  const getSelectionOrder = useStateBasedCache(
@@ -223,11 +224,22 @@ function selectInList(prev) {
223
224
  }
224
225
  const selectionOrder = getSelectionOrder();
225
226
  const currentIndex = selectionOrder.indexOf(currentUuid);
227
+ if (currentIndex === -1) {
228
+ return;
229
+ }
226
230
  const delta = prev ? -1 : 1;
227
- const newIndex = modulo(currentIndex + delta, selectionOrder.length);
228
- const newUuid = selectionOrder[newIndex];
229
- if (newUuid) {
230
- selectBlock(newUuid);
231
+ const length = selectionOrder.length;
232
+ for (let step = 1; step < length; step++) {
233
+ const newIndex = modulo(currentIndex + delta * step, length);
234
+ const candidate = selectionOrder[newIndex];
235
+ if (!candidate) {
236
+ continue;
237
+ }
238
+ const resolved = permissions.getRestrictedAncestor(candidate) ?? candidate;
239
+ if (resolved !== currentUuid) {
240
+ selectBlock(candidate);
241
+ return;
242
+ }
231
243
  }
232
244
  }
233
245
  onBlokkliEvent("keyPressed", (e) => {
@@ -55,7 +55,7 @@
55
55
  <PluginItemAction
56
56
  v-if="isTranslating"
57
57
  id="translate"
58
- :disabled="!canTranslateBlock"
58
+ :disabled="translateDisabledReason"
59
59
  :title="$t('translationsItemAction', 'Translate')"
60
60
  icon="bk_mdi_translate"
61
61
  :weight="-100"
@@ -117,13 +117,16 @@ const items = computed(() => {
117
117
  return null;
118
118
  }).filter(falsy);
119
119
  });
120
- const canTranslateBlock = computed(() => {
120
+ const translateDisabledReason = computed(() => {
121
121
  const block = selection.item.value;
122
122
  if (!block) {
123
123
  return false;
124
124
  }
125
125
  if (block.library?.libraryItemUuid) {
126
- return false;
126
+ return $t(
127
+ "translateLibraryBlock",
128
+ "Reusable blocks cannot be translated here."
129
+ );
127
130
  }
128
131
  const definition = definitions.getBlockDefinition(
129
132
  block.bundle,
@@ -131,16 +134,19 @@ const canTranslateBlock = computed(() => {
131
134
  block.parentBlockBundle
132
135
  );
133
136
  if (definition?.editor?.disableEdit) {
134
- return false;
137
+ return $t(
138
+ "translateEditDisabled",
139
+ "Editing is disabled for this block type."
140
+ );
135
141
  }
136
142
  const type = types.getBlockBundleDefinition(block.bundle);
137
- if (!type) {
138
- return false;
139
- }
140
- if (!type.isTranslatable) {
141
- return false;
143
+ if (!type || !type.isTranslatable) {
144
+ return $t(
145
+ "translateNotTranslatable",
146
+ "This block type is not translatable."
147
+ );
142
148
  }
143
- return true;
149
+ return false;
144
150
  });
145
151
  function onClick(item, event) {
146
152
  if (item.translation?.exists) {
@@ -161,7 +167,7 @@ function onTranslate(items2) {
161
167
  }
162
168
  }
163
169
  onBlokkliEvent("item:doubleClick", function(block) {
164
- if (isTranslating.value && canTranslateBlock.value) {
170
+ if (isTranslating.value && !translateDisabledReason.value) {
165
171
  onTranslate([block]);
166
172
  }
167
173
  });
@@ -3,4 +3,4 @@ export type Orientation = 'horizontal' | 'vertical';
3
3
  export declare const MIN_GAP = 20;
4
4
  export declare function getGapSize(orientation: Orientation, element: HTMLElement): number;
5
5
  export declare function getChildrenOrientation(element: HTMLElement): Orientation;
6
- export declare function determineCanAddChildren(field: BlokkliFieldElement, children: HTMLElement[], uuids: string[], currentCount: number, itemsToAdd: number, draggingBundles?: string[], draggingFragments?: string[]): boolean;
6
+ export declare function determineCanAddChildren(field: BlokkliFieldElement, children: HTMLElement[], uuids: string[], currentCount: number, itemsToAdd: number, draggingBundles?: string[], draggingFragments?: string[], isBundlePermitted?: (bundle: string) => boolean): boolean;
@@ -31,7 +31,7 @@ export function getChildrenOrientation(element) {
31
31
  }
32
32
  return "vertical";
33
33
  }
34
- export function determineCanAddChildren(field, children, uuids, currentCount, itemsToAdd, draggingBundles, draggingFragments) {
34
+ export function determineCanAddChildren(field, children, uuids, currentCount, itemsToAdd, draggingBundles, draggingFragments, isBundlePermitted) {
35
35
  if (field.cardinality !== -1) {
36
36
  const childrenThatAreSelection = children.filter((child) => {
37
37
  const uuid = child.dataset.bkUuid;
@@ -50,7 +50,7 @@ export function determineCanAddChildren(field, children, uuids, currentCount, it
50
50
  }
51
51
  if (!uuids.length) {
52
52
  return draggingBundles.some(
53
- (bundle) => field.allowedBundles.includes(bundle)
53
+ (bundle) => field.allowedBundles.includes(bundle) && (!isBundlePermitted || isBundlePermitted(bundle))
54
54
  );
55
55
  }
56
56
  const bundlesAllowed = draggingBundles.every(
@@ -12,8 +12,11 @@ type __VLS_Props = {
12
12
  title: string;
13
13
  /**
14
14
  * Whether the action is disabled.
15
+ *
16
+ * When a string is provided, the button is disabled and the string is
17
+ * displayed as the tooltip text explaining why.
15
18
  */
16
- disabled?: boolean;
19
+ disabled?: boolean | string;
17
20
  /**
18
21
  * Whether the button should be displayed in an active state.
19
22
  *
@@ -25,6 +25,9 @@
25
25
  group="blocks"
26
26
  @pressed="onClick"
27
27
  />
28
+ <div v-if="disabledReason" class="bk-item-action-disabled-reason">
29
+ <span>{{ disabledReason }}</span>
30
+ </div>
28
31
  </div>
29
32
  </button>
30
33
  </Teleport>
@@ -41,7 +44,7 @@ const uuids = computed(() => selection.uuids.value);
41
44
  const props = defineProps({
42
45
  id: { type: String, required: true },
43
46
  title: { type: String, required: true },
44
- disabled: { type: Boolean, required: false },
47
+ disabled: { type: [Boolean, String], required: false },
45
48
  active: { type: Boolean, required: false },
46
49
  keyCode: { type: String, required: false },
47
50
  meta: { type: Boolean, required: false },
@@ -52,7 +55,10 @@ const props = defineProps({
52
55
  tourText: { type: String, required: false }
53
56
  });
54
57
  const isDisabled = computed(
55
- () => props.disabled || !props.multiple && selection.items.value.length > 1
58
+ () => !!props.disabled || !props.multiple && selection.items.value.length > 1
59
+ );
60
+ const disabledReason = computed(
61
+ () => typeof props.disabled === "string" ? props.disabled : null
56
62
  );
57
63
  const shouldRender = computed(() => {
58
64
  if (props.editOnly) {
@@ -72,7 +78,7 @@ defineCommands(() => ({
72
78
  group: "selection",
73
79
  label: props.title,
74
80
  icon: props.icon,
75
- disabled: props.disabled || !selection.items.value.length,
81
+ disabled: isDisabled.value || !selection.items.value.length,
76
82
  callback: onClick
77
83
  }));
78
84
  defineTourItem(() => {
@@ -12,8 +12,11 @@ type __VLS_Props = {
12
12
  title: string;
13
13
  /**
14
14
  * Whether the action is disabled.
15
+ *
16
+ * When a string is provided, the button is disabled and the string is
17
+ * displayed as the tooltip text explaining why.
15
18
  */
16
- disabled?: boolean;
19
+ disabled?: boolean | string;
17
20
  /**
18
21
  * Whether the button should be displayed in an active state.
19
22
  *
@@ -1,6 +1,27 @@
1
1
  import type { BlokkliAdapter } from '#blokkli/editor/adapter';
2
+ import type { BlockPermission } from '../types/definitions.js';
2
3
  import type { UserPermissions } from '../types/permissions.js';
4
+ import type { BlocksProvider } from './blocks.js';
3
5
  export type PermissionsProvider = {
4
6
  hasPermission: (permission: UserPermissions) => boolean;
7
+ checkBlockBundlePermission: (bundle: string, operation: BlockPermission) => boolean;
8
+ filterDeniedBundles: (bundles: string[], operation: BlockPermission) => string[];
9
+ /**
10
+ * Check if a block has any ancestor whose bundle lacks 'edit' permission.
11
+ *
12
+ * When an ancestor is restricted, all descendants are considered restricted
13
+ * too — the user cannot add, edit, or delete blocks inside a restricted
14
+ * parent.
15
+ */
16
+ blockHasRestrictedAncestor: (uuid: string) => boolean;
17
+ /**
18
+ * Get the nearest ancestor block that restricts editing.
19
+ *
20
+ * Walks up the parent chain and returns the UUID of the first ancestor
21
+ * whose bundle lacks 'edit' permission, or undefined if no ancestor is
22
+ * restricted.
23
+ */
24
+ getRestrictedAncestor: (uuid: string) => string | undefined;
25
+ getBlockBundlePermissions: (bundle: string) => BlockPermission[];
5
26
  };
6
- export default function (adapter: BlokkliAdapter<any>): Promise<PermissionsProvider>;
27
+ export default function (adapter: BlokkliAdapter<any>, blocks: BlocksProvider): Promise<PermissionsProvider>;