PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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 / UpdateImageConfirm.jsx

UpdateImageConfirm.jsx in Extendify 3.1.3, at src/Agent/workflows/block-selector/components/UpdateImageConfirm.jsx

214 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { walkAndUpdateImageDetails } from '@agent/lib/blocks';
2 import { useQuickEditStore } from '@quick-edit/state/store';
3 import {
4 addCustomMediaViewsCss,
5 removeCustomMediaViewsCss,
6 } from '@shared/lib/media-views';
7 import { registerCoreBlocks } from '@wordpress/block-library';
8 import { getBlockTypes } from '@wordpress/blocks';
9 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
10 import { __, sprintf } from '@wordpress/i18n';
11 import { MediaUpload } from '@wordpress/media-utils';
12
13 const openButton = __('Open Media Library', 'extendify-local');
14
15 const normalizeImageUrl = (url) => {
16 if (!url) return '';
17 const base = url.split('?')[0].split('#')[0];
18 try {
19 return decodeURIComponent(base);
20 } catch {
21 return base;
22 }
23 };
24
25 // The rendered src often diverges from inputs.url — a srcset candidate, a
26 // re-encoded comma, or extra CDN query params — so we match on the normalized
27 // URL and, failing that, fall back to the sole image (a single-image block is
28 // unambiguous) rather than missing the swap entirely.
29 const findLiveImage = (blockId, url) => {
30 if (!blockId) return null;
31 const scope = document.querySelector(
32 `[data-extendify-agent-block-id="${blockId}"]`,
33 );
34 if (!scope) return null;
35 const images = [...scope.querySelectorAll('img')];
36 if (!images.length) return null;
37 const target = normalizeImageUrl(url);
38 const match = images.find(
39 (img) => normalizeImageUrl(img.getAttribute('src')) === target,
40 );
41 return match ?? (images.length === 1 ? images[0] : null);
42 };
43
44 export const UpdateImageConfirm = ({ inputs, onConfirm, onCancel }) => {
45 const [showConfirmation, setShowConfirmation] = useState(false);
46 const block = useQuickEditStore((s) => s.agentBlock);
47
48 // The selected attachment plus the live <img> we swapped and its original
49 // src/srcset, so the preview reverts to exactly what was there on cancel.
50 const preview = useRef(null);
51
52 const resetImagePreview = useCallback(() => {
53 const swapped = preview.current;
54 if (!swapped?.el) return;
55 swapped.el.src = swapped.src;
56 if (swapped.srcset) {
57 swapped.el.srcset = swapped.srcset;
58 } else {
59 swapped.el.removeAttribute('srcset');
60 }
61 }, []);
62
63 const confirmed = useRef(false);
64 useEffect(
65 () => () => {
66 if (!confirmed.current) resetImagePreview();
67 },
68 [resetImagePreview],
69 );
70
71 const previewImage = (image) => {
72 const liveImage = findLiveImage(block?.id, inputs.url);
73 preview.current = {
74 image,
75 el: liveImage,
76 src: liveImage?.getAttribute('src') ?? '',
77 srcset: liveImage?.getAttribute('srcset') ?? '',
78 };
79 if (liveImage) {
80 liveImage.srcset = '';
81 liveImage.src = image.url;
82 }
83 // Advance as soon as an image is chosen. The picker's single click
84 // auto-confirms (the frame closes itself), and QuickEdit's QE_INTERIOR
85 // guard keeps that click from canceling the workflow — so the frame is
86 // torn down cleanly before this unmounts the picker. The live swap is
87 // best-effort; the real change is applied server-side on confirm.
88 setShowConfirmation(true);
89 };
90
91 const handleConfirm = async () => {
92 const image = preview.current?.image;
93 if (!image) return;
94 confirmed.current = true;
95 await onConfirm({
96 data: {
97 previousContent: inputs.previousContent,
98 newContent: walkAndUpdateImageDetails(inputs, image),
99 },
100 shouldRefreshPage: true,
101 });
102 };
103
104 useEffect(() => {
105 if (getBlockTypes().length !== 0) return;
106 registerCoreBlocks();
107 }, []);
108
109 useEffect(() => {
110 // Put modal above the Agent
111 const style = document.createElement('style');
112 style.textContent = `.media-modal {
113 z-index: 999999 !important;
114 }`;
115 document.head.appendChild(style);
116 return () => style.remove();
117 }, []);
118
119 useEffect(() => {
120 addCustomMediaViewsCss();
121
122 return () => removeCustomMediaViewsCss();
123 }, []);
124
125 if (showConfirmation) {
126 return (
127 <Wrapper>
128 <Confirmation handleConfirm={handleConfirm} handleCancel={onCancel} />
129 </Wrapper>
130 );
131 }
132
133 return (
134 <Wrapper>
135 <Content>
136 <p className="m-0 p-0 text-sm text-gray-900">
137 {sprintf(
138 __(
139 'The agent has requested the media library. Press "%s" to upload or select an image.',
140 'extendify-local',
141 ),
142 openButton,
143 )}
144 </p>
145 </Content>
146 <div className="flex justify-start gap-2 p-3">
147 <button
148 type="button"
149 className="w-full rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900"
150 onClick={onCancel}
151 >
152 {__('Cancel', 'extendify-local')}
153 </button>
154 <MediaUpload
155 title={__('Select or Upload Image', 'extendify-local')}
156 onSelect={previewImage}
157 allowedTypes={['image']}
158 modalClass="image__media-modal"
159 render={({ open }) => (
160 <button
161 type="button"
162 className="w-full rounded-sm border border-design-main bg-design-main p-2 text-sm text-white"
163 onClick={open}
164 >
165 {openButton}
166 </button>
167 )}
168 />
169 </div>
170 </Wrapper>
171 );
172 };
173
174 const Wrapper = ({ children }) => (
175 <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">
176 {children}
177 </div>
178 );
179
180 const Content = ({ children }) => (
181 <div className="rounded-lg border-b border-gray-300 bg-white">
182 <div className="p-3">{children}</div>
183 </div>
184 );
185
186 const Confirmation = ({ handleConfirm, handleCancel }) => (
187 <>
188 <Content>
189 <p className="m-0 p-0 text-sm text-gray-900">
190 {__(
191 'The agent has made the changes in the browser. Please review and confirm.',
192 'extendify-local',
193 )}
194 </p>
195 </Content>
196 <div className="flex flex-wrap justify-start gap-2 p-3">
197 <button
198 type="button"
199 className="flex-1 rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900"
200 onClick={handleCancel}
201 >
202 {__('Cancel', 'extendify-local')}
203 </button>
204 <button
205 type="button"
206 className="flex-1 rounded-sm border border-design-main bg-design-main p-2 text-sm text-white"
207 onClick={handleConfirm}
208 >
209 {__('Save', 'extendify-local')}
210 </button>
211 </div>
212 </>
213 );
214