PluginProbe
Extendify / 1.17.1
Extendify v1.17.1
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 / Launch / api / WPApi.js

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

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