Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font Library: refactor client side install functions to work with revised API #57844

Merged
merged 9 commits into from
Jan 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
* Internal dependencies
*/
import {
fetchInstallFont,
fetchGetFontFamilyBySlug,
fetchInstallFontFamily,
fetchUninstallFonts,
fetchFontCollections,
fetchFontCollection,
Expand All @@ -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( {} );

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
mikachan marked this conversation as resolved.
Show resolved Hide resolved
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 );
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there families with invalid parameters?

Copy link
Contributor Author

@jffng jffng Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the API returned an error when fontFace or category were present in the settings, so I had to remove them. Maybe the variable should be renamed...

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 ],
jffng marked this conversation as resolved.
Show resolved Hide resolved
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;
}

/*
Expand Down

This file was deleted.

Loading