PluginProbe
Extendify / 2.2.2
Extendify v2.2.2
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 0.7.0 All 126 releases
extendify / src / Launch / api / WPApi.js

WPApi.js in Extendify 2.2.2, at src/Launch/api/WPApi.js

538 lines 14.4 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 { createBlock, parse, serialize } from '@wordpress/blocks';
3 import { __ } from '@wordpress/i18n';
4 import { addQueryArgs } from '@wordpress/url';
5 import { pageNames } from '@shared/lib/pages';
6 import { sleep } from '@shared/lib/utils';
7 import { Axios as api } from '@launch/api/axios';
8 import {
9 fetchFontFaceFile,
10 makeFontFamilyFormData,
11 makeFontFaceFormData,
12 } from '@launch/lib/fonts-helpers';
13
14 const { wpRoot } = window.extOnbData;
15
16 export const updateOption = (option, value) =>
17 api.post('launch/options', { option, value });
18
19 export const updatePattern = (option, value) =>
20 api.post('launch/save-pattern', { option, value });
21
22 export const getOption = async (option) => {
23 const { data } = await api.get('launch/options', {
24 params: { option },
25 });
26 return data;
27 };
28
29 export const createPage = (pageData) =>
30 api.post(`${wpRoot}wp/v2/pages`, pageData);
31
32 export const updatePage = (pageData) =>
33 api.post(`${wpRoot}wp/v2/pages/${pageData.id}`, pageData);
34
35 export const getPageById = (pageId) =>
36 api.get(`${wpRoot}wp/v2/pages/${pageId}`);
37
38 export const createPost = (postData) =>
39 api.post(`${wpRoot}wp/v2/posts`, postData);
40
41 export const uploadMedia = (formData) =>
42 api.post(`${wpRoot}wp/v2/media`, formData);
43
44 export const createCategory = (CategoryData) =>
45 api.post(`${wpRoot}wp/v2/categories`, CategoryData);
46
47 export const createTag = (tagData) => api.post(`${wpRoot}wp/v2/tags`, tagData);
48
49 export const createNavigation = async (
50 content = '',
51 title = __('Header Navigation', 'extendify-local'),
52 slug = 'site-navigation',
53 ) => {
54 const payload = await apiFetch({
55 path: 'extendify/v1/launch/create-navigation',
56 method: 'POST',
57 data: {
58 title,
59 slug,
60 content,
61 },
62 });
63
64 return payload.id;
65 };
66
67 export const updateNavigation = async (id, content) => {
68 const payload = await apiFetch({
69 path: `wp/v2/navigation/${id}`,
70 method: 'POST',
71 data: {
72 content,
73 },
74 });
75
76 return payload.id;
77 };
78
79 export const updateTemplatePart = (part, content) =>
80 api.post(`${wpRoot}wp/v2/template-parts/${part}`, {
81 slug: `${part}`,
82 theme: 'extendable',
83 type: 'wp_template_part',
84 status: 'publish',
85 // See: https://github.com/extendify/company-product/issues/833#issuecomment-1804179527
86 // translators: Launch is the product name. Unless otherwise specified by the glossary, do not translate this name.
87 description: __('Added by Launch', 'extendify-local'),
88 content,
89 });
90
91 const allowedHeaders = ['header', 'header-with-center-nav-and-social'];
92 const allowedFooters = [
93 'footer',
94 'footer-social-icons',
95 'footer-with-center-logo-and-menu',
96 ];
97 const allowedNavFooters = [
98 'footer-with-nav',
99 'footer-with-center-logo-social-nav',
100 ];
101
102 // finds the core/heading in the pattern and replaces it with a core/post-title block
103 const transformHeadingToPostTitle = (rawHTML) => {
104 let done = false;
105
106 const walk = (block) => {
107 if (done) return block;
108
109 if (block.name === 'core/heading') {
110 done = true;
111 const attrs = {
112 level: block.attributes.level,
113 textAlign: block.attributes.textAlign,
114 textColor: block.attributes.textColor,
115 backgroundColor: block.attributes.backgroundColor,
116 isLink: block.attributes.isLink,
117 linkTarget: block.attributes.linkTarget,
118 rel: block.attributes.rel,
119 };
120
121 if (block.attributes.fontSize) {
122 attrs.fontSize = block.attributes.fontSize;
123 }
124
125 const customSize = block.attributes.style?.typography?.fontSize;
126 const linkStyle = block.attributes.style?.elements?.link;
127
128 if (customSize || linkStyle) {
129 attrs.style = {};
130
131 if (customSize) {
132 attrs.style.typography = { fontSize: customSize };
133 }
134 if (linkStyle) {
135 attrs.style.elements = { link: linkStyle };
136 }
137 }
138
139 return createBlock('core/post-title', attrs);
140 }
141
142 if (block.innerBlocks?.length) {
143 block.innerBlocks = block.innerBlocks.map(walk);
144 }
145 return block;
146 };
147
148 return serialize(parse(rawHTML).map(walk));
149 };
150
151 // Replace the page-title pattern in “page-with-title” template with the incoming page-title pattern
152 export const updatePageTitlePattern = async (pageTitlePattern) => {
153 const updatedPattern = transformHeadingToPostTitle(pageTitlePattern);
154
155 const templateContent = `
156 <!-- wp:template-part {"slug":"header","tagName":"header"} /-->
157 <!-- wp:group {"tagName":"main","style":{"spacing":{"margin":{"top":"0px","bottom":"0px"},"blockGap":"0"}}} -->
158 <main class="wp-block-group" style="margin-top:0px;margin-bottom:0px">
159 ${updatedPattern}
160 <!-- wp:post-content {"layout":{"type":"constrained"}} /-->
161 </main>
162 <!-- /wp:group -->
163 <!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
164 `;
165
166 try {
167 await apiFetch({
168 path: '/wp/v2/templates/extendable/page-with-title',
169 method: 'POST',
170 data: {
171 slug: 'page-with-title',
172 theme: 'extendable',
173 type: 'wp_template',
174 status: 'publish',
175 description: __('Added by Launch', 'extendify-local'),
176 content: templateContent,
177 },
178 });
179 return true;
180 } catch {
181 return false;
182 }
183 };
184
185 export const getHeadersAndFooters = async (hasFooterNav = false) => {
186 let patterns = await getTemplateParts();
187 patterns = patterns?.filter((p) => p.theme === 'extendable');
188 const headers = patterns?.filter((p) => allowedHeaders.includes(p?.slug));
189
190 let footerSlugsToUse = allowedFooters;
191
192 if (hasFooterNav) {
193 const navFooters = patterns?.filter((p) =>
194 allowedNavFooters.includes(p?.slug),
195 );
196 // Use navFooters only if any are found; otherwise fall back to allowedFooters
197 if (navFooters.length > 0) {
198 footerSlugsToUse = allowedNavFooters;
199 }
200 }
201
202 const footers = patterns?.filter((p) => footerSlugsToUse.includes(p?.slug));
203 return { headers, footers };
204 };
205
206 const getTemplateParts = () => api.get(wpRoot + 'wp/v2/template-parts');
207
208 export const getThemeVariations = async () => {
209 const variations = await api.get(
210 wpRoot + 'wp/v2/global-styles/themes/extendable/variations',
211 );
212
213 if (!Array.isArray(variations)) {
214 throw new Error('Could not get theme variations');
215 }
216
217 // Filter out color and typography presets, and keep only main style variations.
218 const mainStyleVariations = variations.filter((variation) => {
219 const settingsKeys = Object.keys(variation.settings || {});
220 const stylesKeys = Object.keys(variation.styles || {});
221 const combinedKeys = new Set([...settingsKeys, ...stylesKeys]);
222 return combinedKeys.has('color') && combinedKeys.has('typography');
223 });
224
225 // Use slug from theme if available, otherwise generate one from the title
226 const variationsWithSlugs = mainStyleVariations.map((variation) => {
227 if (variation.slug) return variation;
228 const slug = variation.title.toLowerCase().trim().replace(/\s+/, '-');
229 return { ...variation, slug };
230 });
231
232 // Randomize
233 return [...variationsWithSlugs].sort(() => Math.random() - 0.5);
234 };
235
236 export const updateThemeVariation = (id, variation) =>
237 api.post(`${wpRoot}wp/v2/global-styles/${id}`, {
238 id,
239 settings: variation.settings,
240 styles: variation.styles,
241 });
242
243 export const addSectionLinksToNav = async (
244 navigationId,
245 homePatterns = [],
246 pluginPages = [],
247 createdPages = [],
248 ) => {
249 // Extract plugin page slugs for comparison
250 const pluginPageTitles = pluginPages.map(({ title }) =>
251 title?.rendered?.toLowerCase(),
252 );
253
254 const pages =
255 createdPages
256 ?.filter((page) => page?.slug !== 'home')
257 ?.map((page) => page.slug)
258 ?.filter(Boolean) ?? [];
259
260 // ['about-us', 'services', 'contact-us']
261 const sections = homePatterns
262 .map(({ patternTypes }) => patternTypes?.[0])
263 .filter(Boolean)
264 // Filter out any pattern type that has a page created by 3rd party plugins.
265 .filter((patternType) => {
266 const { slug } =
267 Object.values(pageNames).find(({ alias }) =>
268 alias.includes(patternType),
269 ) || {};
270 return slug && !pluginPageTitles.includes(slug);
271 });
272
273 const seen = new Set();
274
275 const sectionsNavigationLinks = sections.map((patternType) => {
276 const { title, slug } =
277 Object.values(pageNames).find(({ alias }) =>
278 alias.includes(patternType),
279 ) || {};
280 if (!slug) return '';
281 if (seen.has(slug)) return '';
282 seen.add(slug);
283
284 const url = pages.includes(slug)
285 ? `${window.extSharedData.homeUrl}/${slug}`
286 : `${window.extSharedData.homeUrl}/#${slug}`;
287
288 const attributes = JSON.stringify({
289 label: title,
290 type: 'custom',
291 url,
292 kind: 'custom',
293 isTopLevelLink: true,
294 });
295
296 return `<!-- wp:navigation-link ${attributes} /-->`;
297 });
298
299 const pluginPagesNavigationLinks = pluginPages.map(
300 ({ title, id, type, link }) => {
301 const attributes = JSON.stringify({
302 label: title.rendered,
303 id,
304 type,
305 url: link,
306 kind: id ? 'post-type' : 'custom',
307 isTopLevelLink: true,
308 });
309
310 return `<!-- wp:navigation-link ${attributes} /-->`;
311 },
312 );
313
314 const navigationLinks = sectionsNavigationLinks
315 .concat(pluginPagesNavigationLinks)
316 .join('');
317
318 await updateNavigation(navigationId, navigationLinks);
319 };
320
321 export const addPageLinksToNav = async (
322 navigationId,
323 allPages,
324 createdPages,
325 pluginPages = [],
326 ) => {
327 // Because WP may have changed the slug and permalink (i.e., because of different languages),
328 // we are using the `originalSlug` property to match the original pages with the updated ones.
329 const findCreatedPage = ({ slug }) =>
330 createdPages.find(({ originalSlug: s }) => s === slug) || {};
331
332 const filteredCreatedPages = allPages
333 .filter((p) => findCreatedPage(p)?.id) // make sure its a page
334 .filter(({ slug }) => slug !== 'home') // exclude home page
335 .map((page) => findCreatedPage(page));
336
337 const pageLinks = filteredCreatedPages
338 .concat(pluginPages)
339 .map(({ id, title, link, type }) => {
340 const attributes = JSON.stringify({
341 label: title.rendered,
342 id,
343 type,
344 url: link,
345 kind: id ? 'post-type' : 'custom',
346 isTopLevelLink: true,
347 });
348
349 return `<!-- wp:navigation-link ${attributes} /-->`;
350 });
351
352 const topLevelLinks = pageLinks.slice(0, 5).join('');
353 const submenuLinks = pageLinks.slice(5);
354 // We want a max of 6 top-level links, but if 7+, then move the last
355 // two+ to a submenu.
356 const additionalLinks =
357 submenuLinks.length > 1
358 ? ` <!-- wp:navigation-submenu ${JSON.stringify({
359 // translators: "More" here is used for a navigation menu item that contains additional links.
360 label: __('More', 'extendify-local'),
361 url: '#',
362 kind: 'custom',
363 })} --> ${submenuLinks.join('')} <!-- /wp:navigation-submenu -->`
364 : submenuLinks.join(''); // only 1 link here
365
366 await updateNavigation(navigationId, topLevelLinks + additionalLinks);
367 };
368
369 const getNavAttributes = (headerCode) => {
370 try {
371 return JSON.parse(headerCode.match(/<!-- wp:navigation([\s\S]*?)-->/)[1]);
372 } catch (e) {
373 return {};
374 }
375 };
376
377 export const updateNavAttributes = (headerCode, attributes) => {
378 const newAttributes = JSON.stringify({
379 ...getNavAttributes(headerCode),
380 ...attributes,
381 });
382 return headerCode.replace(
383 /(<!--\s*wp:navigation\b[^>]*>)([^]*?)(<!--\s*\/wp:navigation\s*-->)/gi,
384 `<!-- wp:navigation ${newAttributes} /-->`,
385 );
386 };
387
388 export const getActivePlugins = () => api.get('launch/active-plugins');
389
390 export const prefetchAssistData = async () =>
391 await api.get('launch/prefetch-assist-data');
392
393 export const processPlaceholders = (patterns) =>
394 apiFetch({
395 path: '/extendify/v1/shared/process-placeholders',
396 method: 'POST',
397 data: { patterns },
398 });
399
400 export const postLaunchFunctions = () =>
401 apiFetch({
402 path: '/extendify/v1/launch/post-launch-functions',
403 method: 'POST',
404 });
405
406 export const registerFontFamily = async (fontFamily) => {
407 try {
408 const existingFontFamily = (
409 await apiFetch({
410 path: addQueryArgs('/wp/v2/font-families', {
411 slug: fontFamily.slug,
412 _embed: true,
413 }),
414 method: 'GET',
415 })
416 )?.[0];
417
418 if (existingFontFamily) {
419 return {
420 id: existingFontFamily.id,
421 ...existingFontFamily.font_family_settings,
422 fontFace: existingFontFamily._embedded.font_faces.map(
423 ({ id, font_face_settings }) => ({
424 id,
425 ...font_face_settings,
426 }),
427 ),
428 };
429 }
430
431 const newFontFamily = await apiFetch({
432 path: '/wp/v2/font-families',
433 method: 'POST',
434 body: makeFontFamilyFormData(fontFamily),
435 });
436
437 return {
438 id: newFontFamily.id,
439 ...newFontFamily.font_family_settings,
440 fontFace: newFontFamily.fontFaces,
441 };
442 } catch (error) {
443 console.error('Failed to register font family:', error.message);
444 return;
445 }
446 };
447
448 export const registerFontFace = async ({ fontFamilyId, ...fontFace }) => {
449 const max_retries = 2;
450
451 const fontFaceSlug = `${fontFace.fontFamilySlug}-${fontFace.fontWeight}`;
452
453 for (let attempt = 0; attempt <= max_retries; attempt++) {
454 try {
455 // Add delay of 1 second if this is not the first attempt
456 if (attempt > 0) await sleep(1000);
457
458 const response = await apiFetch({
459 path: `/wp/v2/font-families/${fontFamilyId}/font-faces`,
460 method: 'POST',
461 body: makeFontFaceFormData(fontFace),
462 });
463
464 return {
465 id: response.id,
466 ...response.font_face_settings,
467 };
468 } catch (error) {
469 if (attempt <= max_retries) {
470 console.error(
471 `Failed attempt to upload font file ${fontFaceSlug}:`,
472 error.message,
473 );
474 continue;
475 }
476
477 console.error(
478 `Failed to upload font file ${fontFaceSlug} after ${max_retries + 1} attempts.`,
479 );
480
481 return;
482 }
483 }
484 };
485
486 export const installFontFamily = async (fontFamily) => {
487 const fontFaceDownloadRequests = fontFamily.fontFace.map(async (fontFace) => {
488 const file = await fetchFontFaceFile(fontFace.src);
489 if (!file) return;
490 return { ...fontFace, file };
491 });
492
493 const fontFacesWithFile = (
494 await Promise.all(fontFaceDownloadRequests)
495 ).filter(Boolean);
496
497 // If we don't have any font file to install, we don't register the font family.
498 if (!fontFacesWithFile.length) return;
499
500 const registeredFontFamily = await registerFontFamily(fontFamily);
501
502 // If we couldn't register the font family, we don't register the font faces.
503 if (!registeredFontFamily) return;
504
505 // If font family has font faces, it means it was already registered
506 // and doesn't need to be installed.
507 if (registeredFontFamily?.fontFace?.length) {
508 return registeredFontFamily;
509 }
510
511 const fontFaces = fontFacesWithFile.map((fontFace) => ({
512 fontFamilyId: registeredFontFamily.id,
513 fontFamilySlug: registeredFontFamily.slug,
514 ...fontFace,
515 }));
516
517 const registeredFontFaces = [];
518
519 for (const fontFace of fontFaces) {
520 registeredFontFaces.push(await registerFontFace(fontFace));
521 }
522
523 return {
524 ...registeredFontFamily,
525 fontFace: registeredFontFaces.filter(Boolean),
526 };
527 };
528
529 export const installFontFamilies = async (fontFamilies) => {
530 const installedFontFamilies = [];
531
532 for (const fontFamily of fontFamilies) {
533 installedFontFamilies.push(await installFontFamily(fontFamily));
534 }
535
536 return installedFontFamilies.filter(Boolean);
537 };
538