← All changes
|
src/Agent/workflows/block-selector/components/UpdateBlockConfirm.jsx
+97
-46
3.1.4
→
trunk
View file →
| @@ -1,9 +1,19 @@ | ||
| 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'; |
| 5 | 14 | import { buildNewBlock } from '@agent/lib/insertable-blocks'; |
| 15 | +import { SETTING_TEXT_BLOCKS } from '@agent/lib/setting-text-blocks'; | |
| 6 | 16 | import { useQuickEditStore } from '@quick-edit/state/store'; |
| 7 | 17 | import { patchVariantClasses } from '@shared/lib/variant-classes'; |
| 8 | 18 | import apiFetch from '@wordpress/api-fetch'; |
| 9 | 19 | import { parse } from '@wordpress/blocks'; |
| @@ -25,9 +35,17 @@ | ||
| 25 | 35 | node.dataset.extAnimated = 'true'; |
| 26 | 36 | } |
| 27 | 37 | }; |
| 28 | 38 | const PREVIEW_CSS_ATTR = 'data-extendify-preview-css'; |
| 39 | +const PART_SLUG_ATTR = 'data-extendify-part-slug'; | |
| 29 | 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 | + | |
| 30 | 48 | const cssOf = (blockCode) => |
| 31 | 49 | parse(blockCode)[0]?.attributes?.style?.css || null; |
| 32 | 50 | |
| 33 | 51 | // The wp-container-* layout rules enqueue page-side on a full render only, so |
| @@ -54,17 +72,15 @@ | ||
| 54 | 72 | }; |
| 55 | 73 | |
| 56 | 74 | // Swap the rendered preview in for the live element. Returns the detached |
| 57 | 75 | // original (restored on cancel), or null when the target isn't on the page. |
| 58 | -const previewBlock = async (blockId, newContent, css) => { | |
| 76 | +const previewBlock = async (blockId, newContent, css, scope) => { | |
| 59 | 77 | const { content, styles } = await apiFetch({ |
| 60 | 78 | path: '/extendify/v1/agent/get-block-html', |
| 61 | 79 | method: 'POST', |
| 62 | 80 | data: { blockCode: newContent }, |
| 63 | 81 | }); |
| 64 | - const el = document.querySelector( | |
| 65 | - `[data-extendify-agent-block-id="${blockId}"]`, | |
| 66 | - ); | |
| 82 | + const el = findBlockEl(blockId, document, scope); | |
| 67 | 83 | if (!el) return null; |
| 68 | 84 | injectPreviewStylesheet(blockId, styles); |
| 69 | 85 | |
| 70 | 86 | const patched = patchVariantClasses( |
| @@ -78,10 +94,11 @@ | ||
| 78 | 94 | if (!newEl) return null; |
| 79 | 95 | |
| 80 | 96 | // Later ops anchor by id — the replacement and its children keep theirs; |
| 81 | 97 | // 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]')) { | |
| 98 | + newEl.setAttribute(idAttrOf(el), blockId); | |
| 99 | + carryPartSlug(el, newEl); | |
| 100 | + for (const tagged of el.querySelectorAll(BLOCK_ID_SEL)) { | |
| 84 | 101 | const path = []; |
| 85 | 102 | for (let node = tagged; node !== el; node = node.parentElement) { |
| 86 | 103 | if (!node.parentElement) break; |
| 87 | 104 | path.unshift([...node.parentElement.children].indexOf(node)); |
| @@ -86,12 +103,10 @@ | ||
| 86 | 103 | if (!node.parentElement) break; |
| 87 | 104 | path.unshift([...node.parentElement.children].indexOf(node)); |
| 88 | 105 | } |
| 89 | 106 | 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 | - ); | |
| 107 | + match?.setAttribute(idAttrOf(tagged), blockIdOf(tagged)); | |
| 108 | + carryPartSlug(tagged, match); | |
| 94 | 109 | } |
| 95 | 110 | const newElClasses = new Set(newEl.classList); |
| 96 | 111 | el.classList.forEach((className) => { |
| 97 | 112 | if (newElClasses.has(className)) return; |
| @@ -121,15 +136,11 @@ | ||
| 121 | 136 | return el; |
| 122 | 137 | }; |
| 123 | 138 | |
| 124 | 139 | // 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 | +const previewMove = ({ blockId, targetId, position }, scope) => { | |
| 141 | + const el = findBlockEl(blockId, document, scope); | |
| 142 | + const target = findBlockEl(targetId, document, scope); | |
| 132 | 143 | if (!el || !target) return null; |
| 133 | 144 | const marker = document.createElement('div'); |
| 134 | 145 | marker.style.display = 'none'; |
| 135 | 146 | marker.setAttribute('data-extendify-temp-replacement', blockId); |
| @@ -164,12 +175,10 @@ | ||
| 164 | 175 | }; |
| 165 | 176 | |
| 166 | 177 | // Render the new block and slot it next to its anchor. Nothing detaches — |
| 167 | 178 | // 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 | - ); | |
| 179 | +const previewAdd = async ({ anchorId, position, block }, index, scope) => { | |
| 180 | + const anchor = findBlockEl(anchorId, document, scope); | |
| 172 | 181 | if (!anchor) return null; |
| 173 | 182 | const newEl = await renderAddedEl(block, index); |
| 174 | 183 | if (!newEl) return null; |
| 175 | 184 | anchor.parentNode.insertBefore( |
| @@ -183,15 +192,14 @@ | ||
| 183 | 192 | const previewColumnAdd = async ( |
| 184 | 193 | { anchorId, position, block }, |
| 185 | 194 | index, |
| 186 | 195 | wrappers, |
| 196 | + scope, | |
| 187 | 197 | ) => { |
| 188 | - const anchor = document.querySelector( | |
| 189 | - `[data-extendify-agent-block-id="${anchorId}"]`, | |
| 190 | - ); | |
| 198 | + const anchor = findBlockEl(anchorId, document, scope); | |
| 191 | 199 | if (!anchor) return null; |
| 192 | 200 | if (anchor.classList.contains('wp-block-column')) { |
| 193 | - return previewAdd({ anchorId, position, block }, index); | |
| 201 | + return previewAdd({ anchorId, position, block }, index, scope); | |
| 194 | 202 | } |
| 195 | 203 | const shared = wrappers.get(`${anchorId}:${position}`); |
| 196 | 204 | if (shared) { |
| 197 | 205 | const newEl = await renderAddedEl(block, index); |
| @@ -222,12 +230,10 @@ | ||
| 222 | 230 | }; |
| 223 | 231 | |
| 224 | 232 | // The relocated node keeps its block id, so a later add in the batch can |
| 225 | 233 | // 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 | - ); | |
| 234 | +const previewWrap = async ({ blockId, container }, wrappers, scope) => { | |
| 235 | + const el = findBlockEl(blockId, document, scope); | |
| 230 | 236 | const shellCode = WRAP_SHELLS[container]; |
| 231 | 237 | if (!el || !shellCode) return null; |
| 232 | 238 | // Two column wraps in one batch share one section, mirroring the save. |
| 233 | 239 | const sharedShell = |
| @@ -271,13 +277,24 @@ | ||
| 271 | 277 | (shell.querySelector('.wp-block-column') ?? shell).appendChild(el); |
| 272 | 278 | return el; |
| 273 | 279 | }; |
| 274 | 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 | + | |
| 275 | 294 | // 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}"]`, | |
| 279 | - ); | |
| 295 | +const previewDelete = (blockId, scope) => { | |
| 296 | + const el = findBlockEl(blockId, document, scope); | |
| 280 | 297 | if (!el) return null; |
| 281 | 298 | const marker = document.createElement('div'); |
| 282 | 299 | marker.style.display = 'none'; |
| 283 | 300 | marker.setAttribute('data-extendify-temp-replacement', blockId); |
| @@ -285,18 +302,33 @@ | ||
| 285 | 302 | el.parentNode.removeChild(el); |
| 286 | 303 | return el; |
| 287 | 304 | }; |
| 288 | 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 | + | |
| 289 | 320 | // Each target pairs the operation that saves with the preview that shows it. |
| 290 | 321 | // Delete and move ids resolve off the pristine DOM before the preview |
| 291 | 322 | // detaches anything, so preview + save agree on wrapper targets. |
| 292 | 323 | const buildOperationTarget = async ( |
| 293 | - operation, | |
| 324 | + rawOperation, | |
| 294 | 325 | block, |
| 295 | 326 | postId, |
| 296 | 327 | index, |
| 297 | 328 | wrappers, |
| 298 | 329 | ) => { |
| 330 | + const { operation, scope } = unscope(rawOperation, scopeOf(block)); | |
| 299 | 331 | if (operation?.op === 'add') { |
| 300 | 332 | // Same builder the save-time tool uses, so preview and save agree. |
| 301 | 333 | const markup = buildNewBlock( |
| 302 | 334 | operation.blockType, |
| @@ -309,10 +341,10 @@ | ||
| 309 | 341 | preview: () => { |
| 310 | 342 | if (!markup) return null; |
| 311 | 343 | const withMarkup = { ...operation, block: markup }; |
| 312 | 344 | return operation.blockType === 'core/column' |
| 313 | - ? previewColumnAdd(withMarkup, index, wrappers) | |
| 314 | - : previewAdd(withMarkup, index); | |
| 345 | + ? previewColumnAdd(withMarkup, index, wrappers, scope) | |
| 346 | + : previewAdd(withMarkup, index, scope); | |
| 315 | 347 | }, |
| 316 | 348 | }; |
| 317 | 349 | } |
| 318 | 350 | if (operation?.op === 'wrap') { |
| @@ -318,30 +350,30 @@ | ||
| 318 | 350 | if (operation?.op === 'wrap') { |
| 319 | 351 | // Wrapping just a lone child nests the new container inside its old wrapper. |
| 320 | 352 | const resolved = { |
| 321 | 353 | ...operation, |
| 322 | - blockId: resolveDeleteTarget(operation.blockId), | |
| 354 | + blockId: resolveDeleteTarget(operation.blockId, scope), | |
| 323 | 355 | }; |
| 324 | 356 | return { |
| 325 | 357 | operation: resolved, |
| 326 | - preview: () => previewWrap(resolved, wrappers), | |
| 358 | + preview: () => previewWrap(resolved, wrappers, scope), | |
| 327 | 359 | }; |
| 328 | 360 | } |
| 329 | 361 | if (operation?.op === 'move') { |
| 330 | 362 | const resolved = { |
| 331 | 363 | ...operation, |
| 332 | - blockId: resolveDeleteTarget(operation.blockId), | |
| 364 | + blockId: resolveDeleteTarget(operation.blockId, scope), | |
| 333 | 365 | }; |
| 334 | - return { operation: resolved, preview: () => previewMove(resolved) }; | |
| 366 | + return { operation: resolved, preview: () => previewMove(resolved, scope) }; | |
| 335 | 367 | } |
| 336 | 368 | if (operation?.op === 'delete') { |
| 337 | 369 | const resolved = { |
| 338 | 370 | ...operation, |
| 339 | - blockId: resolveDeleteTarget(operation.blockId), | |
| 371 | + blockId: resolveDeleteTarget(operation.blockId, scope), | |
| 340 | 372 | }; |
| 341 | 373 | return { |
| 342 | 374 | operation: resolved, |
| 343 | - preview: () => previewDelete(resolved.blockId), | |
| 375 | + preview: () => previewDelete(resolved.blockId, scope), | |
| 344 | 376 | }; |
| 345 | 377 | } |
| 346 | 378 | // Image swaps live in ReplaceImageConfirm; a stray one here saves as no-change. |
| 347 | 379 | if (operation?.op === 'replace-image') |
| @@ -346,8 +378,14 @@ | ||
| 346 | 378 | // Image swaps live in ReplaceImageConfirm; a stray one here saves as no-change. |
| 347 | 379 | if (operation?.op === 'replace-image') |
| 348 | 380 | return { operation, preview: () => null }; |
| 349 | 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 | + } | |
| 350 | 388 | const newContent = applyBlockPatch( |
| 351 | 389 | await fetchBlockCodeById(blockId, block?.source, postId), |
| 352 | 390 | patch, |
| 353 | 391 | clear ?? [], |
| @@ -355,9 +393,11 @@ | ||
| 355 | 393 | ); |
| 356 | 394 | return { |
| 357 | 395 | operation, |
| 358 | 396 | preview: () => |
| 359 | - newContent ? previewBlock(blockId, newContent, cssOf(newContent)) : null, | |
| 397 | + newContent | |
| 398 | + ? previewBlock(blockId, newContent, cssOf(newContent), scope) | |
| 399 | + : null, | |
| 360 | 400 | }; |
| 361 | 401 | }; |
| 362 | 402 | |
| 363 | 403 | // block-general workflows still send a whole-block newContent replace. |
| @@ -364,9 +404,14 @@ | ||
| 364 | 404 | const buildLegacyTarget = (inputs, block) => ({ |
| 365 | 405 | operation: null, |
| 366 | 406 | preview: () => |
| 367 | 407 | inputs.newContent |
| 368 | - ? previewBlock(block?.id, inputs.newContent, cssOf(inputs.newContent)) | |
| 408 | + ? previewBlock( | |
| 409 | + block?.id, | |
| 410 | + inputs.newContent, | |
| 411 | + cssOf(inputs.newContent), | |
| 412 | + scopeOf(block), | |
| 413 | + ) | |
| 369 | 414 | : null, |
| 370 | 415 | }); |
| 371 | 416 | |
| 372 | 417 | export const UpdateBlockConfirm = ({ |
| @@ -387,9 +432,9 @@ | ||
| 387 | 432 | |
| 388 | 433 | const undoBlockChange = useCallback(() => { |
| 389 | 434 | for (const original of detached.current) { |
| 390 | 435 | const replacement = document.querySelector( |
| 391 | - `[data-extendify-temp-replacement="${original.getAttribute('data-extendify-agent-block-id')}"]`, | |
| 436 | + `[data-extendify-temp-replacement="${CSS.escape(blockIdOf(original))}"]`, | |
| 392 | 437 | ); |
| 393 | 438 | pinThemeAnimations(original); |
| 394 | 439 | replacement?.parentNode?.insertBefore(original, replacement); |
| 395 | 440 | replacement?.remove(); |
| @@ -500,8 +545,14 @@ | ||
| 500 | 545 | return ( |
| 501 | 546 | <Wrapper> |
| 502 | 547 | <Content> |
| 503 | 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 | + /> | |
| 504 | 555 | </Content> |
| 505 | 556 | <div className="flex flex-wrap justify-start gap-2 p-3"> |
| 506 | 557 | <button |
| 507 | 558 | type="button" |
| @@ -529,9 +580,9 @@ | ||
| 529 | 580 | ); |
| 530 | 581 | }; |
| 531 | 582 | |
| 532 | 583 | const Wrapper = ({ children }) => ( |
| 533 | - <div className="mb-4 ms-12 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50"> | |
| 584 | + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50"> | |
| 534 | 585 | {children} |
| 535 | 586 | </div> |
| 536 | 587 | ); |
| 537 | 588 | |