PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/Agent/workflows/block-selector/components/UpdateBlockConfirm.jsx +258 -72 3.1.5 → 3.2.2 View file →
@@ -1,9 +1,20 @@
1 +import { SharedBlockNotice } from '@agent/components/SharedBlockNotice';
1 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';
2 11 import { applyBlockPatch } from '@agent/lib/block-patch';
3 12 import { processCustomCss } from '@agent/lib/custom-css';
4 13 import { resolveDeleteTarget } from '@agent/lib/delete-target';
14 +import { duplicateMarkup } from '@agent/lib/duplicate-block';
5 15 import { buildNewBlock } from '@agent/lib/insertable-blocks';
16 +import { SETTING_TEXT_BLOCKS } from '@agent/lib/setting-text-blocks';
6 17 import { useQuickEditStore } from '@quick-edit/state/store';
7 18 import { patchVariantClasses } from '@shared/lib/variant-classes';
8 19 import apiFetch from '@wordpress/api-fetch';
9 20 import { parse } from '@wordpress/blocks';
@@ -25,9 +36,17 @@
25 36 node.dataset.extAnimated = 'true';
26 37 }
27 38 };
28 39 const PREVIEW_CSS_ATTR = 'data-extendify-preview-css';
40 +const PART_SLUG_ATTR = 'data-extendify-part-slug';
29 41
42 +// Without this a later op in the batch can't tell the replacement from the
43 +// same-numbered block in another part.
44 +const carryPartSlug = (from, to) => {
45 + const slug = from?.getAttribute?.(PART_SLUG_ATTR);
46 + if (slug) to?.setAttribute?.(PART_SLUG_ATTR, slug);
47 +};
48 +
30 49 const cssOf = (blockCode) =>
31 50 parse(blockCode)[0]?.attributes?.style?.css || null;
32 51
33 52 // The wp-container-* layout rules enqueue page-side on a full render only, so
@@ -54,17 +73,15 @@
54 73 };
55 74
56 75 // Swap the rendered preview in for the live element. Returns the detached
57 76 // original (restored on cancel), or null when the target isn't on the page.
58 -const previewBlock = async (blockId, newContent, css) => {
77 +const previewBlock = async (blockId, newContent, css, scope) => {
59 78 const { content, styles } = await apiFetch({
60 79 path: '/extendify/v1/agent/get-block-html',
61 80 method: 'POST',
62 81 data: { blockCode: newContent },
63 82 });
64 - const el = document.querySelector(
65 - `[data-extendify-agent-block-id="${blockId}"]`,
66 - );
83 + const el = findBlockEl(blockId, document, scope);
67 84 if (!el) return null;
68 85 injectPreviewStylesheet(blockId, styles);
69 86
70 87 const patched = patchVariantClasses(
@@ -78,10 +95,11 @@
78 95 if (!newEl) return null;
79 96
80 97 // Later ops anchor by id — the replacement and its children keep theirs;
81 98 // an attribute edit preserves child structure, so ids map by position.
82 - newEl.setAttribute('data-extendify-agent-block-id', blockId);
83 - for (const tagged of el.querySelectorAll('[data-extendify-agent-block-id]')) {
99 + newEl.setAttribute(idAttrOf(el), blockId);
100 + carryPartSlug(el, newEl);
101 + for (const tagged of el.querySelectorAll(BLOCK_ID_SEL)) {
84 102 const path = [];
85 103 for (let node = tagged; node !== el; node = node.parentElement) {
86 104 if (!node.parentElement) break;
87 105 path.unshift([...node.parentElement.children].indexOf(node));
@@ -86,12 +104,10 @@
86 104 if (!node.parentElement) break;
87 105 path.unshift([...node.parentElement.children].indexOf(node));
88 106 }
89 107 const match = path.reduce((node, i) => node?.children?.[i], newEl);
90 - match?.setAttribute(
91 - 'data-extendify-agent-block-id',
92 - tagged.getAttribute('data-extendify-agent-block-id'),
93 - );
108 + match?.setAttribute(idAttrOf(tagged), blockIdOf(tagged));
109 + carryPartSlug(tagged, match);
94 110 }
95 111 const newElClasses = new Set(newEl.classList);
96 112 el.classList.forEach((className) => {
97 113 if (newElClasses.has(className)) return;
@@ -120,21 +136,22 @@
120 136 el.parentNode.removeChild(el);
121 137 return el;
122 138 };
123 139
124 -// Relocate the live node; a hidden marker holds its old slot so undo can put it back.
125 -const previewMove = ({ blockId, targetId, position }) => {
126 - const el = document.querySelector(
127 - `[data-extendify-agent-block-id="${blockId}"]`,
128 - );
129 - const target = document.querySelector(
130 - `[data-extendify-agent-block-id="${targetId}"]`,
131 - );
140 +// A hidden element still counts as a sibling and gives the next block the layout's gap.
141 +const slots = new Map();
142 +const holdSlot = (blockId, parent, before) => {
143 + const slot = document.createComment('');
144 + slots.set(blockId, slot);
145 + parent.insertBefore(slot, before);
146 +};
147 +
148 +// Relocate the live node; a slot holds its old place so undo can put it back.
149 +const previewMove = ({ blockId, targetId, position }, scope) => {
150 + const el = findBlockEl(blockId, document, scope);
151 + const target = findBlockEl(targetId, document, scope);
132 152 if (!el || !target) return null;
133 - const marker = document.createElement('div');
134 - marker.style.display = 'none';
135 - marker.setAttribute('data-extendify-temp-replacement', blockId);
136 - el.parentNode.insertBefore(marker, el);
153 + holdSlot(blockId, el.parentNode, el);
137 154 pinThemeAnimations(el);
138 155 target.parentNode.insertBefore(
139 156 el,
140 157 position === 'after' ? target.nextSibling : target,
@@ -164,12 +181,10 @@
164 181 };
165 182
166 183 // Render the new block and slot it next to its anchor. Nothing detaches —
167 184 // returns true so the caller counts it rendered; undo just removes the node.
168 -const previewAdd = async ({ anchorId, position, block }, index) => {
169 - const anchor = document.querySelector(
170 - `[data-extendify-agent-block-id="${anchorId}"]`,
171 - );
185 +const previewAdd = async ({ anchorId, position, block }, index, scope) => {
186 + const anchor = findBlockEl(anchorId, document, scope);
172 187 if (!anchor) return null;
173 188 const newEl = await renderAddedEl(block, index);
174 189 if (!newEl) return null;
175 190 anchor.parentNode.insertBefore(
@@ -178,20 +193,81 @@
178 193 );
179 194 return true;
180 195 };
181 196
197 +const TEMP_CLASSES_ATTR = 'data-extendify-temp-classes';
198 +const SUBMENU_ITEM_CLASSES = [
199 + 'has-child',
200 + 'open-on-hover-click',
201 + 'wp-block-navigation-submenu',
202 +];
203 +const SUBMENU_OPEN_CLASS = 'extendify-preview-submenu-open';
204 +// WordPress keeps a submenu hidden until hover, which would hide the new link.
205 +const SUBMENU_OPEN_CSS = `.${SUBMENU_OPEN_CLASS} > .wp-block-navigation__submenu-container { visibility: visible !important; opacity: 1 !important; height: auto !important; width: auto !important; min-width: 200px; overflow: visible !important; }`;
206 +
207 +const addTempClasses = (el, classes) => {
208 + const added = classes.filter((name) => !el.classList.contains(name));
209 + if (!added.length) return;
210 + el.classList.add(...added);
211 + const earlier = el.getAttribute(TEMP_CLASSES_ATTR);
212 + el.setAttribute(
213 + TEMP_CLASSES_ATTR,
214 + [earlier, ...added].filter(Boolean).join(' '),
215 + );
216 +};
217 +
218 +const submenuToggle = (label) => {
219 + const button = document.createElement('button');
220 + button.className =
221 + 'wp-block-navigation__submenu-icon wp-block-navigation-submenu__toggle';
222 + button.setAttribute('aria-label', `${label} submenu`);
223 + button.setAttribute('aria-expanded', 'true');
224 + button.innerHTML =
225 + '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>';
226 + return button;
227 +};
228 +
229 +// Mirrors nestInMenuItem with the markup WordPress renders for a submenu, shown open.
230 +const previewNest = async ({ anchorId, block }, index, scope) => {
231 + const anchor = findBlockEl(anchorId, document, scope);
232 + if (!anchor) return null;
233 + const newEl = await renderAddedEl(block, index);
234 + if (!newEl) return null;
235 + const existing = anchor.querySelector(
236 + ':scope > .wp-block-navigation__submenu-container',
237 + );
238 + const list = existing ?? document.createElement('ul');
239 + if (!existing) {
240 + // A sibling dropdown carries the menu's submenu colours; copy them.
241 + list.className =
242 + anchor
243 + .closest('.wp-block-navigation')
244 + ?.querySelector('.wp-block-navigation__submenu-container')?.className ??
245 + 'wp-block-navigation__submenu-container wp-block-navigation-submenu';
246 + const toggle = submenuToggle(anchor.textContent.trim());
247 + for (const el of [toggle, list]) {
248 + el.setAttribute('data-extendify-temp-addition', '');
249 + }
250 + addTempClasses(anchor, SUBMENU_ITEM_CLASSES);
251 + anchor.append(toggle, list);
252 + }
253 + list.appendChild(newEl);
254 + addTempClasses(anchor, [SUBMENU_OPEN_CLASS]);
255 + injectPreviewStylesheet(`nest-${index}`, SUBMENU_OPEN_CSS);
256 + return true;
257 +};
258 +
182 259 // Mirror the server's column routing off the DOM (spliceColumn owns the why).
183 260 const previewColumnAdd = async (
184 261 { anchorId, position, block },
185 262 index,
186 263 wrappers,
264 + scope,
187 265 ) => {
188 - const anchor = document.querySelector(
189 - `[data-extendify-agent-block-id="${anchorId}"]`,
190 - );
266 + const anchor = findBlockEl(anchorId, document, scope);
191 267 if (!anchor) return null;
192 268 if (anchor.classList.contains('wp-block-column')) {
193 - return previewAdd({ anchorId, position, block }, index);
269 + return previewAdd({ anchorId, position, block }, index, scope);
194 270 }
195 271 const shared = wrappers.get(`${anchorId}:${position}`);
196 272 if (shared) {
197 273 const newEl = await renderAddedEl(block, index);
@@ -221,13 +297,11 @@
221 297 '<!-- wp:group {"layout":{"type":"constrained"}} --><div class="wp-block-group"></div><!-- /wp:group -->',
222 298 };
223 299
224 300 // The relocated node keeps its block id, so a later add in the batch can
225 -// still anchor to it; a hidden marker holds its old slot for undo.
226 -const previewWrap = async ({ blockId, container }, wrappers) => {
227 - const el = document.querySelector(
228 - `[data-extendify-agent-block-id="${blockId}"]`,
229 - );
301 +// still anchor to it; a slot holds its old place for undo.
302 +const previewWrap = async ({ blockId, container }, wrappers, scope) => {
303 + const el = findBlockEl(blockId, document, scope);
230 304 const shellCode = WRAP_SHELLS[container];
231 305 if (!el || !shellCode) return null;
232 306 // Two column wraps in one batch share one section, mirroring the save.
233 307 const sharedShell =
@@ -232,12 +306,9 @@
232 306 // Two column wraps in one batch share one section, mirroring the save.
233 307 const sharedShell =
234 308 container === 'core/column' ? wrappers.get('wrap-shell') : null;
235 309 if (sharedShell && !el.contains(sharedShell)) {
236 - const marker = document.createElement('div');
237 - marker.style.display = 'none';
238 - marker.setAttribute('data-extendify-temp-replacement', blockId);
239 - el.parentNode.insertBefore(marker, el);
310 + holdSlot(blockId, el.parentNode, el);
240 311 const column = document.createElement('div');
241 312 column.className = 'wp-block-column';
242 313 pinThemeAnimations(el);
243 314 column.appendChild(el);
@@ -261,42 +332,110 @@
261 332 wrappers.set(`${blockId}:after`, shell);
262 333 wrappers.set(`${blockId}:before`, shell);
263 334 wrappers.set('wrap-shell', shell);
264 335 }
265 - const marker = document.createElement('div');
266 - marker.style.display = 'none';
267 - marker.setAttribute('data-extendify-temp-replacement', blockId);
268 - el.parentNode.insertBefore(marker, el);
269 - el.parentNode.insertBefore(shell, marker);
336 + el.parentNode.insertBefore(shell, el);
337 + holdSlot(blockId, el.parentNode, el);
270 338 pinThemeAnimations(el);
271 339 (shell.querySelector('.wp-block-column') ?? shell).appendChild(el);
272 340 return el;
273 341 };
274 342
275 -// Remove the target, leaving a hidden marker so cancel restores it like a swapped preview.
276 -const previewDelete = (blockId) => {
277 - const el = document.querySelector(
278 - `[data-extendify-agent-block-id="${blockId}"]`,
343 +// A fresh render numbers style variants the page has no CSS for.
344 +const renderCopyOf = async (el, markup, index) => {
345 + const { content, styles } = await apiFetch({
346 + path: '/extendify/v1/agent/get-block-html',
347 + method: 'POST',
348 + data: { blockCode: markup },
349 + });
350 + injectPreviewStylesheet(`copy-${index}`, styles);
351 + const template = document.createElement('template');
352 + template.innerHTML =
353 + patchVariantClasses(content ?? '', el.cloneNode(true), dynamicClasses) ||
354 + '';
355 + return template.content.firstElementChild;
356 +};
357 +
358 +// Id-less, or a later op in the batch could land on the copy.
359 +const previewDuplicate = async (
360 + { blockId, targetId, position, markup },
361 + index,
362 + scope,
363 +) => {
364 + const el = findBlockEl(blockId, document, scope);
365 + const anchor = targetId ? findBlockEl(targetId, document, scope) : el;
366 + if (!el || !anchor) return null;
367 + const copy = markup
368 + ? await renderCopyOf(el, markup, index)
369 + : el.cloneNode(true);
370 + if (!copy) return null;
371 + for (const node of [copy, ...copy.querySelectorAll(BLOCK_ID_SEL)]) {
372 + node.removeAttribute(idAttrOf(node));
373 + }
374 + for (const node of [copy, ...copy.querySelectorAll('.ext-animate--on')]) {
375 + node.classList.remove('ext-animate--on');
376 + }
377 + copy.setAttribute('data-extendify-temp-addition', '');
378 + anchor.parentNode.insertBefore(
379 + copy,
380 + position === 'before' ? anchor : anchor.nextSibling,
279 381 );
382 + return true;
383 +};
384 +
385 +// Re-rendering the markup would preview the old text — the option holds it.
386 +const previewSettingText = (blockId, text, scope) => {
387 + const el = findBlockEl(blockId, document, scope);
280 388 if (!el) return null;
281 - const marker = document.createElement('div');
282 - marker.style.display = 'none';
283 - marker.setAttribute('data-extendify-temp-replacement', blockId);
284 - el.parentNode.insertBefore(marker, el.nextSibling);
389 + const preview = el.cloneNode(true);
390 + const textNode = preview.querySelector('a') ?? preview;
391 + textNode.textContent = text;
392 + preview.setAttribute('data-extendify-temp-replacement', blockId);
393 + el.parentNode.insertBefore(preview, el.nextSibling);
285 394 el.parentNode.removeChild(el);
286 395 return el;
287 396 };
288 397
398 +// Remove the target, leaving a slot so cancel restores it like a swapped preview.
399 +const previewDelete = (blockId, scope) => {
400 + const el = findBlockEl(blockId, document, scope);
401 + if (!el) return null;
402 + holdSlot(blockId, el.parentNode, el);
403 + el.parentNode.removeChild(el);
404 + return el;
405 +};
406 +
407 +// The DOM attribute and the save both carry the bare id, not the scoped one.
408 +const unscope = (operation, fallback) => {
409 + if (!operation) return { operation, scope: fallback };
410 + const next = { ...operation };
411 + let partSlug = null;
412 + for (const field of ['blockId', 'anchorId', 'targetId']) {
413 + if (next[field] == null) continue;
414 + const parsed = parseScopedId(next[field]);
415 + if (parsed.partSlug) partSlug = parsed.partSlug;
416 + next[field] = parsed.blockId;
417 + }
418 + if (Array.isArray(next.texts)) {
419 + next.texts = next.texts.map((entry) => ({
420 + ...entry,
421 + blockId: parseScopedId(entry.blockId).blockId,
422 + }));
423 + }
424 + return { operation: next, scope: partSlug ? { partSlug } : fallback };
425 +};
426 +
289 427 // Each target pairs the operation that saves with the preview that shows it.
290 428 // Delete and move ids resolve off the pristine DOM before the preview
291 429 // detaches anything, so preview + save agree on wrapper targets.
292 430 const buildOperationTarget = async (
293 - operation,
431 + rawOperation,
294 432 block,
295 433 postId,
296 434 index,
297 435 wrappers,
298 436 ) => {
437 + const { operation, scope } = unscope(rawOperation, scopeOf(block));
299 438 if (operation?.op === 'add') {
300 439 // Same builder the save-time tool uses, so preview and save agree.
301 440 const markup = buildNewBlock(
302 441 operation.blockType,
@@ -308,11 +447,14 @@
308 447 operation,
309 448 preview: () => {
310 449 if (!markup) return null;
311 450 const withMarkup = { ...operation, block: markup };
451 + if (operation.position === 'inside') {
452 + return previewNest(withMarkup, index, scope);
453 + }
312 454 return operation.blockType === 'core/column'
313 - ? previewColumnAdd(withMarkup, index, wrappers)
314 - : previewAdd(withMarkup, index);
455 + ? previewColumnAdd(withMarkup, index, wrappers, scope)
456 + : previewAdd(withMarkup, index, scope);
315 457 },
316 458 };
317 459 }
318 460 if (operation?.op === 'wrap') {
@@ -318,30 +460,37 @@
318 460 if (operation?.op === 'wrap') {
319 461 // Wrapping just a lone child nests the new container inside its old wrapper.
320 462 const resolved = {
321 463 ...operation,
322 - blockId: resolveDeleteTarget(operation.blockId),
464 + blockId: resolveDeleteTarget(operation.blockId, scope),
323 465 };
324 466 return {
325 467 operation: resolved,
326 - preview: () => previewWrap(resolved, wrappers),
468 + preview: () => previewWrap(resolved, wrappers, scope),
327 469 };
328 470 }
471 + if (operation?.op === 'duplicate') {
472 + const markup = await duplicateMarkup(operation, block?.source, postId);
473 + return {
474 + operation,
475 + preview: () => previewDuplicate({ ...operation, markup }, index, scope),
476 + };
477 + }
329 478 if (operation?.op === 'move') {
330 479 const resolved = {
331 480 ...operation,
332 - blockId: resolveDeleteTarget(operation.blockId),
481 + blockId: resolveDeleteTarget(operation.blockId, scope),
333 482 };
334 - return { operation: resolved, preview: () => previewMove(resolved) };
483 + return { operation: resolved, preview: () => previewMove(resolved, scope) };
335 484 }
336 485 if (operation?.op === 'delete') {
337 486 const resolved = {
338 487 ...operation,
339 - blockId: resolveDeleteTarget(operation.blockId),
488 + blockId: resolveDeleteTarget(operation.blockId, scope),
340 489 };
341 490 return {
342 491 operation: resolved,
343 - preview: () => previewDelete(resolved.blockId),
492 + preview: () => previewDelete(resolved.blockId, scope),
344 493 };
345 494 }
346 495 // Image swaps live in ReplaceImageConfirm; a stray one here saves as no-change.
347 496 if (operation?.op === 'replace-image')
@@ -346,8 +495,14 @@
346 495 // Image swaps live in ReplaceImageConfirm; a stray one here saves as no-change.
347 496 if (operation?.op === 'replace-image')
348 497 return { operation, preview: () => null };
349 498 const { blockId, patch, clear } = operation ?? {};
499 + if (SETTING_TEXT_BLOCKS[block?.blockType] && patch?.text != null) {
500 + return {
501 + operation,
502 + preview: () => previewSettingText(blockId, patch.text, scope),
503 + };
504 + }
350 505 const newContent = applyBlockPatch(
351 506 await fetchBlockCodeById(blockId, block?.source, postId),
352 507 patch,
353 508 clear ?? [],
@@ -355,9 +510,11 @@
355 510 );
356 511 return {
357 512 operation,
358 513 preview: () =>
359 - newContent ? previewBlock(blockId, newContent, cssOf(newContent)) : null,
514 + newContent
515 + ? previewBlock(blockId, newContent, cssOf(newContent), scope)
516 + : null,
360 517 };
361 518 };
362 519
363 520 // block-general workflows still send a whole-block newContent replace.
@@ -364,9 +521,14 @@
364 521 const buildLegacyTarget = (inputs, block) => ({
365 522 operation: null,
366 523 preview: () =>
367 524 inputs.newContent
368 - ? previewBlock(block?.id, inputs.newContent, cssOf(inputs.newContent))
525 + ? previewBlock(
526 + block?.id,
527 + inputs.newContent,
528 + cssOf(inputs.newContent),
529 + scopeOf(block),
530 + )
369 531 : null,
370 532 });
371 533
372 534 export const UpdateBlockConfirm = ({
@@ -386,11 +548,14 @@
386 548 : null;
387 549
388 550 const undoBlockChange = useCallback(() => {
389 551 for (const original of detached.current) {
390 - const replacement = document.querySelector(
391 - `[data-extendify-temp-replacement="${original.getAttribute('data-extendify-agent-block-id')}"]`,
392 - );
552 + const id = blockIdOf(original);
553 + const replacement =
554 + document.querySelector(
555 + `[data-extendify-temp-replacement="${CSS.escape(id)}"]`,
556 + ) ?? slots.get(id);
557 + slots.delete(id);
393 558 pinThemeAnimations(original);
394 559 replacement?.parentNode?.insertBefore(original, replacement);
395 560 replacement?.remove();
396 561 }
@@ -397,8 +562,14 @@
397 562 for (const added of document.querySelectorAll(
398 563 '[data-extendify-temp-addition]',
399 564 ))
400 565 added.remove();
566 + for (const dressed of document.querySelectorAll(`[${TEMP_CLASSES_ATTR}]`)) {
567 + dressed.classList.remove(
568 + ...dressed.getAttribute(TEMP_CLASSES_ATTR).split(' '),
569 + );
570 + dressed.removeAttribute(TEMP_CLASSES_ATTR);
571 + }
401 572 for (const style of document.querySelectorAll(`style[${PREVIEW_CSS_ATTR}]`))
402 573 style.remove();
403 574 detached.current = [];
404 575 }, []);
@@ -403,10 +574,12 @@
403 574 detached.current = [];
404 575 }, []);
405 576
406 577 const confirmed = useRef(false);
578 + const unmounted = useRef(false);
407 579 useEffect(() => {
408 580 return () => {
581 + unmounted.current = true;
409 582 if (!confirmed.current) undoBlockChange();
410 583 };
411 584 }, [undoBlockChange]);
412 585
@@ -454,14 +627,16 @@
454 627 // An add preview has no original to restore — only count it.
455 628 if (original !== true) originals.push(original);
456 629 }
457 630 detached.current = originals;
631 + // An unmount mid-preview already ran its undo; skipping this leaves the preview stuck.
632 + if (unmounted.current) return undoBlockChange();
458 633 // Nothing rendered means none of the target blocks are on the page.
459 634 if (!rendered) return onCancel();
460 635 setLoading(false);
461 636 };
462 637 run();
463 - }, [block, inputs, onCancel, operations]);
638 + }, [block, inputs, onCancel, operations, undoBlockChange]);
464 639
465 640 if (loading)
466 641 return (
467 642 <Wrapper>
@@ -491,17 +666,28 @@
491 666 ? __(
492 667 'The agent has placed the block in its new container in the browser. Please review and confirm.',
493 668 'extendify-local',
494 669 )
495 - : __(
496 - 'The agent has made the changes in the browser. Please review and confirm.',
497 - 'extendify-local',
498 - );
670 + : onlyOp('duplicate')
671 + ? __(
672 + 'The agent has copied the block in the browser. Please review and confirm.',
673 + 'extendify-local',
674 + )
675 + : __(
676 + 'The agent has made the changes in the browser. Please review and confirm.',
677 + 'extendify-local',
678 + );
499 679
500 680 return (
501 681 <Wrapper>
502 682 <Content>
503 683 <p className="m-0 p-0 text-sm text-gray-900">{message}</p>
684 + <SharedBlockNotice
685 + blockIds={[
686 + ...(operations ?? []).map((operation) => operation?.blockId),
687 + block?.id,
688 + ]}
689 + />
504 690 </Content>
505 691 <div className="flex flex-wrap justify-start gap-2 p-3">
506 692 <button
507 693 type="button"
@@ -529,9 +715,9 @@
529 715 );
530 716 };
531 717
532 718 const Wrapper = ({ children }) => (
533 - <div className="mb-4 ms-12 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
719 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
534 720 {children}
535 721 </div>
536 722 );
537 723