From 3c34de120bd6dddad62cb65dad1bf553637805b8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 7 Sep 2023 12:03:41 +0200 Subject: [PATCH] Add snackbars to /tags (#20046) * feat: add snackbars to /tags * chore: update the props for the component" * feat: update snackbar for following * feat: add tests for snackbars * feat: add error handling and show error on snackbar * chore: tweaks to syntax * refactor: abstract out the details * fix: check for null explicitly * refactor: use hidden instead of assigning a var --- app/javascript/packs/TagsIndex.jsx | 10 +++ app/javascript/tags/Tag.jsx | 87 +++++++++++++------ .../seededFlows/tagsFlows/followTag.spec.js | 35 +++++++- 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/app/javascript/packs/TagsIndex.jsx b/app/javascript/packs/TagsIndex.jsx index c165adc02..26fc13933 100644 --- a/app/javascript/packs/TagsIndex.jsx +++ b/app/javascript/packs/TagsIndex.jsx @@ -1,4 +1,5 @@ import { h, render } from 'preact'; +import { Snackbar } from '../Snackbar/Snackbar'; import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken'; /* global showLoginModal */ @@ -62,8 +63,17 @@ function handleButtonClick({ target }) { } } +function loadSnackbar() { + const root = document.getElementsByClassName('tags-index'); + if (root.length > 0) { + render(, document.getElementById('snack-zone')); + } +} + document.ready.then(() => { const userStatus = document.body.getAttribute('data-user-status'); + loadSnackbar(); + if (userStatus === 'logged-out') { listenForButtonClicks(); return; diff --git a/app/javascript/tags/Tag.jsx b/app/javascript/tags/Tag.jsx index 1f8c022f6..0fe8bef85 100644 --- a/app/javascript/tags/Tag.jsx +++ b/app/javascript/tags/Tag.jsx @@ -1,6 +1,8 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; import PropTypes from 'prop-types'; +import { addSnackbarItem } from '../Snackbar'; + /* global browserStoreCache */ /** @@ -19,29 +21,69 @@ export const Tag = ({ id, name, isFollowing, isHidden }) => { let followingButton; const toggleFollowButton = () => { - setFollowing(!following); - browserStoreCache('remove'); - postFollowItem({ following: !following, hidden }); - }; + const updatedFollowState = !following; - const toggleHideButton = () => { - const updatedHiddenState = !hidden; - setHidden(updatedHiddenState); - - // if the tag's new state will be hidden (clicked on the hide button) then we we set it to following. - // if the tags new state is to be unhidden (clicked on the unhide button) then we set it to not following. - const updatedFollowingState = updatedHiddenState; - setFollowing(updatedFollowingState); - - browserStoreCache('remove'); postFollowItem({ - hidden: updatedHiddenState, - following: updatedFollowingState, + following: updatedFollowState, + hidden, + }).then((response) => { + if (response.ok) { + updateItem( + null, + updatedFollowState, + `You have ${following ? 'un' : ''}followed ${name}.`, + ); + return; + } + + addSnackbarItem({ + message: `An error has occurred.`, + addCloseButton: true, + }); }); }; + const toggleHideButton = () => { + // if the tag's new state will be hidden (clicked on the hide button) then we we set it to following. + // if the tags new state is to be unhidden (clicked on the unhide button) then we set it to unfollow. + const updatedHiddenState = !hidden; + const updatedFollowState = updatedHiddenState; + + postFollowItem({ + hidden: updatedHiddenState, + following: updatedFollowState, + }).then((response) => { + if (response.ok) { + updateItem( + updatedHiddenState, + updatedFollowState, + `You have ${hidden ? 'un' : ''}hidden ${name}.`, + ); + return; + } + + addSnackbarItem({ + message: `An error has occurred.`, + addCloseButton: true, + }); + }); + }; + + const updateItem = (updatedHiddenState, updatedFollowState, message) => { + if (updatedHiddenState !== null) { + setHidden(updatedHiddenState); + } + setFollowing(updatedFollowState); + browserStoreCache('remove'); + addSnackbarItem({ + message, + addCloseButton: true, + }); + return; + }; + const postFollowItem = ({ following, hidden }) => { - fetch('/follows', { + return fetch('/follows', { method: 'POST', headers: { Accept: 'application/json', @@ -55,11 +97,6 @@ export const Tag = ({ id, name, isFollowing, isHidden }) => { explicit_points: hidden ? -1 : 1, }), credentials: 'same-origin', - }).then((response) => { - if (response.status !== 200) { - // TODO: replace this with an actual modal - alert('An error occurred'); - } }); }; @@ -98,8 +135,8 @@ export const Tag = ({ id, name, isFollowing, isHidden }) => { }; Tag.propTypes = { - id: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - following: PropTypes.bool.isRequired, - hidden: PropTypes.bool.isRequired, + following: PropTypes.bool, + hidden: PropTypes.bool, }; diff --git a/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js b/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js index 72066f5ea..c156e697b 100644 --- a/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js +++ b/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js @@ -11,7 +11,24 @@ describe('Follow tag', () => { }); }); - it('Follows and unfollows a tag from the tag index page', () => { + it('shows an error message when following a tag fails', () => { + cy.intercept('POST', '/follows', { + statusCode: 422, + body: { + error: 'Something went wrong.', + }, + }).as('followsRequest'); + cy.findByRole('button', { name: 'Follow tag: tag0' }).as('followButton'); + + cy.get('@followButton').click(); + cy.wait('@followsRequest'); + + cy.findByTestId('snackbar').within(() => { + cy.findByRole('alert').should('have.text', 'An error has occurred.'); + }); + }); + + it('follows and unfollows a tag from the tag index page', () => { cy.intercept('/follows').as('followsRequest'); cy.findByRole('button', { name: 'Follow tag: tag0' }).as('followButton'); @@ -21,11 +38,19 @@ describe('Follow tag', () => { cy.get('@followButton').should('have.text', 'Following'); cy.get('@followButton').should('have.attr', 'aria-pressed', 'true'); + cy.findByTestId('snackbar').within(() => { + cy.findByRole('alert').should('have.text', 'You have followed tag0.'); + }); + cy.get('@followButton').click(); cy.wait('@followsRequest'); cy.get('@followButton').should('have.text', 'Follow'); cy.get('@followButton').should('have.attr', 'aria-pressed', 'false'); + + cy.findByTestId('snackbar').within(() => { + cy.findByRole('alert').should('have.text', 'You have unfollowed tag0.'); + }); }); it('hides and unhides a tag', () => { @@ -42,6 +67,9 @@ describe('Follow tag', () => { // and remove the Follow button cy.get('@hideButton').should('have.text', 'Unhide'); cy.get('@toBeHiddenFollowButton').should('not.exist'); + cy.findByTestId('snackbar').within(() => { + cy.findByRole('alert').should('have.text', 'You have hidden tag0.'); + }); // clicking on 'Unhide' should change it back to 'Hide' // and show a 'Follow' button @@ -61,6 +89,9 @@ describe('Follow tag', () => { 'aria-pressed', 'false', ); + cy.findByTestId('snackbar').within(() => { + cy.findByRole('alert').should('have.text', 'You have unhidden tag0.'); + }); }); }); @@ -77,7 +108,7 @@ describe('Follow tag', () => { }); }); - it('Follows and unfollows a tag from the tag feed page', () => { + it('follows and unfollows a tag from the tag feed page', () => { cy.intercept('/follows').as('followsRequest'); cy.findByRole('button', { name: 'Follow tag: tag1' }).as('followButton');