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

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 (79) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +23 -6
  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 +26 -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/Library/FilterSelect/index.d.vue.ts +15 -0
  52. package/dist/runtime/editor/features/media-library/Library/FilterSelect/index.vue +168 -0
  53. package/dist/runtime/editor/features/media-library/Library/FilterSelect/index.vue.d.ts +15 -0
  54. package/dist/runtime/editor/features/media-library/Library/index.vue +21 -16
  55. package/dist/runtime/editor/features/media-library/index.vue +7 -2
  56. package/dist/runtime/editor/features/multi-select/Renderer/index.vue +4 -1
  57. package/dist/runtime/editor/features/search/index.vue +7 -2
  58. package/dist/runtime/editor/features/selection/AddButtons/Renderer/index.vue +1 -1
  59. package/dist/runtime/editor/features/selection/AddButtons/index.vue +26 -2
  60. package/dist/runtime/editor/features/selection/Renderer/index.vue +23 -5
  61. package/dist/runtime/editor/features/selection/Renderer/vertex.glsl +5 -2
  62. package/dist/runtime/editor/features/selection/index.vue +17 -5
  63. package/dist/runtime/editor/features/translations/index.vue +17 -11
  64. package/dist/runtime/editor/helpers/dropTargets/index.d.ts +1 -1
  65. package/dist/runtime/editor/helpers/dropTargets/index.js +2 -2
  66. package/dist/runtime/editor/plugins/ItemAction/index.d.vue.ts +4 -1
  67. package/dist/runtime/editor/plugins/ItemAction/index.vue +9 -3
  68. package/dist/runtime/editor/plugins/ItemAction/index.vue.d.ts +4 -1
  69. package/dist/runtime/editor/providers/keyboard.js +8 -5
  70. package/dist/runtime/editor/providers/permissions.d.ts +22 -1
  71. package/dist/runtime/editor/providers/permissions.js +99 -3
  72. package/dist/runtime/editor/providers/selection.d.ts +2 -1
  73. package/dist/runtime/editor/providers/selection.js +10 -5
  74. package/dist/runtime/editor/translations/de.json +96 -0
  75. package/dist/runtime/editor/translations/fr.json +96 -0
  76. package/dist/runtime/editor/translations/gsw_CH.json +515 -419
  77. package/dist/runtime/editor/translations/it.json +96 -0
  78. package/dist/runtime/editor/types/definitions.d.ts +2 -0
  79. 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 {
@@ -0,0 +1,15 @@
1
+ type __VLS_Props = {
2
+ label: string;
3
+ options: {
4
+ value: string;
5
+ label: string;
6
+ }[];
7
+ modelValue?: string;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: string) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <div
3
+ ref="root"
4
+ class="bk-media-library-filter-select"
5
+ :class="{ 'bk-is-open': isOpen }"
6
+ @keydown.stop="onKeydown"
7
+ >
8
+ <button type="button" @click="toggle">
9
+ <div>
10
+ <span class="bk-media-library-filter-select-label">{{ label }}</span>
11
+ <span
12
+ v-if="selectedLabel"
13
+ class="bk-media-library-filter-select-value"
14
+ >{{ selectedLabel }}</span
15
+ >
16
+ </div>
17
+ <Icon name="bk_mdi_arrow_drop_down" />
18
+ </button>
19
+ <div
20
+ v-if="isOpen"
21
+ class="bk-media-library-filter-select-dropdown bk-scrollbar-dark"
22
+ >
23
+ <div
24
+ v-if="options.length > 10"
25
+ class="bk-media-library-filter-select-search"
26
+ >
27
+ <input
28
+ ref="searchInput"
29
+ v-model="search"
30
+ class="bk-form-input bk-is-small"
31
+ type="text"
32
+ :placeholder="$t('filterSelectSearch', 'Search...')"
33
+ />
34
+ </div>
35
+ <ul ref="listEl">
36
+ <li v-for="(option, index) in filteredOptions" :key="option.value">
37
+ <button
38
+ type="button"
39
+ :class="{
40
+ 'bk-is-active': option.value === modelValue,
41
+ 'bk-is-highlighted': index === highlightedIndex
42
+ }"
43
+ @click="select(option.value)"
44
+ @mouseenter="highlightedIndex = index"
45
+ >
46
+ {{ option.label }}
47
+ </button>
48
+ </li>
49
+ <li
50
+ v-if="!filteredOptions.length"
51
+ class="bk-media-library-filter-select-empty"
52
+ >
53
+ {{ $t("filterSelectNoResults", "No results") }}
54
+ </li>
55
+ </ul>
56
+ </div>
57
+ </div>
58
+ </template>
59
+
60
+ <script setup>
61
+ import {
62
+ ref,
63
+ computed,
64
+ nextTick,
65
+ watch,
66
+ useTemplateRef,
67
+ onBeforeUnmount,
68
+ useBlokkli
69
+ } from "#imports";
70
+ import { Icon } from "#blokkli/editor/components";
71
+ const { $t } = useBlokkli();
72
+ const props = defineProps({
73
+ label: { type: String, required: true },
74
+ options: { type: Array, required: true },
75
+ modelValue: { type: String, required: false }
76
+ });
77
+ const emit = defineEmits(["update:modelValue"]);
78
+ const isOpen = ref(false);
79
+ const search = ref("");
80
+ const highlightedIndex = ref(-1);
81
+ const searchInput = useTemplateRef("searchInput");
82
+ const root = useTemplateRef("root");
83
+ const listEl = useTemplateRef("listEl");
84
+ const selectedLabel = computed(() => {
85
+ const option = props.options.find((o) => o.value === props.modelValue);
86
+ return option?.label ?? "";
87
+ });
88
+ const filteredOptions = computed(() => {
89
+ if (!search.value) {
90
+ return props.options;
91
+ }
92
+ const term = search.value.toLowerCase();
93
+ return props.options.filter((o) => o.label.toLowerCase().includes(term));
94
+ });
95
+ watch(filteredOptions, () => {
96
+ highlightedIndex.value = -1;
97
+ });
98
+ function scrollToHighlighted() {
99
+ if (!listEl.value || highlightedIndex.value < 0) {
100
+ return;
101
+ }
102
+ const buttons = listEl.value.querySelectorAll("button");
103
+ buttons[highlightedIndex.value]?.scrollIntoView({ block: "nearest" });
104
+ }
105
+ function toggle() {
106
+ isOpen.value = !isOpen.value;
107
+ if (isOpen.value) {
108
+ search.value = "";
109
+ highlightedIndex.value = -1;
110
+ nextTick(() => {
111
+ searchInput.value?.focus();
112
+ });
113
+ }
114
+ }
115
+ function select(value) {
116
+ emit("update:modelValue", value);
117
+ isOpen.value = false;
118
+ }
119
+ function onKeydown(e) {
120
+ if (!isOpen.value) {
121
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
122
+ e.preventDefault();
123
+ toggle();
124
+ }
125
+ return;
126
+ }
127
+ const options = filteredOptions.value;
128
+ switch (e.key) {
129
+ case "ArrowDown":
130
+ e.preventDefault();
131
+ highlightedIndex.value = highlightedIndex.value < options.length - 1 ? highlightedIndex.value + 1 : 0;
132
+ nextTick(scrollToHighlighted);
133
+ break;
134
+ case "ArrowUp":
135
+ e.preventDefault();
136
+ highlightedIndex.value = highlightedIndex.value > 0 ? highlightedIndex.value - 1 : options.length - 1;
137
+ nextTick(scrollToHighlighted);
138
+ break;
139
+ case "Enter": {
140
+ e.preventDefault();
141
+ const option = options[highlightedIndex.value];
142
+ if (option) {
143
+ select(option.value);
144
+ }
145
+ break;
146
+ }
147
+ case "Escape":
148
+ e.preventDefault();
149
+ isOpen.value = false;
150
+ break;
151
+ }
152
+ }
153
+ function onClickOutside(e) {
154
+ if (root.value && !root.value.contains(e.target)) {
155
+ isOpen.value = false;
156
+ }
157
+ }
158
+ watch(isOpen, (open) => {
159
+ if (open) {
160
+ document.addEventListener("click", onClickOutside, true);
161
+ } else {
162
+ document.removeEventListener("click", onClickOutside, true);
163
+ }
164
+ });
165
+ onBeforeUnmount(() => {
166
+ document.removeEventListener("click", onClickOutside, true);
167
+ });
168
+ </script>
@@ -0,0 +1,15 @@
1
+ type __VLS_Props = {
2
+ label: string;
3
+ options: {
4
+ value: string;
5
+ label: string;
6
+ }[];
7
+ modelValue?: string;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: string) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="bk bk-media-library">
2
+ <div class="bk bk-media-library bk-scrollbar-light">
3
3
  <div v-if="status === 'pending'" class="bk-loading">
4
4
  <Icon name="loader" />
5
5
  </div>
@@ -18,20 +18,12 @@
18
18
  :placeholder="filter.placeholder"
19
19
  />
20
20
  </label>
21
- <label v-else-if="filter.type === 'options'" class="bk-form-select">
22
- <div v-if="!filterValues[filter.name]">
23
- {{ filter.label }}
24
- </div>
25
- <select v-model="filterValues[filter.name]">
26
- <option
27
- v-for="option in filter.options"
28
- :key="option.value"
29
- :value="option.value"
30
- >
31
- {{ option.label }}
32
- </option>
33
- </select>
34
- </label>
21
+ <FilterSelect
22
+ v-else-if="filter.type === 'options'"
23
+ v-model="filterValues[filter.name]"
24
+ :label="filter.label"
25
+ :options="filter.options"
26
+ />
35
27
  <FormToggle
36
28
  v-else-if="filter.type === 'checkbox'"
37
29
  v-model="filterValues[filter.name]"
@@ -41,7 +33,7 @@
41
33
  </div>
42
34
  <div
43
35
  ref="listEl"
44
- class="bk-media-library-items bk-scrollbar-light"
36
+ class="bk-media-library-items"
45
37
  :class="'bk-is-' + listView"
46
38
  >
47
39
  <Sortli no-transition :get-drag-items="getDragItems" :build-item>
@@ -85,6 +77,7 @@ import {
85
77
  FormToggle
86
78
  } from "#blokkli/editor/components";
87
79
  import Item from "./Item.vue";
80
+ import FilterSelect from "./FilterSelect/index.vue";
88
81
  import { falsy } from "#blokkli/helpers";
89
82
  import { onBlokkliEvent } from "#blokkli/editor/composables";
90
83
  defineProps({
@@ -139,6 +132,7 @@ const toggleListView = () => {
139
132
  listView.value = listView.value === "grid" ? "horizontal" : "grid";
140
133
  };
141
134
  const filterValues = ref({});
135
+ const defaultsApplied = ref(false);
142
136
  watch(key, () => {
143
137
  page.value = 0;
144
138
  });
@@ -162,6 +156,17 @@ const items = computed(() => data.value?.items || []);
162
156
  const filters = computed(() => {
163
157
  return data.value?.filters ?? [];
164
158
  });
159
+ watch(filters, (newFilters) => {
160
+ if (defaultsApplied.value || !newFilters.length) {
161
+ return;
162
+ }
163
+ defaultsApplied.value = true;
164
+ for (const filter of newFilters) {
165
+ if ("defaultValue" in filter && filter.defaultValue !== void 0 && filterValues.value[filter.name] === void 0) {
166
+ filterValues.value[filter.name] = filter.defaultValue;
167
+ }
168
+ }
169
+ });
165
170
  const firstSelectedBundle = computed(() => {
166
171
  if (selected.value.length) {
167
172
  const item = items.value.find((v) => v.mediaId === selected.value[0]);
@@ -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;