diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js index 06c50cbbfa..5b4e51f322 100644 --- a/client/modules/IDE/actions/projects.js +++ b/client/modules/IDE/actions/projects.js @@ -1,6 +1,6 @@ import { apiClient } from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; -import { startLoader, stopLoader } from '../reducers/loading'; +import { startLoader, stopLoader, setError } from '../reducers/loading'; // eslint-disable-next-line export function getProjects(username) { @@ -22,11 +22,11 @@ export function getProjects(username) { dispatch(stopLoader()); }) .catch((error) => { + dispatch(setError(error?.response?.data || 'Failed to load sketches')); dispatch({ type: ActionTypes.ERROR, error: error?.response?.data }); - dispatch(stopLoader()); }); }; } diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index c0e2cc42c5..97c88228da 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -10,6 +10,7 @@ import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; +import { clearError as clearLoadingError } from '../reducers/loading'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; import Overlay from '../../App/components/Overlay'; @@ -27,16 +28,19 @@ const SketchList = ({ sorting, toggleDirectionForField, resetSorting, - mobile + mobile, + clearError }) => { const [isInitialDataLoad, setIsInitialDataLoad] = useState(true); const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null); const { t } = useTranslation(); useEffect(() => { + setIsInitialDataLoad(true); + clearError(); getProjects(username); resetSorting(); - }, [getProjects, username, resetSorting]); + }, [getProjects, username, resetSorting, clearError]); useEffect(() => { if (Array.isArray(sketches)) { @@ -52,14 +56,38 @@ const SketchList = ({ [username, user.username, t] ); - const isLoading = () => loading && isInitialDataLoad; - - const hasSketches = () => !isLoading() && sketches.length > 0; + const isLoading = () => isInitialDataLoad || loading.isLoading; + const hasError = () => loading.error && !loading.isLoading; + const hasSketches = () => !isLoading() && !hasError() && sketches.length > 0; + const isEmpty = () => !isLoading() && !hasError() && sketches.length === 0; const renderLoader = () => isLoading() && ; + const renderError = () => { + if (hasError()) { + return ( +
+

+ {t('SketchList.LoadError', { error: loading.error })} +

+ +
+ ); + } + return null; + }; + const renderEmptyTable = () => { - if (!isLoading() && sketches.length === 0) { + if (isEmpty()) { return (

{t('SketchList.NoSketches')}

); @@ -127,6 +155,7 @@ const SketchList = ({ {getSketchesTitle} {renderLoader()} + {renderError()} {renderEmptyTable()} {hasSketches() && ( true, - stopLoader: () => false + startLoader: (state) => { + state.isLoading = true; + state.error = null; + }, + stopLoader: (state) => { + state.isLoading = false; + }, + setError: (state, action) => { + state.isLoading = false; + state.error = action.payload; + }, + clearError: (state) => { + state.error = null; + } } }); -export const { startLoader, stopLoader } = loadingSlice.actions; +export const { + startLoader, + stopLoader, + setError, + clearError +} = loadingSlice.actions; export default loadingSlice.reducer; diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 401f575728..a549ad27c9 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -251,6 +251,49 @@ font-size: #{math.div(16, $base-font-size)}rem; padding: #{math.div(42, $base-font-size)}rem 0; } + +.sketches-table__error { + text-align: center; + padding: #{math.div(42, $base-font-size)}rem 0; + + .sketches-table__error-message { + font-size: #{math.div(16, $base-font-size)}rem; + margin-bottom: #{math.div(16, $base-font-size)}rem; + @include themify() { + color: getThemifyVariable("error-color"); + } + } + + .sketches-table__retry-button { + background: none; + border: 2px solid; + border-radius: 4px; + padding: #{math.div(8, $base-font-size)}rem #{math.div(16, $base-font-size)}rem; + font-size: #{math.div(14, $base-font-size)}rem; + cursor: pointer; + transition: all 0.2s ease; + + @include themify() { + color: getThemifyVariable("logo-color"); + border-color: getThemifyVariable("logo-color"); + } + + &:hover { + @include themify() { + background-color: getThemifyVariable("logo-color"); + color: getThemifyVariable("background-color"); + } + } + + &:focus { + outline: 2px solid; + outline-offset: 2px; + @include themify() { + outline-color: getThemifyVariable("logo-color"); + } + } + } +} .sketches-table__row a:hover{ @include themify() { color: getThemifyVariable("logo-color"); diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index b101b56e8f..66b7e6525e 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -591,7 +591,10 @@ "HeaderCreatedAt_mobile": "Created", "HeaderUpdatedAt": "Date Updated", "HeaderUpdatedAt_mobile": "Updated", - "NoSketches": "No sketches." + "NoSketches": "No sketches.", + "LoadError": "Failed to load sketches: {{error}}", + "RetryButton": "Retry", + "RetryButtonARIA": "Retry loading sketches" }, "AddToCollectionSketchList": { "Title": "p5.js Web Editor | My sketches",