PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/Agent/workflows/theme/components/change-site-design/SelectSiteDesign.jsx +164 -62 3.0.5 → 3.2.2 View file →
@@ -1,47 +1,62 @@
1 +import { usePaletteOverride } from '@agent/hooks/usePaletteOverride';
2 +import { usePalettes } from '@agent/hooks/usePalettes';
1 3 import { useSiteVibesOverride } from '@agent/hooks/useSiteVibesOverride';
2 4 import { useSiteVibesVariations } from '@agent/hooks/useSiteVibesVariations';
3 5 import { useVariationOverride } from '@agent/hooks/useVariationOverride';
6 +import { refreshBlockHighlight } from '@agent/lib/block-highlight';
4 7 import { DesignOption } from '@agent/workflows/theme/components/change-site-design/DesignOption';
8 +import { fontsOnlyVariation } from '@agent/workflows/theme/components/change-site-design/utils/fontsOnlyVariation';
5 9 import { removeAnimationClasses } from '@agent/workflows/theme/components/change-site-design/utils/removeAnimationClasses';
10 +import { handleSiteImages } from '@auto-launch/fetchers/get-images';
11 +import { useUserSelectionStore } from '@launch/state/user-selections';
12 +import { paletteDuotone } from '@shared/lib/palette-preview';
13 +import { samplePalettes } from '@shared/lib/palettes';
14 +import { safeParseJson } from '@shared/lib/parsing';
15 +import { normalizeSiteImages } from '@shared/lib/site-images';
6 16 import apiFetch from '@wordpress/api-fetch';
7 17 import { registerCoreBlocks } from '@wordpress/block-library';
8 18 import { getBlockTypes, parse, serialize } from '@wordpress/blocks';
9 -import { Spinner } from '@wordpress/components';
10 19 import { useDispatch } from '@wordpress/data';
11 20 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
12 21 import { __ } from '@wordpress/i18n';
13 22 import classnames from 'classnames';
14 23
24 +let originalHeroElement = null;
25 +
15 26 const undoHeroSectionChange = () => {
16 - document.querySelector('.ext-hero-section-preview')?.remove();
17 -
18 - const heroSectionElement = document.querySelector('.ext-hero-section');
19 -
20 - if (heroSectionElement) heroSectionElement.style.display = 'block';
27 + const preview = document.querySelector('.ext-hero-section-preview');
28 + if (preview && originalHeroElement) {
29 + preview.replaceWith(originalHeroElement);
30 + } else {
31 + preview?.remove();
32 + }
33 + originalHeroElement = null;
21 34 };
22 35
23 36 const updateHeroSection = (content) => {
24 - document.querySelector('.ext-hero-section-preview')?.remove();
37 + const tempDiv = document.createElement('div');
38 + tempDiv.innerHTML = content;
39 + const newNode = tempDiv.firstElementChild;
40 + newNode.classList.add('ext-hero-section-preview');
25 41
42 + const existingPreview = document.querySelector('.ext-hero-section-preview');
43 + if (existingPreview) {
44 + existingPreview.replaceWith(newNode);
45 + return;
46 + }
47 +
26 48 const heroSectionElement = document.querySelector('.ext-hero-section');
27 -
28 49 if (heroSectionElement) {
29 - heroSectionElement.style.display = 'none';
50 + originalHeroElement = heroSectionElement;
51 + heroSectionElement.replaceWith(newNode);
52 + return;
30 53 }
31 54
32 55 const contentArea =
33 56 document.querySelector('.entry-content') ?? document.querySelector('main');
34 -
35 57 if (!contentArea) return;
36 -
37 - contentArea.insertAdjacentHTML('afterbegin', content);
38 -
39 - const visibleHeroSection = [
40 - ...document.querySelectorAll('.ext-hero-section'),
41 - ].find((el) => el.style.display !== 'none');
42 -
43 - visibleHeroSection?.classList.add('ext-hero-section-preview');
58 + contentArea.insertAdjacentElement('afterbegin', newNode);
44 59 };
45 60
46 61 const { context } = window.extAgentData;
47 62 const isAdmin = context?.adminPage;
@@ -47,8 +62,31 @@
47 62 const isAdmin = context?.adminPage;
48 63
49 64 const PAGE_SIZE = 5;
50 65
66 +const hasImages = ({ hero, general }) => hero.length > 0 || general.length > 0;
67 +
68 +const resolveSiteImages = async ({ storedSiteImages, siteProfile }) => {
69 + const stored = normalizeSiteImages(storedSiteImages);
70 + if (hasImages(stored)) return stored;
71 +
72 + // A browser that didn't run Launch otherwise reuses one image on every option.
73 + const localized = normalizeSiteImages(window.extSharedData.siteImages);
74 + if (hasImages(localized)) return localized;
75 +
76 + if (siteProfile) {
77 + const { siteImages } = await handleSiteImages({ siteProfile });
78 + const fetched = normalizeSiteImages(siteImages);
79 + if (hasImages(fetched)) return fetched;
80 + }
81 +
82 + const saved = await apiFetch({
83 + path: '/extendify/v1/shared/site-images',
84 + }).catch(() => null);
85 +
86 + return normalizeSiteImages(saved?.siteImages);
87 +};
88 +
51 89 export const SelectSiteDesign = ({ onConfirm, onCancel }) => {
52 90 const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
53 91 const [isLoading, setIsLoading] = useState(false);
54 92 const [isSaving, setIsSaving] = useState(false);
@@ -61,8 +99,9 @@
61 99
62 100 const [selectedColorAndFonts, setSelectedColorAndFonts] = useState();
63 101 const [selectedHeroPattern, setSelectedHeroPattern] = useState();
64 102 const [selectedVibe, setSelectedVibe] = useState();
103 + const [selectedPalette, setSelectedPalette] = useState();
65 104
66 105 const { updateSettings } = useDispatch('core/block-editor');
67 106
68 107 const injectedLinksRef = useRef([]);
@@ -89,14 +128,35 @@
89 128 });
90 129 injectedLinksRef.current = [];
91 130 };
92 131
132 + const {
133 + palettes,
134 + preferred,
135 + css: paletteCss,
136 + isLoading: isLoadingPalettes,
137 + } = usePalettes();
138 + const onPalettes = Boolean(palettes?.length);
139 + const shownPalettes = useMemo(
140 + () => samplePalettes(palettes, preferred),
141 + [palettes, preferred],
142 + );
143 + const paletteFor = (index) =>
144 + onPalettes ? shownPalettes[index % shownPalettes.length] : undefined;
145 +
93 146 const { undoChange: undoColorAndFontsChange } = useVariationOverride({
94 147 css: !isAdmin && selectedColorAndFonts?.css,
95 148 duotoneTheme:
96 - !isAdmin && selectedColorAndFonts?.settings?.color?.duotone?.theme,
149 + !isAdmin &&
150 + !onPalettes &&
151 + selectedColorAndFonts?.settings?.color?.duotone?.theme,
97 152 });
98 153
154 + const { undoChange: undoPaletteChange } = usePaletteOverride({
155 + css: (!isAdmin && paletteCss?.[selectedPalette?.slug]) || '',
156 + duotoneTheme: (!isAdmin && paletteDuotone(selectedPalette)) || null,
157 + });
158 +
99 159 const { undoChange: undoVibesChange } = useSiteVibesOverride({
100 160 css: !isAdmin && selectedVibe?.css,
101 161 slug: !isAdmin && selectedVibe?.slug,
102 162 });
@@ -104,9 +164,10 @@
104 164 const { data: vibesData, isLoading: isLoadingVibes } =
105 165 useSiteVibesVariations();
106 166
107 167 const vibes = useMemo(() => {
108 - if (isLoadingVibes) return null;
168 + // Null for a theme with no block variations, which .css would throw on.
169 + if (isLoadingVibes || !vibesData?.css) return [];
109 170
110 171 const vibes = Object.entries(vibesData.css)
111 172 .filter(([slug]) => slug !== vibesData.currentVibe)
112 173 .map(([slug, css]) => ({
@@ -111,8 +172,13 @@
111 172 .filter(([slug]) => slug !== vibesData.currentVibe)
112 173 .map(([slug, css]) => ({
113 174 slug,
114 175 css: css?.replaceAll(slug, 'natural-1'),
176 + // Css only adds, so a cloned page keeps the applied vibe's slots.
177 + previewCss: `${vibesData.resets?.[slug] ?? ''}${css ?? ''}`.replaceAll(
178 + slug,
179 + 'natural-1',
180 + ),
115 181 }))
116 182 .sort(() => Math.random() - 0.5);
117 183
118 184 return [...vibes, ...vibes.slice(0, 3)];
@@ -139,12 +205,12 @@
139 205 heroSectionElement?.querySelector('p')?.textContent ?? null;
140 206 const cta =
141 207 heroSectionElement?.querySelector('.wp-block-button__link') ?? null;
142 208 const heroPatternName =
143 - [...(heroSectionElement?.classList ?? [])]
144 - .find((className) => className.startsWith('ext-hero-section--'))
145 - ?.replace('ext-hero-section--', '') ?? null;
146 - const images = [...(heroSectionElement?.querySelectorAll('img') ?? [])]
209 + heroSectionElement?.className?.match(
210 + /ext-hero-section ext-hero-section--(\S+)/,
211 + )?.[1] ?? null;
212 + const domImages = [...(heroSectionElement?.querySelectorAll('img') ?? [])]
147 213 .filter(
148 214 (img) =>
149 215 !img.src.includes('.svg') && !img.src.includes('data:image/svg+xml'),
150 216 )
@@ -156,35 +222,54 @@
156 222 return img.src;
157 223 }
158 224 });
159 225
160 - apiFetch({
161 - path: '/extendify/v1/agent/site-design-variations',
162 - method: 'POST',
163 - data: {
164 - title,
165 - images,
166 - description,
167 - currentHeroPattern: heroPatternName,
168 - cta: {
169 - label: cta?.textContent,
170 - link: cta?.href,
226 + const { siteId } = window.extSharedData;
227 + const stored = safeParseJson(
228 + localStorage.getItem(`extendify-launch-data-${siteId}`),
229 + );
230 + const launchState = useUserSelectionStore.getState();
231 + const storedSiteImages =
232 + stored?.state?.siteImages ?? launchState?.siteImages?.siteImages ?? [];
233 + const siteProfile = stored?.state?.siteProfile ?? launchState?.siteProfile;
234 +
235 + (async () => {
236 + const siteImages = await resolveSiteImages({
237 + storedSiteImages,
238 + siteProfile,
239 + });
240 +
241 + apiFetch({
242 + path: '/extendify/v1/agent/site-design-variations',
243 + method: 'POST',
244 + data: {
245 + title,
246 + images: domImages,
247 + siteImages,
248 + postId: context?.postId,
249 + description,
250 + currentHeroPattern: heroPatternName,
251 + source: 'change-site-design-workflow',
252 + cta: {
253 + label: cta?.textContent,
254 + link: cta?.href,
255 + },
171 256 },
172 - },
173 - })
174 - .then((data) => {
175 - setHeroPatterns(data?.patterns?.flat() ?? []);
176 - setColorAndFontsVariations(
177 - [...(data?.colorAndFontsVariations ?? [])].sort(
178 - () => Math.random() - 0.5,
179 - ),
180 - );
257 + })
258 + .then((data) => {
259 + setHeroPatterns(data?.patterns?.flat() ?? []);
260 + setColorAndFontsVariations(
261 + [...(data?.colorAndFontsVariations ?? [])].sort(
262 + () => Math.random() - 0.5,
263 + ),
264 + );
181 265
182 - setBlockEditorStyles(data?.blockEditorSettings);
183 - })
184 - .finally(() => {
185 - setIsLoading(false);
186 - });
266 + setBlockEditorStyles(data?.blockEditorSettings);
267 + })
268 + .finally(() => {
269 + setIsLoading(false);
270 + });
271 + })();
187 272 }, []);
188 273
189 274 useEffect(() => {
190 275 if (blockEditorStyles) {
@@ -206,12 +291,21 @@
206 291
207 292 const undoChanges = () => {
208 293 undoHeroSectionChange();
209 294 undoColorAndFontsChange();
295 + undoPaletteChange();
210 296 undoVibesChange();
211 297 removeInjectedLinks();
298 + refreshBlockHighlight();
212 299 };
213 300
301 + const confirmed = useRef(false);
302 + useEffect(() => {
303 + return () => {
304 + if (!confirmed.current) undoChanges();
305 + };
306 + }, []);
307 +
214 308 const handleCancel = () => {
215 309 undoChanges();
216 310 onCancel();
217 311 };
@@ -219,8 +313,9 @@
219 313 const handleConfirm = async () => {
220 314 if (!selectedHeroPattern) return;
221 315
222 316 if (selectedHeroPattern.isCurrent) {
317 + confirmed.current = true;
223 318 onConfirm({
224 319 data: { postId: context?.postId },
225 320 shouldRefreshPage: false,
226 321 });
@@ -255,14 +350,18 @@
255 350 return parse(selectedHeroPattern.code)?.[0] || block;
256 351 }),
257 352 );
258 353
354 + confirmed.current = true;
259 355 onConfirm({
260 356 data: {
261 357 updatedPageBlocks,
262 358 postId,
263 359 vibeSlug: selectedVibe.slug,
264 - colorAndFontsVariation: selectedColorAndFonts,
360 + colorAndFontsVariation: onPalettes
361 + ? fontsOnlyVariation(selectedColorAndFonts)
362 + : selectedColorAndFonts,
363 + colorPalette: selectedPalette?.slug,
265 364 },
266 365 shouldRefreshPage: true,
267 366 });
268 367 } catch (error) {
@@ -271,18 +370,18 @@
271 370 setIsSaving(false);
272 371 }
273 372 };
274 373
275 - if (isLoading || isLoadingVibes) {
374 + if (isLoading || isLoadingVibes || isLoadingPalettes) {
276 375 return (
277 - <div className="flex justify-center flex-col gap-1">
278 - <Spinner />
376 + <div className="min-h-24 p-2 text-center text-sm">
377 + {__('Loading design options...', 'extendify-local')}
279 378 </div>
280 379 );
281 380 }
282 381
283 382 return (
284 - <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">
383 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
285 384 <div className="rounded-lg border-b border-gray-300 bg-white">
286 385 <div className="flex flex-col gap-4 p-3">
287 386 {currentHeroHtml && (
288 387 <DesignOption
@@ -292,8 +391,9 @@
292 391 onClick={() => {
293 392 setSelectedHeroPattern(currentDesignOption);
294 393 setSelectedColorAndFonts(null);
295 394 setSelectedVibe(null);
395 + setSelectedPalette(null);
296 396
297 397 undoChanges();
298 398 }}
299 399 />
@@ -305,11 +405,13 @@
305 405 isSelected={selectedHeroPattern?.id === heroPattern.id}
306 406 styles={{
307 407 linkStyles: heroPattern.linkStyles,
308 408 colorAndFontsVariations: colorAndFontsVariations[i].css,
309 - duotoneTheme:
310 - colorAndFontsVariations[i]?.settings?.color?.duotone?.theme,
311 - vibes: vibes[i]?.css,
409 + paletteCss: paletteCss?.[paletteFor(i)?.slug],
410 + duotoneTheme: onPalettes
411 + ? paletteDuotone(paletteFor(i))
412 + : colorAndFontsVariations[i]?.settings?.color?.duotone?.theme,
413 + vibes: vibes[i]?.previewCss,
312 414 blockSupportsCss: heroPattern.blockSupportsCss,
313 415 }}
314 416 onClick={() => {
315 417 setSelectedHeroPattern(heroPattern);
@@ -314,8 +416,9 @@
314 416 onClick={() => {
315 417 setSelectedHeroPattern(heroPattern);
316 418 setSelectedColorAndFonts(colorAndFontsVariations[i]);
317 419 setSelectedVibe(vibes[i]);
420 + setSelectedPalette(paletteFor(i));
318 421
319 422 if (!isAdmin) {
320 423 const blockSupportsCss = heroPattern.blockSupportsCss;
321 424 if (blockSupportsCss) {
@@ -330,8 +433,9 @@
330 433
331 434 injectLinkStyles(heroPattern.linkStyles);
332 435
333 436 updateHeroSection(heroPattern.renderedHtml);
437 + refreshBlockHighlight();
334 438 }
335 439 }}
336 440 />
337 441 ))}
@@ -378,13 +482,11 @@
378 482 )}
379 483 disabled={!selectedHeroPattern || isSaving}
380 484 onClick={handleConfirm}
381 485 >
382 - {isSaving ? (
383 - <Spinner className="m-0" />
384 - ) : (
385 - __('Save', 'extendify-local')
386 - )}
486 + {isSaving
487 + ? __('Saving...', 'extendify-local')
488 + : __('Save', 'extendify-local')}
387 489 </button>
388 490 </div>
389 491 </div>
390 492 );