PluginProbe
Extendify / trunk
Extendify vtrunk
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.3 → trunk 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