diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index e0749845788d60..c6e37d45c689ec 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -14,7 +14,8 @@ import { * Internal dependencies */ import { - fetchInstallFont, + fetchGetFontFamilyBySlug, + fetchInstallFontFamily, fetchUninstallFonts, fetchFontCollections, fetchFontCollection, @@ -26,10 +27,11 @@ import { mergeFontFamilies, loadFontFaceInBrowser, getDisplaySrcFromFontFace, - makeFormDataFromFontFamily, + makeFontFacesFormData, + makeFontFamilyFormData, + batchInstallFontFaces, } from './utils'; import { toggleFont } from './utils/toggleFont'; -import getIntersectingFontFaces from './utils/get-intersecting-font-faces'; export const FontLibraryContext = createContext( {} ); @@ -60,12 +62,19 @@ function FontLibraryProvider( { children } ) { records: libraryPosts = [], isResolving: isResolvingLibrary, hasResolved: hasResolvedLibrary, - } = useEntityRecords( 'postType', 'wp_font_family', { refreshKey } ); + } = useEntityRecords( 'postType', 'wp_font_family', { + refreshKey, + _embed: true, + } ); const libraryFonts = - ( libraryPosts || [] ).map( ( post ) => - JSON.parse( post.content.raw ) - ) || []; + ( libraryPosts || [] ).map( ( post ) => { + post.font_family_settings.fontFace = + post?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || []; + return post.font_family_settings; + } ) || []; // Global Styles (settings) font families const [ fontFamilies, setFontFamilies ] = useGlobalSetting( @@ -195,32 +204,108 @@ function FontLibraryProvider( { children } ) { async function installFont( font ) { setIsInstalling( true ); try { - // Prepare formData to install. - const formData = makeFormDataFromFontFamily( font ); + // Get the ID of the font family post, if it is already installed. + let installedFontFamily = await fetchGetFontFamilyBySlug( + font.slug + ) + .then( ( response ) => { + if ( ! response || response.length === 0 ) { + return null; + } + const fontFamilyPost = response[ 0 ]; + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; + } ) + .catch( ( e ) => { + // eslint-disable-next-line no-console + console.error( e ); + return null; + } ); + + // Otherwise, install it. + if ( ! installedFontFamily ) { + const fontFamilyFormData = makeFontFamilyFormData( font ); + // Prepare font family form data to install. + installedFontFamily = await fetchInstallFontFamily( + fontFamilyFormData + ) + .then( ( response ) => { + return { + id: response.id, + ...response.font_face_settings, + fontFace: [], + }; + } ) + .catch( ( e ) => { + throw Error( e.message ); + } ); + } + + // Filter Font Faces that have already been installed + // We determine that by comparing the fontWeight and fontStyle + font.fontFace = font.fontFace.filter( ( fontFaceToInstall ) => { + return ( + -1 === + installedFontFamily.fontFace.findIndex( + ( installedFontFace ) => { + return ( + installedFontFace.fontWeight === + fontFaceToInstall.fontWeight && + installedFontFace.fontStyle === + fontFaceToInstall.fontStyle + ); + } + ) + ); + } ); + + if ( font.fontFace.length === 0 ) { + // Looks like we're only trying to install fonts that are already installed. + // Let's not do that. + // TODO: Exit with an error message? + return { + errors: [ 'All font faces are already installed' ], + }; + } + + // Prepare font faces form data to install. + const fontFacesFormData = makeFontFacesFormData( font ); + // Install the fonts (upload the font files to the server and create the post in the database). - const response = await fetchInstallFont( formData ); - const fontsInstalled = response?.successes || []; - // Get intersecting font faces between the fonts we tried to installed and the fonts that were installed - // (to avoid activating a non installed font). - const fontToBeActivated = getIntersectingFontFaces( - fontsInstalled, - [ font ] + const response = await batchInstallFontFaces( + installedFontFamily.id, + fontFacesFormData ); - // Activate the font families (add the font families to the global styles). - activateCustomFontFamilies( fontToBeActivated ); + + const fontFacesInstalled = response?.successes || []; + + // Rebuild fontFace settings + font.fontFace = + fontFacesInstalled.map( ( face ) => { + return face.font_face_settings; + } ) || []; + + // Activate the font family (add the font family to the global styles). + activateCustomFontFamilies( [ font ] ); // Save the global styles to the database. saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [ 'settings.typography.fontFamilies', ] ); refreshLibrary(); - setIsInstalling( false ); return response; } catch ( error ) { - setIsInstalling( false ); return { errors: [ error ], }; + } finally { + setIsInstalling( false ); } } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 2e7f413a6fa45b..08e37dc7ee95fb 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -7,7 +7,7 @@ */ import apiFetch from '@wordpress/api-fetch'; -export async function fetchInstallFont( data ) { +export async function fetchInstallFontFamily( data ) { const config = { path: '/wp/v2/font-families', method: 'POST', @@ -16,6 +16,23 @@ export async function fetchInstallFont( data ) { return apiFetch( config ); } +export async function fetchInstallFontFace( fontFamilyId, data ) { + const config = { + path: `/wp/v2/font-families/${ fontFamilyId }/font-faces`, + method: 'POST', + body: data, + }; + return apiFetch( config ); +} + +export async function fetchGetFontFamilyBySlug( slug ) { + const config = { + path: `/wp/v2/font-families?slug=${ slug }&_embed=true`, + method: 'GET', + }; + return apiFetch( config ); +} + export async function fetchUninstallFonts( fonts ) { const data = { font_families: fonts, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 0aa0f7edb4aec9..98b6375740e5b4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -8,6 +8,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; import { unlock } from '../../../../lock-unlock'; +import { fetchInstallFontFace } from '../resolvers'; /** * Browser dependencies @@ -135,39 +136,84 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } -export function makeFormDataFromFontFamily( fontFamily ) { +export function makeFontFamilyFormData( fontFamily ) { const formData = new FormData(); const { kebabCase } = unlock( componentsPrivateApis ); - const newFontFamily = { - ...fontFamily, + const { fontFace, category, ...familyWithValidParameters } = fontFamily; + const fontFamilySettings = { + ...familyWithValidParameters, slug: kebabCase( fontFamily.slug ), }; - if ( newFontFamily?.fontFace ) { - const newFontFaces = newFontFamily.fontFace.map( - ( face, faceIndex ) => { - if ( face.file ) { - // Slugified file name because the it might contain spaces or characters treated differently on the server. - const fileId = `file-${ faceIndex }`; - // Add the files to the formData - formData.append( fileId, face.file, face.file.name ); - // remove the file object from the face object the file is referenced by the uploadedFile key - const { file, ...faceWithoutFileProperty } = face; - const newFace = { - ...faceWithoutFileProperty, - uploadedFile: fileId, - }; - return newFace; - } - return face; + formData.append( + 'font_family_settings', + JSON.stringify( fontFamilySettings ) + ); + return formData; +} + +export function makeFontFacesFormData( font ) { + if ( font?.fontFace ) { + const fontFacesFormData = font.fontFace.map( ( face, faceIndex ) => { + const formData = new FormData(); + if ( face.file ) { + // Slugified file name because the it might contain spaces or characters treated differently on the server. + const fileId = `file-${ faceIndex }`; + // Add the files to the formData + formData.append( fileId, face.file, face.file.name ); + // remove the file object from the face object the file is referenced in src + const { file, ...faceWithoutFileProperty } = face; + const fontFaceSettings = { + ...faceWithoutFileProperty, + src: fileId, + }; + formData.append( + 'font_face_settings', + JSON.stringify( fontFaceSettings ) + ); + } else { + formData.append( 'font_face_settings', JSON.stringify( face ) ); } - ); - newFontFamily.fontFace = newFontFaces; + return formData; + } ); + + return fontFacesFormData; } +} - formData.append( 'font_family_settings', JSON.stringify( newFontFamily ) ); - return formData; +export async function batchInstallFontFaces( fontFamilyId, fontFacesData ) { + const promises = fontFacesData.map( ( faceData ) => + fetchInstallFontFace( fontFamilyId, faceData ) + ); + const responses = await Promise.allSettled( promises ); + + const results = { + errors: [], + successes: [], + }; + + responses.forEach( ( result, index ) => { + if ( result.status === 'fulfilled' ) { + const response = result.value; + if ( response.id ) { + results.successes.push( response ); + } else { + results.errors.push( { + data: fontFacesData[ index ], + message: `Error: ${ response.message }`, + } ); + } + } else { + // Handle network errors or other fetch-related errors + results.errors.push( { + data: fontFacesData[ index ], + error: `Fetch error: ${ result.reason }`, + } ); + } + } ); + + return results; } /* diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js deleted file mode 100644 index 9f38903c89759b..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Internal dependencies - */ -import { makeFormDataFromFontFamily } from '../index'; - -/* global File */ - -describe( 'makeFormDataFromFontFamily', () => { - it( 'should process fontFamilies and return FormData', () => { - const mockFontFamily = { - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - fontFace: [ - { - file: new File( [ 'content' ], 'test-font1.woff2' ), - fontWeight: '500', - fontStyle: 'normal', - }, - { - file: new File( [ 'content' ], 'test-font2.woff2' ), - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }; - - const formData = makeFormDataFromFontFamily( mockFontFamily ); - - expect( formData instanceof FormData ).toBeTruthy(); - - // Check if files are added correctly - expect( formData.get( 'file-0' ).name ).toBe( 'test-font1.woff2' ); - expect( formData.get( 'file-1' ).name ).toBe( 'test-font2.woff2' ); - - // Check if 'fontFamilies' key in FormData is correct - const expectedFontFamily = { - fontFace: [ - { - fontWeight: '500', - fontStyle: 'normal', - uploadedFile: 'file-0', - }, - { - fontWeight: '400', - fontStyle: 'normal', - uploadedFile: 'file-1', - }, - ], - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - }; - expect( JSON.parse( formData.get( 'font_family_settings' ) ) ).toEqual( - expectedFontFamily - ); - } ); -} );