Make error handling for ajax list incrementing more robust

This commit is contained in:
Timothy Warren 2023-03-17 11:09:43 -04:00
parent 0e780f26b9
commit 60ddcaa08e
14 changed files with 1375 additions and 533 deletions

0
app/logs/.gitkeep Normal file → Executable file
View File

View File

@ -1,5 +1,6 @@
import _ from './anime-client.js' import _ from './anime-client.js'
import { renderSearchResults } from './template-helpers.js' import { renderSearchResults } from './template-helpers.js'
import { getNestedProperty, hasNestedProperty } from "./fns";
const search = (query, isCollection = false) => { const search = (query, isCollection = false) => {
// Show the loader // Show the loader
@ -70,6 +71,14 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
} }
}; };
const displayMessage = (type, message) => {
_.hide('#loading-shadow');
_.showMessage(type, `${message} ${title}.`);
_.scrollToTop();
}
const showError = () => displayMessage('error', 'Failed to update');
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently watching // change status to currently watching
if (isNaN(watchedCount) || watchedCount === 0) { if (isNaN(watchedCount) || watchedCount === 0) {
@ -89,36 +98,31 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
success: (res) => { success: (res) => {
try {
const resData = JSON.parse(res); const resData = JSON.parse(res);
if (resData.error) { // Do a rough sanity check for weird errors
_.hide('#loading-shadow'); let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
_.showMessage('error', `Failed to update ${title}. `); if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
_.scrollToTop(); showError();
return; return;
} }
// We've completed the series // We've completed the series
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') { if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
_.hide('#loading-shadow'); displayMessage('success', 'Completed')
_.showMessage('success', `Successfully completed ${title}`);
_.scrollToTop();
return; return;
} }
_.hide('#loading-shadow'); // Just a normal update
_.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount; _.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;
_.scrollToTop(); displayMessage('success', 'Updated');
}, } catch (_) {
error: () => { showError();
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
} }
},
error: showError,
}); });
}); });

103
frontEndSrc/js/fns.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Make sure properties are in an easily splittable format
*
* @private
* @param {String} props
* @param {String} [sep='.'] The default separator
* @return {String}
*/
function _normalizeProperty(props, sep = '.') {
// Since we split by period, and property lookup
// is the same by dot or [], replace bracket lookups
// with periods
return props.replace(/\[(.*?)]/g, sep + '$1');
}
/**
* Tell if a nested object has a given property (or array a given index)
* given an object such as a.b.c.d = 5, hasNestedProperty(a, 'b.c.d') will return true.
*
* @param {Object} object the object to get the property from
* @param {String} property the path to the property as a string
* @returns {boolean} true when property in object, false otherwise
*/
export function hasNestedProperty(object, property) {
if (object && typeof object === 'object') {
if (typeof property === 'string' && property !== '') {
property = _normalizeProperty(property);
let split = property.split('.');
return split.reduce((obj, prop, idx, array) => {
if (idx === array.length - 1) {
return !!(obj && obj.hasOwnProperty(prop));
}
return obj && obj[prop];
}, object);
} else if (typeof property === 'number') {
return property in object;
}
}
return false;
}
/**
* Get the value of a deeply nested property in an object
*
* @param {Object} object the object to get the property
* @param {string} property the path to the property as a string
* @param {string} [sep='.'] The default separator to split on
* @return {*} the value of the property
*/
export function getNestedProperty(object, property, sep = '.') {
if (isType('string', property) && property !== '') {
// convert numbers to dot syntax
property = _normalizeProperty(property, sep);
const levels = property.split(sep);
try {
return levels.reduce((obj, prop) => obj[prop], object);
} catch (e) {
return undefined;
}
}
return null;
}
/**
* Reliably get the type of the value of a variable
*
* @param {*} x The variable to get the type of
* @return {string} The name of the type
*/
export function getType(x) {
// is it an array?
if (Array.isArray(x)) {
return 'array';
}
// Use typeof for truthy primitives
if (typeof x !== 'object') {
return (typeof x).toLowerCase();
}
const type = function () {
return Object.prototype.toString.call(this).slice(8, -1);
}
// Otherwise, strip the type out of the '[Object x]' toString value
return type.call(x).toLowerCase();
}
/**
* Check whether the value matches the passed type name
*
* @param {string} type Javascript type name
* @param {*} val The value to type check
* @return {boolean}
*/
export function isType(type, val) {
return getType(val) === String(type).toLowerCase();
}

View File

@ -1,5 +1,6 @@
import _ from './anime-client.js' import _ from './anime-client.js'
import { renderSearchResults } from './template-helpers.js' import { renderSearchResults } from './template-helpers.js'
import { getNestedProperty, hasNestedProperty } from "./fns";
const search = (query) => { const search = (query) => {
_.show('.cssload-loader'); _.show('.cssload-loader');
@ -36,7 +37,7 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume'; let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0; let completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;
let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10); let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);
let mangaName = _.$('.name', parentSel)[ 0 ].textContent; let title = _.$('.name', parentSel)[ 0 ].textContent;
if (isNaN(completed)) { if (isNaN(completed)) {
completed = 0; completed = 0;
@ -45,12 +46,21 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
// Setup the update data // Setup the update data
let data = { let data = {
id: parentSel.dataset.kitsuId, id: parentSel.dataset.kitsuId,
anilist_id: parentSel.dataset.anilistId,
mal_id: parentSel.dataset.malId, mal_id: parentSel.dataset.malId,
data: { data: {
progress: completed progress: completed
} }
}; };
const displayMessage = (type, message) => {
_.hide('#loading-shadow');
_.showMessage(type, `${message} ${title}.`);
_.scrollToTop();
}
const showError = () => displayMessage('error', 'Failed to update');
// If the episode count is 0, and incremented, // If the episode count is 0, and incremented,
// change status to currently reading // change status to currently reading
if (isNaN(completed) || completed === 0) { if (isNaN(completed) || completed === 0) {
@ -73,33 +83,32 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
type: 'POST', type: 'POST',
mimeType: 'application/json', mimeType: 'application/json',
success: (res) => { success: (res) => {
const resData = JSON.parse(res) try {
if (resData.error) { const resData = JSON.parse(res);
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}. `); // Do a rough sanity check for weird errors
_.scrollToTop(); let updatedProgress = getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.progress');
if (hasNestedProperty(resData, 'error') || updatedProgress !== data.data.progress) {
showError();
return; return;
} }
if (String(data.data.status).toUpperCase() === 'COMPLETED') { // We've completed the series
if (getNestedProperty(resData, 'data.libraryEntry.update.libraryEntry.status') === 'COMPLETED') {
_.hide(parentSel); _.hide(parentSel);
_.hide('#loading-shadow'); displayMessage('success', 'Completed')
_.showMessage('success', `Successfully completed ${mangaName}`);
_.scrollToTop();
return; return;
} }
_.hide('#loading-shadow'); // Just a normal update
_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed); _.$(`.${type}s_read`, parentSel)[ 0 ].textContent = String(completed);
_.showMessage('success', `Successfully updated ${mangaName}`); displayMessage('success', 'Updated');
_.scrollToTop();
}, } catch (_) {
error: () => { showError();
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${mangaName}`);
_.scrollToTop();
} }
},
error: showError,
}); });
}); });

View File

@ -15,7 +15,7 @@
"cssnano": "^5.0.1", "cssnano": "^5.0.1",
"postcss": "^8.2.6", "postcss": "^8.2.6",
"postcss-import": "^15.0.0", "postcss-import": "^15.0.0",
"postcss-preset-env": "^7.8.2", "postcss-preset-env": "^8.0.1",
"watch": "^1.0.2" "watch": "^1.0.2"
} }
} }

View File

@ -1,4 +1,6 @@
module.exports = { const { config } = require("@swc/core/spack");
module.exports = config({
entry: { entry: {
'scripts.min': __dirname + '/js/index.js', 'scripts.min': __dirname + '/js/index.js',
'tables.min': __dirname + '/js/base/sort-tables.js', 'tables.min': __dirname + '/js/base/sort-tables.js',
@ -8,12 +10,15 @@ module.exports = {
}, },
options: { options: {
jsc: { jsc: {
target: 'es3', parser: {
loose: true, syntax: "ecmascript",
jsx: false,
},
target: 'es2016',
loose: false,
}, },
minify: true, minify: true,
module: { sourceMaps: false,
type: 'es6' isModule: true,
}
}
} }
});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace("episodes: ","").replace("-",0).split("/");var arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init(); const LightTableSorter=(()=>{let th=null;let cellIndex=null;let order="";const text=row=>row.cells.item(cellIndex).textContent.toLowerCase();const sort=(a,b)=>{let textA=text(a);let textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){let arrayA=textA.replace("episodes: ","").replace("-",0).split("/");let arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};const toggle=()=>{const c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};const reset=()=>{th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};const onClickEvent=e=>{if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;const tbody=th.offsetParent.getElementsByTagName("tbody")[0];let rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(row=>{tbody.appendChild(row)})}}};return{init:()=>{let ths=document.getElementsByTagName("th");let results=[];for(let i=0,len=ths.length;i<len;i++){let th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}})();LightTableSorter.init();

File diff suppressed because one or more lines are too long