← All changes
|
src/Agent/workflows/block-selector/tools/block-patching.js
+82
-16
3.1.4
→
trunk
View file →
| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | import { applyBlockPatch } from '@agent/lib/block-patch'; |
| 3 | 3 | import { buildNewBlock } from '@agent/lib/insertable-blocks'; |
| 4 | 4 | import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks'; |
| 5 | 5 | import { swapBlockImage } from '@agent/lib/replace-image'; |
| 6 | +import { SETTING_TEXT_BLOCKS } from '@agent/lib/setting-text-blocks'; | |
| 6 | 7 | import { useQuickEditStore } from '@quick-edit/state/store'; |
| 7 | 8 | import apiFetch from '@wordpress/api-fetch'; |
| 8 | 9 | |
| 9 | 10 | // clear lists attributes to reset — null in a patch means "no change", not "remove". |
| @@ -51,8 +52,39 @@ | ||
| 51 | 52 | wrap: ({ blockId, container }) => ({ op: 'wrap', blockId, container }), |
| 52 | 53 | add: buildAdd, |
| 53 | 54 | }; |
| 54 | 55 | |
| 56 | +const writeSetting = (data) => | |
| 57 | + apiFetch({ path: '/wp/v2/settings', method: 'POST', data }); | |
| 58 | + | |
| 59 | +// The text belongs in the option row; the rest of the patch is block markup. | |
| 60 | +const settingTextBridge = (setting) => (operation) => { | |
| 61 | + const { text, ...attrs } = operation?.patch ?? {}; | |
| 62 | + if (text == null) return null; | |
| 63 | + return { | |
| 64 | + commit: () => writeSetting({ [setting]: text }), | |
| 65 | + rest: Object.keys(attrs).length ? { ...operation, patch: attrs } : null, | |
| 66 | + }; | |
| 67 | +}; | |
| 68 | + | |
| 69 | +// Content living outside the block markup gets a per-block-type bridge, which | |
| 70 | +// returns the part of the op it didn't consume — null when it handles nothing. | |
| 71 | +// An option row has no rollback, so `commit` waits for the block save. | |
| 72 | +const CONTENT_BRIDGES = { | |
| 73 | + 'core/site-logo': { | |
| 74 | + 'replace-image': ({ image }) => | |
| 75 | + image?.id | |
| 76 | + ? { commit: () => writeSetting({ site_logo: image.id }), rest: null } | |
| 77 | + : null, | |
| 78 | + }, | |
| 79 | + ...Object.fromEntries( | |
| 80 | + Object.entries(SETTING_TEXT_BLOCKS).map(([blockType, setting]) => [ | |
| 81 | + blockType, | |
| 82 | + { edit: settingTextBridge(setting) }, | |
| 83 | + ]), | |
| 84 | + ), | |
| 85 | +}; | |
| 86 | + | |
| 55 | 87 | export default async (input) => { |
| 56 | 88 | await ensureCoreBlocksRegistered(); |
| 57 | 89 | const operations = Array.isArray(input?.operations) ? input.operations : []; |
| 58 | 90 | if (!operations.length) return { refused: true, reason: 'no-block' }; |
| @@ -57,30 +89,64 @@ | ||
| 57 | 89 | const operations = Array.isArray(input?.operations) ? input.operations : []; |
| 58 | 90 | if (!operations.length) return { refused: true, reason: 'no-block' }; |
| 59 | 91 | |
| 60 | 92 | const { agentBlock } = useQuickEditStore.getState(); |
| 61 | - // Template parts have a separate id space; refusing beats a silent no-op. | |
| 62 | - if (agentBlock?.source?.kind === 'template-part') | |
| 63 | - return { refused: true, reason: 'template-part' }; | |
| 64 | - | |
| 93 | + const source = agentBlock?.source; | |
| 65 | 94 | const { postId } = window.extAgentData?.context ?? {}; |
| 95 | + const bridges = CONTENT_BRIDGES[agentBlock?.blockType] ?? {}; | |
| 96 | + const bridged = operations.map((operation) => | |
| 97 | + bridges[operation?.op] ? bridges[operation.op](operation) : null, | |
| 98 | + ); | |
| 66 | 99 | const results = await Promise.all( |
| 67 | - operations.map((operation) => | |
| 68 | - BUILDERS[operation?.op]?.(operation, agentBlock?.source, postId), | |
| 69 | - ), | |
| 100 | + operations.map((operation, index) => { | |
| 101 | + const remaining = bridged[index] ? bridged[index].rest : operation; | |
| 102 | + return remaining | |
| 103 | + ? BUILDERS[remaining?.op]?.(remaining, source, postId) | |
| 104 | + : null; | |
| 105 | + }), | |
| 70 | 106 | ); |
| 107 | + | |
| 71 | 108 | const built = results.filter(Boolean); |
| 72 | 109 | // A no-op build must surface, or the reply claims a change that never happened. |
| 73 | 110 | const dropped = operations |
| 74 | - .filter((operation, index) => operation && !results[index]) | |
| 111 | + .filter( | |
| 112 | + (operation, index) => operation && !results[index] && !bridged[index], | |
| 113 | + ) | |
| 75 | 114 | .map(({ blockId }) => ({ blockId, reason: 'no-change' })); |
| 76 | - if (!built.length) return { refused: true, reason: 'no-op' }; | |
| 115 | + if (!built.length && !bridged.some(Boolean)) { | |
| 116 | + return { refused: true, reason: 'no-op' }; | |
| 117 | + } | |
| 77 | 118 | |
| 78 | - const { applied = [], refused = [] } = await apiFetch({ | |
| 79 | - path: '/extendify/v1/agent/update-blocks', | |
| 80 | - method: 'POST', | |
| 81 | - data: { postId, operations: built }, | |
| 82 | - }); | |
| 83 | - if (!applied.length) return { refused: true, reason: 'not-applied' }; | |
| 119 | + let applied = []; | |
| 120 | + let refused = []; | |
| 121 | + if (built.length) { | |
| 122 | + const scope = | |
| 123 | + source?.kind === 'template-part' | |
| 124 | + ? { partSlug: source.partSlug } | |
| 125 | + : { postId }; | |
| 126 | + ({ applied = [], refused = [] } = await apiFetch({ | |
| 127 | + path: '/extendify/v1/agent/update-blocks', | |
| 128 | + method: 'POST', | |
| 129 | + data: { ...scope, operations: built }, | |
| 130 | + })); | |
| 131 | + if (!applied.length) return { refused: true, reason: 'not-applied' }; | |
| 132 | + } | |
| 133 | + | |
| 134 | + // An option row has no rollback, so a bridge waits on its own op, not the batch. | |
| 135 | + const landed = new Set(applied.map(({ blockId }) => String(blockId))); | |
| 136 | + const committed = []; | |
| 137 | + for (const [index, entry] of bridged.entries()) { | |
| 138 | + if (!entry) continue; | |
| 139 | + if (results[index] && !landed.has(String(operations[index].blockId))) { | |
| 140 | + continue; | |
| 141 | + } | |
| 142 | + await entry.commit(); | |
| 143 | + committed.push({ op: operations[index].op }); | |
| 144 | + } | |
| 145 | + | |
| 84 | 146 | // Not `refused`: Agent.jsx reads any truthy `refused` — even [] — as a refusal. |
| 85 | - return { ok: true, applied, refusedOperations: [...refused, ...dropped] }; | |
| 147 | + return { | |
| 148 | + ok: true, | |
| 149 | + applied: [...committed, ...applied], | |
| 150 | + refusedOperations: [...refused, ...dropped], | |
| 151 | + }; | |
| 86 | 152 | }; |