Talk by Javier Gamarra for /dev/24 about the new Liferay Community Site (also called Questions), built using React + GraphQL.
Ready to be launched [soon™]
Personal opinions follow:
- We want more participation of Liferay product teams in the forums
- We need to improve feature request flow, listen more to the community
- We want to ease interaction and participation
- We need to do dog-fooding of our APIs
- React... Why?
- It started as a create-react app
- GraphQL... Why?
- Using Liferay MB APIs (MBMessage, MBThread...)
-
New view for MBoards
- Will be launched as such in the future (but hey, Open Source, it's there in the repo).
-
Questions is a portlet*
- Just configuration, deal with ranks and create the URL for the file ItemSelector.
- Served inside Liferay with existing fetch -> no auth/CSRF management
-
Headless use
- Uses permissions to avoid client permissions
{answer.actions['reply-to-message'] && ( <ClayButton className="text-reset" displayType="unstyled" onClick={() => setShowNewComment(true)} > {Liferay.Language.get('reply')} </ClayButton> )}
- Heavy use of GraphQL (around 40 queries, with fragments)*
- Heavy use of GraphQL relationships
export const getMessagesQuery = gql` query messageBoardThreadMessageBoardMessages( $messageBoardThreadId: Long! $page: Int! $pageSize: Int! $sort: String! ) { messageBoardThreadMessageBoardMessages( messageBoardThreadId: $messageBoardThreadId page: $page pageSize: $pageSize sort: $sort ) { items { actions aggregateRating { ratingAverage ratingCount ratingValue } articleBody creator { id image name } creatorStatistics { joinDate lastPostDate postsNumber rank } encodingFormat friendlyUrlPath id messageBoardMessages { items { actions articleBody creator { id image name } encodingFormat id showAsAnswer } } myRating { ratingValue } showAsAnswer } pageSize totalCount } } `;
- Including parent relationships
export const getSectionBySectionTitleQuery = gql` query messageBoardSections($filter: String!, $siteKey: String!) { messageBoardSections( filter: $filter flatten: true pageSize: 1 siteKey: $siteKey sort: "title:asc" ) { actions items { actions id messageBoardSections(sort: "title:asc") { actions items { id description numberOfMessageBoardSections numberOfMessageBoardThreads parentMessageBoardSectionId subscribed title } } numberOfMessageBoardSections parentMessageBoardSection { id messageBoardSections { items { id numberOfMessageBoardSections parentMessageBoardSectionId subscribed title } } numberOfMessageBoardSections parentMessageBoardSectionId subscribed title } parentMessageBoardSectionId subscribed title } } } `;
- We use MB* APIs + Ratings + Subscriptions
-
API Changes
-
No extension of APIs (remember that GraphQL APIs are inferred from REST + extended)
- We can add our API, and endpoint inside an existing API, change an existing endpoint, change a entity to add or remove fields, remove an endpoint...
- +do it only for GraphQL
@GraphQLTypeExtension(Document.class) public class GetDocumentFolderTypeExtension { public GetDocumentFolderTypeExtension(Document document) { _document = document; } @GraphQLField(description = "Retrieves the document folder.") public DocumentFolder folder() throws Exception { return _applyComponentServiceObjects( _documentFolderResourceComponentServiceObjects, Query.this::_populateResourceContext, documentFolderResource -> documentFolderResource.getDocumentFolder( _document.getDocumentFolderId())); } private Document _document; }
public interface GraphQLContributor { public default Object getMutation() { return null; } public String getPath(); public default Object getQuery() { return null; } }
-
New APIs: ranked information
-
-
Uses Apollo JS Client with hooks (we were using a custom client before)
const {data, loading} = useQuery(getUserActivityQuery, { variables: {filter: `creatorId eq ${creatorId}`, page, pageSize, siteKey,}, });
- Not ideal
- Hooks move data transformation & variables to component
- A bit verbose
- Refetch + onComplete is wonky
- Hard to deal with conditional logic
- Cache is hit&miss, too many manual invalidation
- But I would say that is worth it, just for the times the syntax is brief and local cache alone
- Mixed usage with Apollo client directly
- Mixed/continuation calls are ugly so we avoid them:
const [deleteMessage] = useMutation(deleteMessageQuery, { onCompleted() { if (comments && comments.length) { Promise.all( comments.map(({id}) => client.mutate({ mutation: deleteMessageQuery, variables: {messageBoardMessageId: id}, }) ) ).then(() => { deleteAnswer(answer); }); } else { deleteAnswer(answer); } }, });
-
Works with old forum
- Compatible with old data and has to render a large forum (500.000? threads)
- Should-be compatible with BBCode and HTML but we are going the HTML way (conversion!)
- Support for old URLs with urlrewrite.xml + redirect components
<rule> <from>^/web/guest/community/forums/message_boards(.{0,2000})$</from> <to type="permanent-redirect">%{context-path}/web/guest/community/forums/-/message_boards$1</to> </rule>
- +slugs for everything
- Uses CKEditor (conversion!).
Verythin layer over Editor in frontend-editor-ckeditor-web (that is a very thin layer over CKEditor 4) + itemselector plugin.
import CKEditor from 'ckeditor4-react'; import React, {useEffect} from 'react'; const BASEPATH = '/o/frontend-editor-ckeditor-web/ckeditor/'; const Editor = React.forwardRef((props, ref) => { useEffect(() => { Liferay.once('beforeScreenFlip', () => { if ( window.CKEDITOR && Object.keys(window.CKEDITOR.instances).length === 0 ) { delete window.CKEDITOR; } }); }, []); return <CKEditor ref={ref} {...props} />; }); CKEditor.editorUrl = `${BASEPATH}ckeditor.js`; window.CKEDITOR_BASEPATH = BASEPATH; export {Editor};
- Uses Highlight JS
-
Performance
- We are reviewing all the endpoints
- Elastic issues with big size windows... UX makes sense? || scrolling API || DB
- After small tuning (1 day) everything looks good (<1 sec load)
- NestedFields... future lazy implementation?
-
Styles
- Some new UI patterns like breadcrumbs or lists
- But Clay everywhere (FTW!)
- Very hacky implementation for subscriptions URLs
- Some limitations regarding rankings APIs because DB structure
- Use fragments in GraphQL calls !
MBCategoryMessageBoardSection management is messy- Add more friendlyURLs
- Better use of related queries
- Review accesibility :(
- ...
- ...
- ...
Thanks!