PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
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
extendify / src / Agent / workflows / block-selector / components / UpdateBlockConfirm.jsx

UpdateBlockConfirm.jsx in Extendify 2.2.0, at src/Agent/workflows/block-selector/components/UpdateBlockConfirm.jsx

204 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import apiFetch from '@wordpress/api-fetch';
2 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
3 import { __ } from '@wordpress/i18n';
4 import { useWorkflowStore } from '@agent/state/workflows';
5
6 const dynamicClasses = ['is-style-outline'];
7
8 export const UpdateBlockConfirm = ({ inputs, onConfirm, onCancel }) => {
9 const { block } = useWorkflowStore();
10 const [loading, setLoading] = useState(true);
11 const detachedEl = useRef(null);
12
13 const handleConfirm = async () => {
14 await onConfirm({ data: inputs });
15 setTimeout(() => {
16 // Reload the page after a short delay to let the DOM update
17 window.location.reload();
18 }, 1500);
19 };
20
21 const handleCancel = useCallback(() => {
22 // remove the new block we added
23 const el = document.querySelector('[data-extendify-temp-replacement]');
24 // unhide the block
25 if (detachedEl.current) {
26 el?.parentNode?.insertBefore(detachedEl.current, el);
27 detachedEl.current = null;
28 }
29 if (el) el.remove();
30 onCancel();
31 }, [onCancel]);
32
33 useEffect(() => {
34 apiFetch({
35 path: '/extendify/v1/agent/get-block-html',
36 method: 'POST',
37 data: { blockCode: inputs.newContent },
38 }).then(({ content }) => {
39 // Remove the highlighter
40 window.dispatchEvent(new Event('extendify-agent:remove-block-highlight'));
41 // hide the block
42 const el = document.querySelector(
43 `[data-extendify-agent-block-id="${block.id}"]`,
44 );
45 // TODO: work out a way to propagate an error here
46 if (!el) return onCancel();
47 if (detachedEl.current) return; // already done
48 detachedEl.current = el;
49
50 const patched = patchVariantClasses(
51 content,
52 el.cloneNode(true),
53 dynamicClasses,
54 );
55
56 const template = document.createElement('template');
57 template.innerHTML = patched || '<div style="display:none"></div>';
58 const newEl = template.content.firstElementChild;
59 if (!newEl) return onCancel();
60 newEl.setAttribute('data-extendify-temp-replacement', true);
61 el.parentNode.insertBefore(newEl, el.nextSibling);
62 el.parentNode?.removeChild(el);
63 setLoading(false);
64 });
65 }, [block, inputs, onCancel]);
66
67 if (loading)
68 return (
69 <Wrapper>
70 <Content>{__('Loading...', 'extendify-local')}</Content>
71 </Wrapper>
72 );
73
74 return (
75 <Wrapper>
76 <Content>
77 <p className="m-0 p-0 text-sm text-gray-900">
78 {__(
79 'The agent has made the changes in the browser. Please review and confirm.',
80 'extendify-local',
81 )}
82 </p>
83 </Content>
84 <div className="flex justify-start gap-2 p-3">
85 <button
86 type="button"
87 className="w-full rounded border border-gray-300 bg-white p-2 text-sm text-gray-700"
88 onClick={handleCancel}>
89 {__('Cancel', 'extendify-local')}
90 </button>
91 <button
92 type="button"
93 className="w-full rounded border border-design-main bg-design-main p-2 text-sm text-white"
94 onClick={handleConfirm}>
95 {__('Save', 'extendify-local')}
96 </button>
97 </div>
98 </Wrapper>
99 );
100 };
101
102 const Wrapper = ({ children }) => (
103 <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">
104 {children}
105 </div>
106 );
107
108 const Content = ({ children }) => (
109 <div className="rounded-lg border-b border-gray-300 bg-white">
110 <div className="p-3">{children}</div>
111 </div>
112 );
113
114 // escape for regex building
115 const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
117 const getVariantNumbersInTree = (rootEl, base) => {
118 if (!rootEl) return [];
119 const re = new RegExp(`^${esc(base)}--(\\d+)$`, 'i');
120 const out = [];
121 const seen = new Set();
122 // include root + descendants
123 const all = [rootEl, ...rootEl.querySelectorAll(`[class*="${base}--"]`)];
124 for (const el of all) {
125 for (const cls of el.classList) {
126 const m = cls.match(re);
127 if (m) {
128 const n = Number(m[1]);
129 if (!seen.has(n)) {
130 seen.add(n);
131 out.push(n);
132 }
133 }
134 }
135 }
136 return out; // e.g., [3,4]
137 };
138
139 const getVariantNumbersInHtml = (html, base) => {
140 const wrapper = document.createElement('div');
141 wrapper.innerHTML = html;
142 return getVariantNumbersInTree(wrapper, base); // e.g., [1,2]
143 };
144
145 const applyVariantNumberMapToHtml = (html, base, numberMap) => {
146 if (!numberMap || !numberMap.size) return html;
147 const wrapper = document.createElement('div');
148 wrapper.innerHTML = html;
149
150 const reToken = new RegExp(`^${esc(base)}--(\\d+)$`, 'i');
151 const ensureBase = base; // e.g. "is-style-outline"
152
153 // target every element that *could* contain the class
154 const els = wrapper.querySelectorAll(`[class*="${base}--"]`);
155 els.forEach((el) => {
156 const classes = Array.from(el.classList);
157 let changed = false;
158
159 for (let i = 0; i < classes.length; i++) {
160 const m = classes[i].match(reToken);
161 if (!m) continue;
162 const oldN = Number(m[1]);
163 if (numberMap.has(oldN)) {
164 const newN = numberMap.get(oldN);
165 const nextCls = `${base}--${newN}`;
166 if (nextCls !== classes[i]) {
167 classes[i] = nextCls;
168 changed = true;
169 }
170 }
171 }
172
173 // keep the base style class too (e.g. "is-style-outline")
174 if (!classes.includes(ensureBase)) {
175 classes.push(ensureBase);
176 changed = true;
177 }
178
179 if (changed) el.className = classes.join(' ');
180 });
181
182 return wrapper.innerHTML;
183 };
184
185 /**
186 * This takes classes like "is-style-outline--N" and patches them to match the current DOM structure, since they are reordered by WP when parsing blocks.
187 */
188 const patchVariantClasses = (html, el, bases) => {
189 let out = html;
190 bases.forEach((base) => {
191 const targetNums = getVariantNumbersInTree(el, base); // e.g., [3,4]
192 const currentNums = getVariantNumbersInHtml(out, base); // e.g., [1,2]
193 if (!targetNums.length || !currentNums.length) return;
194
195 // order-preserving mapping: 1->3, 2->4, ...
196 const count = Math.min(targetNums.length, currentNums.length);
197 const map = new Map();
198 for (let i = 0; i < count; i++) map.set(currentNums[i], targetNums[i]);
199
200 out = applyVariantNumberMapToHtml(out, base, map);
201 });
202 return out;
203 };
204