PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | src/Agent/workflows/block-selector/components/UpdateBlockConfirm.jsx +504 -61 3.1.23.2.1 View file →
@@ -1,12 +1,420 @@
1 +import { SharedBlockNotice } from '@agent/components/SharedBlockNotice';
2 +import { fetchBlockCodeById } from '@agent/lib/block-code';
3 +import {
4 + BLOCK_ID_SEL,
5 + blockIdOf,
6 + findBlockEl,
7 + idAttrOf,
8 + parseScopedId,
9 + scopeOf,
10 +} from '@agent/lib/block-el';
11 +import { applyBlockPatch } from '@agent/lib/block-patch';
12 +import { processCustomCss } from '@agent/lib/custom-css';
13 +import { resolveDeleteTarget } from '@agent/lib/delete-target';
14 +import { buildNewBlock } from '@agent/lib/insertable-blocks';
15 +import { SETTING_TEXT_BLOCKS } from '@agent/lib/setting-text-blocks';
1 16 import { useQuickEditStore } from '@quick-edit/state/store';
2 17 import { patchVariantClasses } from '@shared/lib/variant-classes';
3 18 import apiFetch from '@wordpress/api-fetch';
19 +import { parse } from '@wordpress/blocks';
4 20 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
5 21 import { __ } from '@wordpress/i18n';
6 22
7 23 const dynamicClasses = ['is-style-ext-preset', 'is-style-outline'];
24 +const wpBlockAttributeClasses =
25 + /^has-([\w-]+-)?(background-color|color|font-size|gradient-background)$|^has-background$|^has-text-color$/;
26 +// Carrying stale align/layout classes renders the preview with the old layout
27 +const layoutEngineClasses =
28 + /^align(full|wide|left|right|center)$|^is-layout-|^wp-container-|-is-layout-|^is-content-justification-|^is-(vertical|horizontal|nowrap|wrap)$|^has-global-padding$/;
29 +const themeAnimationClasses = /^ext-animated?(-|$)/;
8 30
31 +// Re-set ext-animated to prevent animating while patching
32 +const pinThemeAnimations = (el) => {
33 + for (const node of [el, ...el.querySelectorAll('.ext-animate')]) {
34 + if (node.classList?.contains('ext-animate'))
35 + node.dataset.extAnimated = 'true';
36 + }
37 +};
38 +const PREVIEW_CSS_ATTR = 'data-extendify-preview-css';
39 +const PART_SLUG_ATTR = 'data-extendify-part-slug';
40 +
41 +// Without this a later op in the batch can't tell the replacement from the
42 +// same-numbered block in another part.
43 +const carryPartSlug = (from, to) => {
44 + const slug = from?.getAttribute?.(PART_SLUG_ATTR);
45 + if (slug) to?.setAttribute?.(PART_SLUG_ATTR, slug);
46 +};
47 +
48 +const cssOf = (blockCode) =>
49 + parse(blockCode)[0]?.attributes?.style?.css || null;
50 +
51 +// The wp-container-* layout rules enqueue page-side on a full render only, so
52 +// the fragment ships them and the preview injects them, tagged for teardown.
53 +const injectPreviewStylesheet = (blockId, cssText) => {
54 + if (!cssText) return;
55 + const style = document.createElement('style');
56 + style.setAttribute(PREVIEW_CSS_ATTR, blockId);
57 + style.textContent = cssText;
58 + document.head.appendChild(style);
59 +};
60 +
61 +// The block fragment ships without style.css's server rule, so inject it here,
62 +// tagged for teardown. No rule means WP discards this CSS too — show nothing.
63 +const injectPreviewCss = (el, blockId, css) => {
64 + const cls = `ext-preview-css-${blockId}`;
65 + const rule = processCustomCss(css, `.${cls}`);
66 + if (!rule) return;
67 + el.classList.add(cls);
68 + const style = document.createElement('style');
69 + style.setAttribute(PREVIEW_CSS_ATTR, blockId);
70 + style.textContent = rule;
71 + document.head.appendChild(style);
72 +};
73 +
74 +// Swap the rendered preview in for the live element. Returns the detached
75 +// original (restored on cancel), or null when the target isn't on the page.
76 +const previewBlock = async (blockId, newContent, css, scope) => {
77 + const { content, styles } = await apiFetch({
78 + path: '/extendify/v1/agent/get-block-html',
79 + method: 'POST',
80 + data: { blockCode: newContent },
81 + });
82 + const el = findBlockEl(blockId, document, scope);
83 + if (!el) return null;
84 + injectPreviewStylesheet(blockId, styles);
85 +
86 + const patched = patchVariantClasses(
87 + content,
88 + el.cloneNode(true),
89 + dynamicClasses,
90 + );
91 + const template = document.createElement('template');
92 + template.innerHTML = patched || '<div style="display:none"></div>';
93 + const newEl = template.content.firstElementChild;
94 + if (!newEl) return null;
95 +
96 + // Later ops anchor by id — the replacement and its children keep theirs;
97 + // an attribute edit preserves child structure, so ids map by position.
98 + newEl.setAttribute(idAttrOf(el), blockId);
99 + carryPartSlug(el, newEl);
100 + for (const tagged of el.querySelectorAll(BLOCK_ID_SEL)) {
101 + const path = [];
102 + for (let node = tagged; node !== el; node = node.parentElement) {
103 + if (!node.parentElement) break;
104 + path.unshift([...node.parentElement.children].indexOf(node));
105 + }
106 + const match = path.reduce((node, i) => node?.children?.[i], newEl);
107 + match?.setAttribute(idAttrOf(tagged), blockIdOf(tagged));
108 + carryPartSlug(tagged, match);
109 + }
110 + const newElClasses = new Set(newEl.classList);
111 + el.classList.forEach((className) => {
112 + if (newElClasses.has(className)) return;
113 + if (wpBlockAttributeClasses.test(className)) return;
114 + if (layoutEngineClasses.test(className)) return;
115 + if (themeAnimationClasses.test(className)) return;
116 + newEl.classList.add(className);
117 + });
118 + // The custom-CSS hash class points at a stale/absent server rule — drop it;
119 + // injectPreviewCss applies the current css.
120 + for (const className of [...newEl.classList]) {
121 + if (
122 + className === 'has-custom-css' ||
123 + className.startsWith('wp-custom-css-')
124 + )
125 + newEl.classList.remove(className);
126 + }
127 + if (css) injectPreviewCss(newEl, blockId, css);
128 + newEl.setAttribute('data-extendify-temp-replacement', blockId);
129 + // ext-animate--on sets opacity:0 and won't re-run on a replaced node, so the
130 + // preview would stay invisible — strip it.
131 + for (const node of [newEl, ...newEl.querySelectorAll('.ext-animate--on')]) {
132 + node.classList.remove('ext-animate--on');
133 + }
134 + el.parentNode.insertBefore(newEl, el.nextSibling);
135 + el.parentNode.removeChild(el);
136 + return el;
137 +};
138 +
139 +// Relocate the live node; a hidden marker holds its old slot so undo can put it back.
140 +const previewMove = ({ blockId, targetId, position }, scope) => {
141 + const el = findBlockEl(blockId, document, scope);
142 + const target = findBlockEl(targetId, document, scope);
143 + if (!el || !target) return null;
144 + const marker = document.createElement('div');
145 + marker.style.display = 'none';
146 + marker.setAttribute('data-extendify-temp-replacement', blockId);
147 + el.parentNode.insertBefore(marker, el);
148 + pinThemeAnimations(el);
149 + target.parentNode.insertBefore(
150 + el,
151 + position === 'after' ? target.nextSibling : target,
152 + );
153 + return el;
154 +};
155 +
156 +const renderAddedEl = async (block, index) => {
157 + const { content, styles } = await apiFetch({
158 + path: '/extendify/v1/agent/get-block-html',
159 + method: 'POST',
160 + data: { blockCode: block },
161 + });
162 + if (!content) return null;
163 + injectPreviewStylesheet(`add-${index}`, styles);
164 + const template = document.createElement('template');
165 + template.innerHTML = content;
166 + const newEl = template.content.firstElementChild;
167 + if (!newEl) return null;
168 + newEl.setAttribute('data-extendify-temp-addition', '');
169 + const css = cssOf(block);
170 + if (css) injectPreviewCss(newEl, `add-${index}`, css);
171 + for (const node of [newEl, ...newEl.querySelectorAll('.ext-animate--on')]) {
172 + node.classList.remove('ext-animate--on');
173 + }
174 + return newEl;
175 +};
176 +
177 +// Render the new block and slot it next to its anchor. Nothing detaches —
178 +// returns true so the caller counts it rendered; undo just removes the node.
179 +const previewAdd = async ({ anchorId, position, block }, index, scope) => {
180 + const anchor = findBlockEl(anchorId, document, scope);
181 + if (!anchor) return null;
182 + const newEl = await renderAddedEl(block, index);
183 + if (!newEl) return null;
184 + anchor.parentNode.insertBefore(
185 + newEl,
186 + position === 'after' ? anchor.nextSibling : anchor,
187 + );
188 + return true;
189 +};
190 +
191 +// Mirror the server's column routing off the DOM (spliceColumn owns the why).
192 +const previewColumnAdd = async (
193 + { anchorId, position, block },
194 + index,
195 + wrappers,
196 + scope,
197 +) => {
198 + const anchor = findBlockEl(anchorId, document, scope);
199 + if (!anchor) return null;
200 + if (anchor.classList.contains('wp-block-column')) {
201 + return previewAdd({ anchorId, position, block }, index, scope);
202 + }
203 + const shared = wrappers.get(`${anchorId}:${position}`);
204 + if (shared) {
205 + const newEl = await renderAddedEl(block, index);
206 + if (!newEl) return null;
207 + shared.appendChild(newEl);
208 + return true;
209 + }
210 + const newEl = await renderAddedEl(
211 + `<!-- wp:columns --><div class="wp-block-columns">${block}</div><!-- /wp:columns -->`,
212 + index,
213 + );
214 + if (!newEl) return null;
215 + anchor.parentNode.insertBefore(
216 + newEl,
217 + position === 'after' ? anchor.nextSibling : anchor,
218 + );
219 + wrappers.set(`${anchorId}:${position}`, newEl);
220 + return true;
221 +};
222 +
223 +// Keyed by the model-facing container word — the code supplies the
224 +// core/columns parent a bare column needs, mirroring the server templates.
225 +const WRAP_SHELLS = {
226 + 'core/column':
227 + '<!-- wp:columns --><div class="wp-block-columns"><!-- wp:column --><div class="wp-block-column"></div><!-- /wp:column --></div><!-- /wp:columns -->',
228 + 'core/group':
229 + '<!-- wp:group {"layout":{"type":"constrained"}} --><div class="wp-block-group"></div><!-- /wp:group -->',
230 +};
231 +
232 +// The relocated node keeps its block id, so a later add in the batch can
233 +// still anchor to it; a hidden marker holds its old slot for undo.
234 +const previewWrap = async ({ blockId, container }, wrappers, scope) => {
235 + const el = findBlockEl(blockId, document, scope);
236 + const shellCode = WRAP_SHELLS[container];
237 + if (!el || !shellCode) return null;
238 + // Two column wraps in one batch share one section, mirroring the save.
239 + const sharedShell =
240 + container === 'core/column' ? wrappers.get('wrap-shell') : null;
241 + if (sharedShell && !el.contains(sharedShell)) {
242 + const marker = document.createElement('div');
243 + marker.style.display = 'none';
244 + marker.setAttribute('data-extendify-temp-replacement', blockId);
245 + el.parentNode.insertBefore(marker, el);
246 + const column = document.createElement('div');
247 + column.className = 'wp-block-column';
248 + pinThemeAnimations(el);
249 + column.appendChild(el);
250 + sharedShell.appendChild(column);
251 + wrappers.set(`${blockId}:after`, sharedShell);
252 + wrappers.set(`${blockId}:before`, sharedShell);
253 + return el;
254 + }
255 + const { content } = await apiFetch({
256 + path: '/extendify/v1/agent/get-block-html',
257 + method: 'POST',
258 + data: { blockCode: shellCode },
259 + });
260 + const template = document.createElement('template');
261 + template.innerHTML = content ?? '';
262 + const shell = template.content.firstElementChild;
263 + if (!shell) return null;
264 + shell.setAttribute('data-extendify-temp-addition', '');
265 + if (container === 'core/column') {
266 + // A later column add anchored to the wrapped block joins this shell.
267 + wrappers.set(`${blockId}:after`, shell);
268 + wrappers.set(`${blockId}:before`, shell);
269 + wrappers.set('wrap-shell', shell);
270 + }
271 + const marker = document.createElement('div');
272 + marker.style.display = 'none';
273 + marker.setAttribute('data-extendify-temp-replacement', blockId);
274 + el.parentNode.insertBefore(marker, el);
275 + el.parentNode.insertBefore(shell, marker);
276 + pinThemeAnimations(el);
277 + (shell.querySelector('.wp-block-column') ?? shell).appendChild(el);
278 + return el;
279 +};
280 +
281 +// Re-rendering the markup would preview the old text — the option holds it.
282 +const previewSettingText = (blockId, text, scope) => {
283 + const el = findBlockEl(blockId, document, scope);
284 + if (!el) return null;
285 + const preview = el.cloneNode(true);
286 + const textNode = preview.querySelector('a') ?? preview;
287 + textNode.textContent = text;
288 + preview.setAttribute('data-extendify-temp-replacement', blockId);
289 + el.parentNode.insertBefore(preview, el.nextSibling);
290 + el.parentNode.removeChild(el);
291 + return el;
292 +};
293 +
294 +// Remove the target, leaving a hidden marker so cancel restores it like a swapped preview.
295 +const previewDelete = (blockId, scope) => {
296 + const el = findBlockEl(blockId, document, scope);
297 + if (!el) return null;
298 + const marker = document.createElement('div');
299 + marker.style.display = 'none';
300 + marker.setAttribute('data-extendify-temp-replacement', blockId);
301 + el.parentNode.insertBefore(marker, el.nextSibling);
302 + el.parentNode.removeChild(el);
303 + return el;
304 +};
305 +
306 +// The DOM attribute and the save both carry the bare id, not the scoped one.
307 +const unscope = (operation, fallback) => {
308 + if (!operation) return { operation, scope: fallback };
309 + const next = { ...operation };
310 + let partSlug = null;
311 + for (const field of ['blockId', 'anchorId', 'targetId']) {
312 + if (next[field] == null) continue;
313 + const parsed = parseScopedId(next[field]);
314 + if (parsed.partSlug) partSlug = parsed.partSlug;
315 + next[field] = parsed.blockId;
316 + }
317 + return { operation: next, scope: partSlug ? { partSlug } : fallback };
318 +};
319 +
320 +// Each target pairs the operation that saves with the preview that shows it.
321 +// Delete and move ids resolve off the pristine DOM before the preview
322 +// detaches anything, so preview + save agree on wrapper targets.
323 +const buildOperationTarget = async (
324 + rawOperation,
325 + block,
326 + postId,
327 + index,
328 + wrappers,
329 +) => {
330 + const { operation, scope } = unscope(rawOperation, scopeOf(block));
331 + if (operation?.op === 'add') {
332 + // Same builder the save-time tool uses, so preview and save agree.
333 + const markup = buildNewBlock(
334 + operation.blockType,
335 + operation.patch,
336 + operation.clear ?? [],
337 + window.extAgentData?.context?.presetSlugs ?? {},
338 + );
339 + return {
340 + operation,
341 + preview: () => {
342 + if (!markup) return null;
343 + const withMarkup = { ...operation, block: markup };
344 + return operation.blockType === 'core/column'
345 + ? previewColumnAdd(withMarkup, index, wrappers, scope)
346 + : previewAdd(withMarkup, index, scope);
347 + },
348 + };
349 + }
350 + if (operation?.op === 'wrap') {
351 + // Wrapping just a lone child nests the new container inside its old wrapper.
352 + const resolved = {
353 + ...operation,
354 + blockId: resolveDeleteTarget(operation.blockId, scope),
355 + };
356 + return {
357 + operation: resolved,
358 + preview: () => previewWrap(resolved, wrappers, scope),
359 + };
360 + }
361 + if (operation?.op === 'move') {
362 + const resolved = {
363 + ...operation,
364 + blockId: resolveDeleteTarget(operation.blockId, scope),
365 + };
366 + return { operation: resolved, preview: () => previewMove(resolved, scope) };
367 + }
368 + if (operation?.op === 'delete') {
369 + const resolved = {
370 + ...operation,
371 + blockId: resolveDeleteTarget(operation.blockId, scope),
372 + };
373 + return {
374 + operation: resolved,
375 + preview: () => previewDelete(resolved.blockId, scope),
376 + };
377 + }
378 + // Image swaps live in ReplaceImageConfirm; a stray one here saves as no-change.
379 + if (operation?.op === 'replace-image')
380 + return { operation, preview: () => null };
381 + const { blockId, patch, clear } = operation ?? {};
382 + if (SETTING_TEXT_BLOCKS[block?.blockType] && patch?.text != null) {
383 + return {
384 + operation,
385 + preview: () => previewSettingText(blockId, patch.text, scope),
386 + };
387 + }
388 + const newContent = applyBlockPatch(
389 + await fetchBlockCodeById(blockId, block?.source, postId),
390 + patch,
391 + clear ?? [],
392 + window.extAgentData?.context?.presetSlugs ?? {},
393 + );
394 + return {
395 + operation,
396 + preview: () =>
397 + newContent
398 + ? previewBlock(blockId, newContent, cssOf(newContent), scope)
399 + : null,
400 + };
401 +};
402 +
403 +// block-general workflows still send a whole-block newContent replace.
404 +const buildLegacyTarget = (inputs, block) => ({
405 + operation: null,
406 + preview: () =>
407 + inputs.newContent
408 + ? previewBlock(
409 + block?.id,
410 + inputs.newContent,
411 + cssOf(inputs.newContent),
412 + scopeOf(block),
413 + )
414 + : null,
415 +});
416 +
9 417 export const UpdateBlockConfirm = ({
10 418 inputs,
11 419 onConfirm,
12 420 onCancel,
@@ -13,17 +421,32 @@
13 421 onRetry,
14 422 }) => {
15 423 const block = useQuickEditStore((s) => s.agentBlock);
16 424 const [loading, setLoading] = useState(true);
17 - const detachedEl = useRef(null);
425 + const detached = useRef([]);
426 + // What actually saves — delete rewrites this to the DOM-resolved wrapper ids.
427 + const saveData = useRef(inputs);
18 428
429 + const operations = Array.isArray(inputs.operations)
430 + ? inputs.operations
431 + : null;
432 +
19 433 const undoBlockChange = useCallback(() => {
20 - const el = document.querySelector('[data-extendify-temp-replacement]');
21 - if (detachedEl.current) {
22 - el?.parentNode?.insertBefore(detachedEl.current, el);
23 - detachedEl.current = null;
434 + for (const original of detached.current) {
435 + const replacement = document.querySelector(
436 + `[data-extendify-temp-replacement="${CSS.escape(blockIdOf(original))}"]`,
437 + );
438 + pinThemeAnimations(original);
439 + replacement?.parentNode?.insertBefore(original, replacement);
440 + replacement?.remove();
24 441 }
25 - if (el) el.remove();
442 + for (const added of document.querySelectorAll(
443 + '[data-extendify-temp-addition]',
444 + ))
445 + added.remove();
446 + for (const style of document.querySelectorAll(`style[${PREVIEW_CSS_ATTR}]`))
447 + style.remove();
448 + detached.current = [];
26 449 }, []);
27 450
28 451 const confirmed = useRef(false);
29 452 useEffect(() => {
@@ -29,13 +452,13 @@
29 452 useEffect(() => {
30 453 return () => {
31 454 if (!confirmed.current) undoBlockChange();
32 455 };
33 - }, []);
456 + }, [undoBlockChange]);
34 457
35 458 const handleConfirm = async () => {
36 459 confirmed.current = true;
37 - await onConfirm({ data: inputs, shouldRefreshPage: true });
460 + await onConfirm({ data: saveData.current, shouldRefreshPage: true });
38 461 };
39 462
40 463 const handleRetry = useCallback(() => {
41 464 undoBlockChange();
@@ -41,58 +464,49 @@
41 464 undoBlockChange();
42 465 onRetry();
43 466 }, [undoBlockChange, onRetry]);
44 467
468 + // Re-renders (the staged block changes identity on page clicks) must not
469 + // inject the preview again and clobber the undo list.
470 + const previewed = useRef(false);
45 471 useEffect(() => {
46 - apiFetch({
47 - path: '/extendify/v1/agent/get-block-html',
48 - method: 'POST',
49 - data: { blockCode: inputs.newContent },
50 - }).then(({ content }) => {
51 - // Remove the highlighter
52 - window.dispatchEvent(new Event('extendify-agent:remove-block-highlight'));
53 - // hide the block
54 - const el = document.querySelector(
55 - `[data-extendify-agent-block-id="${block?.id}"]`,
56 - );
57 - // TODO: work out a way to propagate an error here
58 - if (!el && !detachedEl.current) return onCancel();
59 - if (detachedEl.current) return; // already done
60 - detachedEl.current = el;
472 + if (previewed.current) return;
473 + previewed.current = true;
474 + const run = async () => {
475 + const postId = window.extAgentData?.context?.postId;
476 + const operations = Array.isArray(inputs.operations)
477 + ? inputs.operations
478 + : null;
479 + const wrappers = new Map();
480 + const targets = operations
481 + ? await Promise.all(
482 + operations.map((operation, index) =>
483 + buildOperationTarget(operation, block, postId, index, wrappers),
484 + ),
485 + )
486 + : [buildLegacyTarget(inputs, block)];
487 + if (operations)
488 + saveData.current = {
489 + ...inputs,
490 + operations: targets.map(({ operation }) => operation),
491 + };
61 492
62 - const patched = patchVariantClasses(
63 - content,
64 - el.cloneNode(true),
65 - dynamicClasses,
66 - );
67 -
68 - const template = document.createElement('template');
69 - template.innerHTML = patched || '<div style="display:none"></div>';
70 - const newEl = template.content.firstElementChild;
71 - if (!newEl) return onCancel();
72 - const wpBlockAttributeClasses =
73 - /^has-([\w-]+-)?(background-color|color|font-size|gradient-background)$|^has-background$|^has-text-color$/;
74 - const newElClasses = new Set(newEl.classList);
75 - el.classList.forEach((className) => {
76 - if (newElClasses.has(className)) return;
77 - if (wpBlockAttributeClasses.test(className)) return;
78 - newEl.classList.add(className);
79 - });
80 - newEl.setAttribute('data-extendify-temp-replacement', true);
81 - // Strip animation classes so the preview is visible immediately.
82 - // The ext-animate--on class sets opacity:0 as initial state, but
83 - // the animation JS won't re-run on replaced DOM elements.
84 - for (const node of [
85 - newEl,
86 - ...newEl.querySelectorAll('.ext-animate--on'),
87 - ]) {
88 - node.classList.remove('ext-animate--on');
493 + const originals = [];
494 + let rendered = 0;
495 + for (const target of targets) {
496 + const original = await target.preview();
497 + if (!original) continue;
498 + rendered++;
499 + // An add preview has no original to restore — only count it.
500 + if (original !== true) originals.push(original);
89 501 }
90 - el.parentNode.insertBefore(newEl, el.nextSibling);
91 - el.parentNode?.removeChild(el);
502 + detached.current = originals;
503 + // Nothing rendered means none of the target blocks are on the page.
504 + if (!rendered) return onCancel();
92 505 setLoading(false);
93 - });
94 - }, [block, inputs, onCancel]);
506 + };
507 + run();
508 + }, [block, inputs, onCancel, operations]);
95 509
96 510 if (loading)
97 511 return (
98 512 <Wrapper>
@@ -99,17 +513,46 @@
99 513 <Content>{__('Loading...', 'extendify-local')}</Content>
100 514 </Wrapper>
101 515 );
102 516
517 + const onlyOp = (op) =>
518 + Array.isArray(inputs.operations) &&
519 + inputs.operations.every((operation) => operation?.op === op);
520 + const message = onlyOp('delete')
521 + ? __(
522 + 'The agent will remove the selected block. Please review and confirm.',
523 + 'extendify-local',
524 + )
525 + : onlyOp('move')
526 + ? __(
527 + 'The agent has rearranged the blocks in the browser. Please review and confirm.',
528 + 'extendify-local',
529 + )
530 + : onlyOp('add')
531 + ? __(
532 + 'The agent has added the new block in the browser. Please review and confirm.',
533 + 'extendify-local',
534 + )
535 + : onlyOp('wrap')
536 + ? __(
537 + 'The agent has placed the block in its new container in the browser. Please review and confirm.',
538 + 'extendify-local',
539 + )
540 + : __(
541 + 'The agent has made the changes in the browser. Please review and confirm.',
542 + 'extendify-local',
543 + );
544 +
103 545 return (
104 546 <Wrapper>
105 547 <Content>
106 - <p className="m-0 p-0 text-sm text-gray-900">
107 - {__(
108 - 'The agent has made the changes in the browser. Please review and confirm.',
109 - 'extendify-local',
110 - )}
111 - </p>
548 + <p className="m-0 p-0 text-sm text-gray-900">{message}</p>
549 + <SharedBlockNotice
550 + blockIds={[
551 + ...(operations ?? []).map((operation) => operation?.blockId),
552 + block?.id,
553 + ]}
554 + />
112 555 </Content>
113 556 <div className="flex flex-wrap justify-start gap-2 p-3">
114 557 <button
115 558 type="button"
@@ -137,9 +580,9 @@
137 580 );
138 581 };
139 582
140 583 const Wrapper = ({ children }) => (
141 - <div className="mb-4 ml-10 mr-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50 rtl:ml-2 rtl:mr-10">
584 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
142 585 {children}
143 586 </div>
144 587 );
145 588