mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added loading state when ordering comments (#21788)
PLG-280 - Added a loading state implementation when changing the ordering of comments. - This improves the overall UX particularly with slower connections. - Due to the nature of how comments and ordering are handled, we approached it with a simple state that determines whether it's done loading or not around the API query. --------- Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
parent
0757f270eb
commit
55dc9d997f
8 changed files with 107 additions and 32 deletions
|
@ -29,7 +29,8 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
|
|||
popup: null,
|
||||
labs: {},
|
||||
order: 'count__likes desc, created_at desc',
|
||||
adminApi: null
|
||||
adminApi: null,
|
||||
commentsIsLoading: false
|
||||
});
|
||||
|
||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||
|
@ -173,7 +174,8 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
|
|||
pagination,
|
||||
commentCount: count,
|
||||
order,
|
||||
labs: labs
|
||||
labs: labs,
|
||||
commentsIsLoading: false
|
||||
};
|
||||
|
||||
setState(state);
|
||||
|
|
|
@ -81,7 +81,8 @@ export type EditableAppContext = {
|
|||
popup: Page | null,
|
||||
labs: LabsContextType,
|
||||
order: string,
|
||||
adminApi: AdminApi | null
|
||||
adminApi: AdminApi | null,
|
||||
commentsIsLoading?: boolean
|
||||
}
|
||||
|
||||
export type TranslationFunction = (key: string, replacements?: Record<string, string | number>) => string;
|
||||
|
|
|
@ -23,18 +23,27 @@ async function loadMoreComments({state, api, options, order}: {state: EditableAp
|
|||
}
|
||||
|
||||
async function setOrder({state, data: {order}, options, api}: {state: EditableAppContext, data: {order: string}, options: CommentsOptions, api: GhostApi}) {
|
||||
let data;
|
||||
state.commentsIsLoading = true;
|
||||
|
||||
if (state.admin && state.adminApi && state.labs.commentImprovements) {
|
||||
data = await state.adminApi.browse({page: 1, postId: options.postId, order});
|
||||
try {
|
||||
let data;
|
||||
if (state.admin && state.adminApi && state.labs.commentImprovements) {
|
||||
data = await state.adminApi.browse({page: 1, postId: options.postId, order});
|
||||
} else {
|
||||
data = await api.comments.browse({page: 1, postId: options.postId, order});
|
||||
}
|
||||
|
||||
return {
|
||||
comments: [...data.comments],
|
||||
pagination: data.meta.pagination,
|
||||
order,
|
||||
commentsIsLoading: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to set order:', error); // eslint-disable-line no-console
|
||||
state.commentsIsLoading = false;
|
||||
throw error; // Rethrow the error to allow upstream handling
|
||||
}
|
||||
data = await api.comments.browse({page: 1, postId: options.postId, order: order});
|
||||
|
||||
return {
|
||||
comments: [...data.comments],
|
||||
pagination: data.meta.pagination,
|
||||
order
|
||||
};
|
||||
}
|
||||
|
||||
async function loadMoreReplies({state, api, data: {comment, limit}, isReply}: {state: EditableAppContext, api: GhostApi, data: {comment: any, limit?: number | 'all'}, isReply: boolean}): Promise<Partial<EditableAppContext>> {
|
||||
|
|
|
@ -17,8 +17,10 @@ type AnimatedCommentProps = {
|
|||
};
|
||||
|
||||
const AnimatedComment: React.FC<AnimatedCommentProps> = ({comment, parent}) => {
|
||||
const {commentsIsLoading} = useAppContext();
|
||||
return (
|
||||
<Transition
|
||||
className={`${commentsIsLoading ? 'animate-pulse' : ''}`}
|
||||
data-testid="animated-comment"
|
||||
enter="transition-opacity duration-300 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
|
|
|
@ -10,7 +10,7 @@ import {useEffect} from 'react';
|
|||
|
||||
const Content = () => {
|
||||
const labs = useLabs();
|
||||
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, openFormCount, t} = useAppContext();
|
||||
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, openFormCount, commentsIsLoading, t} = useAppContext();
|
||||
|
||||
let commentsElements;
|
||||
const commentsDataset = comments;
|
||||
|
@ -64,7 +64,7 @@ const Content = () => {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="z-10" data-test="comment-elements">
|
||||
<div className={`z-10 transition-opacity duration-100 ${commentsIsLoading ? 'opacity-50' : ''}`} data-testid="comment-elements">
|
||||
{commentsElements}
|
||||
</div>
|
||||
<Pagination />
|
||||
|
@ -76,7 +76,7 @@ const Content = () => {
|
|||
<>
|
||||
<ContentTitle count={commentCount} showCount={showCount} title={title}/>
|
||||
<Pagination />
|
||||
<div className={!pagination ? 'mt-4' : ''} data-test="comment-elements">
|
||||
<div className={!pagination ? 'mt-4' : ''} data-testid="comment-elements">
|
||||
{commentsElements}
|
||||
</div>
|
||||
<div>
|
||||
|
@ -92,7 +92,7 @@ const Content = () => {
|
|||
}
|
||||
</div>
|
||||
{
|
||||
labs?.testFlag ? <div data-testid="this-comes-from-a-flag" style={{display: 'none'}}></div> : null
|
||||
labs?.testFlag ? <div data-testid="this-comes-from-a-flag" style={{display: 'none'}}></div> : null // do not remove
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
module.exports = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
heartbeat: 'heartbeat 0.35s ease-in-out forwards',
|
||||
pulse: 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite'
|
||||
},
|
||||
keyframes: {
|
||||
heartbeat: {
|
||||
'0%, 100%': {transform: 'scale(1)'},
|
||||
'50%': {transform: 'scale(1.3)'}
|
||||
}
|
||||
}
|
||||
},
|
||||
screens: {
|
||||
sm: '481px',
|
||||
md: '768px',
|
||||
|
@ -156,15 +168,6 @@ module.exports = {
|
|||
'0 57.7px 33.4px rgba(0, 0, 0, 0.072)',
|
||||
'0 138px 80px rgba(0, 0, 0, 0.1)'
|
||||
]
|
||||
},
|
||||
animation: {
|
||||
heartbeat: 'heartbeat 0.35s ease-in-out forwards'
|
||||
},
|
||||
keyframes: {
|
||||
heartbeat: {
|
||||
'0%, 100%': {transform: 'scale(1)'},
|
||||
'50%': {transform: 'scale(1.3)'}
|
||||
}
|
||||
}
|
||||
},
|
||||
content: [
|
||||
|
|
|
@ -414,15 +414,65 @@ test.describe('Actions', async () => {
|
|||
'comments-sorting-form-dropdown'
|
||||
);
|
||||
|
||||
const newestOption = await sortingDropdown.getByText('Newest');
|
||||
await newestOption.click();
|
||||
const optionSelect = await sortingDropdown.getByText('Newest');
|
||||
mockedApi.setDelay(100);
|
||||
await optionSelect.click();
|
||||
const commentsElement = await frame.getByTestId('comment-elements');
|
||||
const hasOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasOpacity50).toBe(true);
|
||||
|
||||
const comments = await frame.getByTestId('comment-component');
|
||||
|
||||
await expect(comments.nth(0)).toContainText('This is the newest comment');
|
||||
|
||||
const hasNoOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasNoOpacity50).toBe(false);
|
||||
});
|
||||
|
||||
test('Sorts by oldest', async ({page}) => {
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 2</p>',
|
||||
created_at: new Date('2024-03-02T00:00:00Z'),
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 52
|
||||
}
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is the oldest</p>',
|
||||
created_at: new Date('2024-02-01T00:00:00Z')
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is the newest comment</p>',
|
||||
created_at: new Date('2024-04-03T00:00:00Z')
|
||||
});
|
||||
|
||||
const {frame} = await initializeTest(page, {labs: true});
|
||||
|
||||
const sortingForm = await frame.getByTestId('comments-sorting-form');
|
||||
|
||||
await sortingForm.click();
|
||||
|
||||
const sortingDropdown = await frame.getByTestId(
|
||||
'comments-sorting-form-dropdown'
|
||||
);
|
||||
|
||||
const optionSelect = await sortingDropdown.getByText('Oldest');
|
||||
mockedApi.setDelay(100);
|
||||
await optionSelect.click();
|
||||
const commentsElement = await frame.getByTestId('comment-elements');
|
||||
const hasOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasOpacity50).toBe(true);
|
||||
|
||||
const comments = await frame.getByTestId('comment-component');
|
||||
|
||||
await expect(comments.nth(0)).toContainText('This is the oldest');
|
||||
|
||||
const hasNoOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasNoOpacity50).toBe(false);
|
||||
});
|
||||
|
||||
test('has loading state when changing sorting', async ({page}) => {
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is the oldest</p>',
|
||||
created_at: new Date('2024-02-01T00:00:00Z')
|
||||
|
@ -446,12 +496,19 @@ test.describe('Actions', async () => {
|
|||
'comments-sorting-form-dropdown'
|
||||
);
|
||||
|
||||
const newestOption = await sortingDropdown.getByText('Oldest');
|
||||
await newestOption.click();
|
||||
const optionSelect = await sortingDropdown.getByText('Newest');
|
||||
mockedApi.setDelay(100);
|
||||
await optionSelect.click();
|
||||
const commentsElement = await frame.getByTestId('comment-elements');
|
||||
const hasOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasOpacity50).toBe(true);
|
||||
|
||||
const comments = await frame.getByTestId('comment-component');
|
||||
|
||||
await expect(comments.nth(0)).toContainText('This is the oldest');
|
||||
await expect(comments.nth(0)).toContainText('This is the newest comment');
|
||||
|
||||
const hasNoOpacity50 = await commentsElement.evaluate(el => el.classList.contains('opacity-50'));
|
||||
expect(hasNoOpacity50).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ const setup = async () => {
|
|||
brandColor: site.accent_color,
|
||||
page: 'signup',
|
||||
initStatus: 'success',
|
||||
showPopup: true
|
||||
showPopup: true,
|
||||
commentsIsLoading: false
|
||||
};
|
||||
const {...utils} = render(
|
||||
<App testState={testState} />
|
||||
|
|
Loading…
Add table
Reference in a new issue