From 519ef2c3a7525db3dc4e5ee007048d3e09a39172 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Mon, 15 Jan 2024 11:32:01 -0800 Subject: [PATCH 01/31] add debounce util function --- src/utils/misc.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/utils/misc.js b/src/utils/misc.js index de72be82..ee5d92f7 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -4,6 +4,25 @@ export function deepCopy(obj) { export function deepEquals(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } + +export const debounce = (mainFunction, delay) => { + + let timer; + + // return an anonymous functiont hat takes in any number of arguments + const debouncedFunction = (...args) => { + // clear previous timer to prevent the execution of 'mainFunction' + clearTimeout(timer); + + // set a new timer that will execute 'mainFunction' after the delay + timer = setTimeout(() => { + mainFunction(...args); + }, delay); + }; + + return debouncedFunction; +} + /* Convert firebase to javascript, mostly just used to get real array elements */ From 4c9dcbf55bcc31edf0db3fbadc039738ea9c85a6 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Mon, 15 Jan 2024 13:58:52 -0800 Subject: [PATCH 02/31] added checkURL misc util function --- src/utils/misc.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/misc.js b/src/utils/misc.js index ee5d92f7..a33dd2b8 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -23,6 +23,10 @@ export const debounce = (mainFunction, delay) => { return debouncedFunction; } +export const checkURL = () => { + console.log("check URL executed"); +} + /* Convert firebase to javascript, mostly just used to get real array elements */ From 1c0815b5bcfb9225380b18c829adc6623f784eec Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 11:44:50 -0800 Subject: [PATCH 03/31] validateURL already defined in validate.js, remove definition from Resources.jsx --- src/components/FormComponents/Resources.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 3030b31e..a3fcba0a 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -12,10 +12,9 @@ import { En, Fr, I18n } from "../I18n"; import BilingualTextInput from "./BilingualTextInput"; import RequiredMark from "./RequiredMark"; import { deepCopy } from "../../utils/misc"; +import { validateURL } from "../../utils/validate"; import { QuestionText, paperClass, SupplementalText } from "./QuestionStyles"; -const validateURL = (url) => !url || validator.isURL(url); - const Resources = ({ updateResources, resources, disabled }) => { const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; From cfe7e216eb39a729fff5dbead9998081a97f4f3f Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 11:46:00 -0800 Subject: [PATCH 04/31] add isURLActive --- src/utils/misc.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index a33dd2b8..4a567d49 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -1,3 +1,5 @@ +import { validateURL } from "./validate"; + export function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } @@ -23,9 +25,17 @@ export const debounce = (mainFunction, delay) => { return debouncedFunction; } -export const checkURL = () => { - console.log("check URL executed"); -} +export const isURLActive = async (url) => { + if (validateURL(url)) { + try { + const response = await fetch(url, {method: "HEAD" }); + return response.ok; + } catch (error) { + return false + } + } + return false; +}; /* Convert firebase to javascript, mostly just used to get real array elements From 42d78ba94122f9ae8bf120af9c994f553f170c49 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 14:22:19 -0800 Subject: [PATCH 05/31] update debounce to handle promises and be more readable --- src/utils/misc.js | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index 4a567d49..2ce9ce75 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -1,5 +1,3 @@ -import { validateURL } from "./validate"; - export function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } @@ -7,36 +5,35 @@ export function deepEquals(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } +/** + * Creates a debounced version of a function. + * + * @param {Function} mainFunction - The function to debounce. + * @param {number} delay - The amount of time (in milliseconds) to delay. + * @return {Function} A debounced version of the specified function. + */ export const debounce = (mainFunction, delay) => { - let timer; - // return an anonymous functiont hat takes in any number of arguments const debouncedFunction = (...args) => { - // clear previous timer to prevent the execution of 'mainFunction' - clearTimeout(timer); + // Return a promise that resolves or rejects based on the mainFunction's execution + return new Promise((resolve, reject) => { + // Clear existing timer to reset the debounce period + clearTimeout(timer); - // set a new timer that will execute 'mainFunction' after the delay - timer = setTimeout(() => { - mainFunction(...args); - }, delay); + // Set a new timer to delay the execution of mainFunction + timer = setTimeout(() => { + // Execute mainFunction and handle its promise + Promise.resolve(mainFunction(...args)) + .then(resolve) + .catch(reject); + }, delay); + }); }; return debouncedFunction; } -export const isURLActive = async (url) => { - if (validateURL(url)) { - try { - const response = await fetch(url, {method: "HEAD" }); - return response.ok; - } catch (error) { - return false - } - } - return false; -}; - /* Convert firebase to javascript, mostly just used to get real array elements */ From 44dc768e5664edb13e9935844b7d882137cbc09d Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 14:52:54 -0800 Subject: [PATCH 06/31] add isURLActive Firebase Function --- firebase-functions/functions/index.js | 2 ++ firebase-functions/functions/serverUtils.js | 19 +++++++++++++++++++ src/providers/UserProvider.jsx | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 firebase-functions/functions/serverUtils.js diff --git a/firebase-functions/functions/index.js b/firebase-functions/functions/index.js index 973efe12..afbf83bb 100644 --- a/firebase-functions/functions/index.js +++ b/firebase-functions/functions/index.js @@ -1,6 +1,7 @@ const admin = require("firebase-admin"); const { translate } = require("./translate"); const { createDraftDoi, updateDraftDoi, deleteDraftDoi } = require("./datacite"); +const { isURLActive } = require("./serverUtils"); const { notifyReviewer, notifyUser } = require("./notify"); const { updatesRecordCreate, @@ -23,3 +24,4 @@ exports.regenerateXMLforRecord = regenerateXMLforRecord; exports.createDraftDoi = createDraftDoi; exports.deleteDraftDoi = deleteDraftDoi; exports.updateDraftDoi = updateDraftDoi; +exports.isURLActive = isURLActive; diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js new file mode 100644 index 00000000..7c148d57 --- /dev/null +++ b/firebase-functions/functions/serverUtils.js @@ -0,0 +1,19 @@ +const functions = require("firebase-functions"); +const fetch = require('node-fetch'); + +// Function to check if a given URL is active +exports.isURLActive = functions.https.onCall(async (data) => { + const url = data.url; + + if (!url) { + throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "url".'); + } + + try { + const response = await fetch(url, {method: "HEAD" }); + return response.ok; // Return true if response is OK, otherwise false + } catch (error) { + console.error('Error in isURLActive:', error); // Log the error for debugging + return false; // Return false if an error occurs + } +}) \ No newline at end of file diff --git a/src/providers/UserProvider.jsx b/src/providers/UserProvider.jsx index 5b9254ae..56c534cb 100644 --- a/src/providers/UserProvider.jsx +++ b/src/providers/UserProvider.jsx @@ -87,6 +87,7 @@ class UserProvider extends FormClassTemplate { const createDraftDoi = firebase.functions().httpsCallable("createDraftDoi"); const updateDraftDoi = firebase.functions().httpsCallable("updateDraftDoi"); const deleteDraftDoi = firebase.functions().httpsCallable("deleteDraftDoi"); + const isURLActive = firebase.functions().httpsCallable("isURLActive"); return ( {children} From 3ccfb1170644ec23b39a3c221f57f5f0494b62d6 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 17:16:59 -0800 Subject: [PATCH 07/31] fix url assignment --- firebase-functions/functions/serverUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js index 7c148d57..7a564da0 100644 --- a/firebase-functions/functions/serverUtils.js +++ b/firebase-functions/functions/serverUtils.js @@ -3,7 +3,8 @@ const fetch = require('node-fetch'); // Function to check if a given URL is active exports.isURLActive = functions.https.onCall(async (data) => { - const url = data.url; + const url = data; + functions.logger.log('Received URL:', data); if (!url) { throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "url".'); From fc5749f30b47702bb6ff67d33191d427bf7e5b55 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 17:17:40 -0800 Subject: [PATCH 08/31] use url variable --- firebase-functions/functions/serverUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js index 7a564da0..ebf6b6a0 100644 --- a/firebase-functions/functions/serverUtils.js +++ b/firebase-functions/functions/serverUtils.js @@ -4,7 +4,7 @@ const fetch = require('node-fetch'); // Function to check if a given URL is active exports.isURLActive = functions.https.onCall(async (data) => { const url = data; - functions.logger.log('Received URL:', data); + functions.logger.log('Received URL:', url); if (!url) { throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "url".'); From 045dc6c61e8e983d815426cb49eb4ce996b0d459 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 21:24:01 -0800 Subject: [PATCH 09/31] update checkURLActive function --- firebase-functions/functions/index.js | 4 ++-- firebase-functions/functions/serverUtils.js | 11 ++++++++--- src/providers/UserProvider.jsx | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/firebase-functions/functions/index.js b/firebase-functions/functions/index.js index afbf83bb..9dcceafd 100644 --- a/firebase-functions/functions/index.js +++ b/firebase-functions/functions/index.js @@ -1,7 +1,7 @@ const admin = require("firebase-admin"); const { translate } = require("./translate"); const { createDraftDoi, updateDraftDoi, deleteDraftDoi } = require("./datacite"); -const { isURLActive } = require("./serverUtils"); +const { checkURLActive } = require("./serverUtils"); const { notifyReviewer, notifyUser } = require("./notify"); const { updatesRecordCreate, @@ -24,4 +24,4 @@ exports.regenerateXMLforRecord = regenerateXMLforRecord; exports.createDraftDoi = createDraftDoi; exports.deleteDraftDoi = deleteDraftDoi; exports.updateDraftDoi = updateDraftDoi; -exports.isURLActive = isURLActive; +exports.checkURLActive = checkURLActive; diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js index ebf6b6a0..ee09ea46 100644 --- a/firebase-functions/functions/serverUtils.js +++ b/firebase-functions/functions/serverUtils.js @@ -2,19 +2,24 @@ const functions = require("firebase-functions"); const fetch = require('node-fetch'); // Function to check if a given URL is active -exports.isURLActive = functions.https.onCall(async (data) => { - const url = data; +exports.checkURLActive = functions.https.onCall(async (data) => { + let url = data; functions.logger.log('Received URL:', url); if (!url) { throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "url".'); } + // Prepend 'http://' if the URL does not start with 'http://' or 'https://' + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'http://' + url; + } + try { const response = await fetch(url, {method: "HEAD" }); return response.ok; // Return true if response is OK, otherwise false } catch (error) { - console.error('Error in isURLActive:', error); // Log the error for debugging + console.error('Error in checkURLActive:', error); // Log the error for debugging return false; // Return false if an error occurs } }) \ No newline at end of file diff --git a/src/providers/UserProvider.jsx b/src/providers/UserProvider.jsx index 56c534cb..0ec6358a 100644 --- a/src/providers/UserProvider.jsx +++ b/src/providers/UserProvider.jsx @@ -87,7 +87,7 @@ class UserProvider extends FormClassTemplate { const createDraftDoi = firebase.functions().httpsCallable("createDraftDoi"); const updateDraftDoi = firebase.functions().httpsCallable("updateDraftDoi"); const deleteDraftDoi = firebase.functions().httpsCallable("deleteDraftDoi"); - const isURLActive = firebase.functions().httpsCallable("isURLActive"); + const checkURLActive = firebase.functions().httpsCallable("checkURLActive"); return ( {children} From 7b9584f6242d4aa843417ce65d76fe66721d45f8 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 16 Jan 2024 21:24:36 -0800 Subject: [PATCH 10/31] implement checkURLActive call in Resources component --- src/components/FormComponents/Resources.jsx | 52 ++++++++++++++++----- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index a3fcba0a..db2fce1e 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext, useState, useEffect, useCallback } from "react"; import { Add, Delete, @@ -11,13 +11,40 @@ import { En, Fr, I18n } from "../I18n"; import BilingualTextInput from "./BilingualTextInput"; import RequiredMark from "./RequiredMark"; -import { deepCopy } from "../../utils/misc"; +import { deepCopy, debounce } from "../../utils/misc"; import { validateURL } from "../../utils/validate"; import { QuestionText, paperClass, SupplementalText } from "./QuestionStyles"; +import { UserContext } from "../../providers/UserProvider"; const Resources = ({ updateResources, resources, disabled }) => { + + const { checkURLActive } = useContext(UserContext); + + const [urlIsActive, setUrlIsActive] = useState(false); const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; + const debouncedcheckURLActive = useCallback( + debounce(checkURLActive, 500), + [checkURLActive] + ); + + useEffect(() => { + resources.forEach((resource, index) => { + if (resource.url && validateURL(resource.url)) { + debouncedcheckURLActive(resource.url) + .then((isActive) => { + setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: isActive })); + }) + .catch(() => { + setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: false })); + }); + } else { + // URL is empty, set isActive to true + setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: true })); + } + }); + }, [resources, debouncedcheckURLActive]); + function addResource() { updateResources(resources.concat(deepCopy(emptyResource))); } @@ -34,13 +61,15 @@ const Resources = ({ updateResources, resources, disabled }) => { resources.splice(newIndex, 0, element); updateResources(resources); } + const nameLabel = ; const descriptionLabel = ; return (
- {resources.map((dist = deepCopy(emptyResource), i) => { - const urlIsValid = !dist.url || validateURL(dist.url); + {resources.map((resourceItem = deepCopy(emptyResource), i) => { + const urlIsValid = !resourceItem.url || validateURL(resourceItem.url); + function handleResourceChange(key) { return (e) => { const newValue = [...resources]; @@ -57,11 +86,11 @@ const Resources = ({ updateResources, resources, disabled }) => { Enter a name for the resource Entrez un titre pour la ressource - + { Entrez l'URL de la ressource - + @@ -98,11 +127,12 @@ const Resources = ({ updateResources, resources, disabled }) => { + (!urlIsValid && ) + || (urlIsActive[i] && urlIsActive[i].data === false && ) } - error={!urlIsValid} + error={!urlIsValid || (urlIsActive[i] && urlIsActive[i].data === false)} label="URL" - value={dist.url} + value={resourceItem.url} onChange={handleResourceChange("url")} fullWidth disabled={disabled} @@ -118,7 +148,7 @@ const Resources = ({ updateResources, resources, disabled }) => { From c1b71d502afaebbebdcd3643044ef8e74e0e6f21 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Tue, 23 Jan 2024 15:38:56 -0800 Subject: [PATCH 11/31] add more debugging logs for checkURLActive: --- firebase-functions/functions/serverUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js index ee09ea46..7ba0676e 100644 --- a/firebase-functions/functions/serverUtils.js +++ b/firebase-functions/functions/serverUtils.js @@ -17,9 +17,10 @@ exports.checkURLActive = functions.https.onCall(async (data) => { try { const response = await fetch(url, {method: "HEAD" }); + functions.logger.log(`Fetch response status for ${url}:`, response.status); return response.ok; // Return true if response is OK, otherwise false } catch (error) { - console.error('Error in checkURLActive:', error); // Log the error for debugging + functions.logger.error('Error in checkURLActive for URL:', url, error); return false; // Return false if an error occurs } }) \ No newline at end of file From ec2d3f68403cdde7b0ef3a1be4fb2e8434482bb9 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Wed, 24 Jan 2024 11:27:10 -0800 Subject: [PATCH 12/31] broken - debounce url input --- src/components/FormComponents/Resources.jsx | 90 +++++++++++++++------ 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index db2fce1e..de6634b2 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useState, useEffect, useCallback } from "react"; +import React, { useContext, useEffect, useState, useRef } from "react"; import { Add, Delete, @@ -7,43 +7,85 @@ import { } from "@material-ui/icons"; import { Button, Grid, Paper, TextField } from "@material-ui/core"; import validator from "validator"; +import { useDebouncedCallback } from 'use-debounce'; import { En, Fr, I18n } from "../I18n"; import BilingualTextInput from "./BilingualTextInput"; import RequiredMark from "./RequiredMark"; -import { deepCopy, debounce } from "../../utils/misc"; +import { deepCopy } from "../../utils/misc"; import { validateURL } from "../../utils/validate"; import { QuestionText, paperClass, SupplementalText } from "./QuestionStyles"; import { UserContext } from "../../providers/UserProvider"; const Resources = ({ updateResources, resources, disabled }) => { + const mounted = useRef(false); const { checkURLActive } = useContext(UserContext); - - const [urlIsActive, setUrlIsActive] = useState(false); + const [urlIsActive, setUrlIsActive] = useState({}); const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; - const debouncedcheckURLActive = useCallback( - debounce(checkURLActive, 500), - [checkURLActive] - ); + + // Debounce callback for URL updates + // const debouncedResourceUrls = useDebouncedCallback((index, url) => { + // const newResources = [...resources]; + // newResources[index].debouncedUrl = url; + // }, 3000); + + // console.log(debouncedResourceUrls); + + // the below line doesn't work because useDebounce cannot be called inside a callback + // const [debouncedResourceUrls] = resources.map((res) => useDebounce(res.url, 1000)); + + // console.log(`debounced urls: ${debouncedResourceUrls}`) + + // const checkURLActive = useCallback( + // debounce(checkURLActive, 500), + // [checkURLActive] + // ); useEffect(() => { - resources.forEach((resource, index) => { - if (resource.url && validateURL(resource.url)) { - debouncedcheckURLActive(resource.url) - .then((isActive) => { - setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: isActive })); - }) - .catch(() => { - setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: false })); - }); - } else { - // URL is empty, set isActive to true - setUrlIsActive((prevStatus) => ({ ...prevStatus, [index]: true })); + + mounted.current = true; + console.log(`resources array: ${JSON.stringify(resources)}`) + + // debouncedResources.forEach... + resources.forEach((resource) => { + // Check if the URL is not empty and valid + if (resource.url && resource.debouncedUrl && validateURL(resource.url)) { + + console.log(`Checking activity for URL: ${resource.url}`); + checkURLActive(resource.debouncedUrl) + .then((response) => { + + // Log the entire response for debugging + console.log(`Response from checkURLActive for ${resource.debouncedUrl}:`, response); + + const isActive = response.data; + + + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: isActive })); + + // Log based on the isActive value + if (isActive) { + console.log(`URL found active: ${resource.url}`); + } else { + console.log(`URL found inactive: ${resource.url}`); + } } + ) + } else if (!resource.url || !validateURL(resource.url)) { + // Handles both invalid and empty URLs + + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: false })); + console.log(`URL is invalid or empty, marked as inactive: ${resource.url}`); + + } }); - }, [resources, debouncedcheckURLActive]); + + return () => { + mounted.current = false; + }; + }, [resources, checkURLActive, debouncedResourceUrls ]); function addResource() { updateResources(resources.concat(deepCopy(emptyResource))); @@ -65,6 +107,8 @@ const Resources = ({ updateResources, resources, disabled }) => { const nameLabel = ; const descriptionLabel = ; + console.log(`url is active object: ${JSON.stringify(urlIsActive)}`) + return (
{resources.map((resourceItem = deepCopy(emptyResource), i) => { @@ -128,9 +172,9 @@ const Resources = ({ updateResources, resources, disabled }) => { ) - || (urlIsActive[i] && urlIsActive[i].data === false && ) + || (resourceItem.url && urlIsActive[resourceItem.url] === false && ) } - error={!urlIsValid || (urlIsActive[i] && urlIsActive[i].data === false)} + error={!urlIsValid || (resourceItem.url && urlIsActive[resourceItem.url] === false)} label="URL" value={resourceItem.url} onChange={handleResourceChange("url")} From b05b0dbf83dcc8366aa8268d99d06a39318f576a Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Wed, 24 Jan 2024 11:57:20 -0800 Subject: [PATCH 13/31] urlMessage don't flag inactive URL as error --- src/components/FormComponents/Resources.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index de6634b2..b2628425 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -174,7 +174,7 @@ const Resources = ({ updateResources, resources, disabled }) => { (!urlIsValid && ) || (resourceItem.url && urlIsActive[resourceItem.url] === false && ) } - error={!urlIsValid || (resourceItem.url && urlIsActive[resourceItem.url] === false)} + error={!urlIsValid} label="URL" value={resourceItem.url} onChange={handleResourceChange("url")} From ae48f4a845ac520d972c889a392c9ec0f887eb9e Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Wed, 24 Jan 2024 11:57:47 -0800 Subject: [PATCH 14/31] provide positive feedback for active URL --- src/components/FormComponents/Resources.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index b2628425..0f737682 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -173,6 +173,7 @@ const Resources = ({ updateResources, resources, disabled }) => { helperText={ (!urlIsValid && ) || (resourceItem.url && urlIsActive[resourceItem.url] === false && ) + || (resourceItem.url && urlIsActive[resourceItem.url] === true && ) } error={!urlIsValid} label="URL" From 1c69d16761c3dddd829f50d96ee0f5aded131d4e Mon Sep 17 00:00:00 2001 From: Matthew Foster Date: Wed, 24 Jan 2024 13:27:27 -0800 Subject: [PATCH 15/31] this seems to get pretty close to what we want. There is still an error when adding a resource, not sure what that is about --- src/components/FormComponents/Resources.jsx | 63 ++++++--------------- 1 file changed, 16 insertions(+), 47 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index de6634b2..a7c3058f 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -26,66 +26,35 @@ const Resources = ({ updateResources, resources, disabled }) => { // Debounce callback for URL updates - // const debouncedResourceUrls = useDebouncedCallback((index, url) => { - // const newResources = [...resources]; - // newResources[index].debouncedUrl = url; - // }, 3000); + const debounced = useDebouncedCallback(async (resource) => { + const response = await checkURLActive(resource.url) + console.log(response.data); + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) + }, 3000); - // console.log(debouncedResourceUrls); - - // the below line doesn't work because useDebounce cannot be called inside a callback - // const [debouncedResourceUrls] = resources.map((res) => useDebounce(res.url, 1000)); - - // console.log(`debounced urls: ${debouncedResourceUrls}`) - - // const checkURLActive = useCallback( - // debounce(checkURLActive, 500), - // [checkURLActive] - // ); - - useEffect(() => { + useEffect( () => { mounted.current = true; console.log(`resources array: ${JSON.stringify(resources)}`) - // debouncedResources.forEach... - resources.forEach((resource) => { + resources.forEach( (resource) => { // Check if the URL is not empty and valid - if (resource.url && resource.debouncedUrl && validateURL(resource.url)) { - - console.log(`Checking activity for URL: ${resource.url}`); - checkURLActive(resource.debouncedUrl) - .then((response) => { - - // Log the entire response for debugging - console.log(`Response from checkURLActive for ${resource.debouncedUrl}:`, response); - - const isActive = response.data; - - - setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: isActive })); - - // Log based on the isActive value - if (isActive) { - console.log(`URL found active: ${resource.url}`); - } else { - console.log(`URL found inactive: ${resource.url}`); - } - } - ) - } else if (!resource.url || !validateURL(resource.url)) { - // Handles both invalid and empty URLs + if (resource.url && validateURL(resource.url)) { + debounced(resource); + } + // else if (!resource.url || !validateURL(resource.url)) { + // // Handles both invalid and empty URLs - setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: false })); - console.log(`URL is invalid or empty, marked as inactive: ${resource.url}`); + // setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: false })); + // console.log(`URL is invalid or empty, marked as inactive: ${resource.url}`); - } + // } }); return () => { mounted.current = false; }; - }, [resources, checkURLActive, debouncedResourceUrls ]); + }, [resources, checkURLActive, debounced]); function addResource() { updateResources(resources.concat(deepCopy(emptyResource))); From 7fa9b7b3bca4045050508da92e2fa0165676b528 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Wed, 24 Jan 2024 16:49:54 -0800 Subject: [PATCH 16/31] url warning validation - broken --- src/components/Tabs/SubmitTab.jsx | 5 ++- src/utils/validate.js | 59 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index e8f108c3..6cc6603f 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -16,7 +16,7 @@ import { useParams } from "react-router-dom"; import { paperClass } from "../FormComponents/QuestionStyles"; import { En, Fr, I18n } from "../I18n"; -import { getErrorsByTab, recordIsValid } from "../../utils/validate"; +import { getErrorsByTab, recordIsValid, getUrlWarningsByTab } from "../../utils/validate"; import tabs from "../../utils/tabs"; import GetRegionInfo from "../FormComponents/Regions"; @@ -27,9 +27,12 @@ const SubmitTab = ({ record, submitRecord }) => { const { language } = useParams(); const validationErrors = getErrorsByTab(record); + const validationWarnings = getUrlWarningsByTab(record); const submitted = record.status === "submitted"; const regionInfo = GetRegionInfo(); + console.log(validationWarnings) + return ( diff --git a/src/utils/validate.js b/src/utils/validate.js index c8f51b30..13b5ac74 100644 --- a/src/utils/validate.js +++ b/src/utils/validate.js @@ -1,8 +1,20 @@ import validator from "validator"; +import firebase from "../firebase"; + export const validateEmail = (email) => !email || validator.isEmail(email); export const validateURL = (url) => !url || validator.isURL(url); +const checkURLActive = firebase.functions().httpsCallable('checkURLActive'); +checkURLActive('www.google.com') + .then((result) => { + const {data} = result.data; + console.log(data) + }) + .catch((err) => { + console.log(err) + }) + // See https://stackoverflow.com/a/48524047/7416701 export const doiRegexp = /^(https:\/\/doi.org\/)?10\.\d{4,9}\/[-._;()/:A-Z0-9]+$/i; function isValidHttpUrl(string) { @@ -235,6 +247,30 @@ const validators = { }, }, }; + + const warnings = { + distribution: { + tab: "resources", + validation: (val) => + Array.isArray(val) && + val.filter( async (dist) => { + if (dist.url || validator.isURL(dist.url)) + {const status = await checkURLActive(dist.url) + console.log(`warnings status log: ${JSON.stringify(status)}`) + return !status} + return true + }) > 0, + + + error: { + en: + "Must have at least one resource. If a URL is included it must be valid.", + fr: + "Doit avoir au moins une ressource. Vérifiez si votre URL est valide.", + }, + }, + }; + export const validateField = (record, fieldName) => { const valueToValidate = record[fieldName]; // if no validator funciton exists, then it is not a required field @@ -259,6 +295,29 @@ export const getErrorsByTab = (record) => { }, {}); }; +export const validateFieldWarning = (record, fieldName) => { + const valueToValidate = record[fieldName]; + + console.log(`validateFieldWarning: ${JSON.stringify(valueToValidate)}`) + // if no validator funciton exists, then it is not a required field + const validationFunction = + (warnings[fieldName] && warnings[fieldName].validation) || (() => true); + + return validationFunction && validationFunction(valueToValidate, record); +}; + +// needs to be async... +export const getUrlWarningsByTab = (record) => { + + // get warnings + const fieldWarnings = Object.keys(warnings); + // get inactive urls - maybe from validateField function or write a similar function? + const inactiveUrls = fieldWarnings.filter( async (field) => { const urlWarning = await validateFieldWarning(record, field) + return urlWarning;}); + console.log(`inactive urls: ${JSON.stringify(inactiveUrls)}`); + return inactiveUrls; +} + export const percentValid = (record) => { const fields = Object.keys(validators); const numTotalRequired = fields.filter((field) => !validators[field].optional) From 377f86b0b388a8b39e012cf23b723588607fa9d9 Mon Sep 17 00:00:00 2001 From: Matthew Foster Date: Thu, 25 Jan 2024 11:14:48 -0800 Subject: [PATCH 17/31] super hack --- src/components/Tabs/SubmitTab.jsx | 87 +++++++++++++++++++++++++++++-- src/utils/validate.js | 73 ++++++++++---------------- 2 files changed, 110 insertions(+), 50 deletions(-) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index 6cc6603f..5cf4cd8f 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Paper, @@ -16,22 +16,55 @@ import { useParams } from "react-router-dom"; import { paperClass } from "../FormComponents/QuestionStyles"; import { En, Fr, I18n } from "../I18n"; -import { getErrorsByTab, recordIsValid, getUrlWarningsByTab } from "../../utils/validate"; +import { getErrorsByTab, recordIsValid, warnings, validateFieldWarning } from "../../utils/validate"; import tabs from "../../utils/tabs"; import GetRegionInfo from "../FormComponents/Regions"; const SubmitTab = ({ record, submitRecord }) => { const [isSubmitting, setSubmitting] = useState(false); + const [validationWarnings, setValidationWarnings] = useState(false); const { language } = useParams(); const validationErrors = getErrorsByTab(record); - const validationWarnings = getUrlWarningsByTab(record); const submitted = record.status === "submitted"; const regionInfo = GetRegionInfo(); - console.log(validationWarnings) + useEffect(() => { + // some good info https://devtrium.com/posts/async-functions-useeffect + + // we could move this function outside of useEffect if we wrap it in a useCallback + // see https://devtrium.com/posts/async-functions-useeffect#what-if-you-need-to-extract-the-function-outside-useeffect + const getUrlWarningsByTab = async (record) => { + const fields = Object.keys(warnings); + const validatedFields = {}; + for (const field of fields) { + const res = await validateFieldWarning(record, field); + validatedFields[field]= res; + }; + const inactiveUrls = fields.filter((field) => { + return validatedFields[field]} + ); + const fieldWarningInfo = inactiveUrls.map((field) => { + const { error, tab } = warnings[field]; + return { error, tab }; + }); + + console.log(`inactive urls: ${JSON.stringify(inactiveUrls)}`); + + const fieldWarningInfoReduced = fieldWarningInfo.reduce((acc, { error, tab }) => { + if (!acc[tab]) acc[tab] = []; + acc[tab].push(error); + return acc; + }, {}); + + setValidationWarnings(fieldWarningInfoReduced); + } + + getUrlWarningsByTab(record); + + }, [record]) return ( @@ -160,8 +193,54 @@ const SubmitTab = ({ record, submitRecord }) => { )} + + {validationWarnings && Object.keys(validationWarnings).length > 0 ? ( + <> + + + + + Some warnings were generated for the following fields. + Please review and fix the warnings as needed befor submitting + the record. + + + Certains avertissements ont été générés pour les champs suivants. + Veuillez examiner et corriger les avertissements si nécessaire + avant de soumettre l'enregistrement. + + + + + + + {Object.keys(validationWarnings).map((tab) => ( +
+ + {tabs[tab][language]} + + + {validationWarnings[tab].map( + ({ [language]: error }, i) => ( + + + + ) + )} + +
+ ))} +
+ + + ): + (" ")} )} + + + +
); diff --git a/src/utils/validate.js b/src/utils/validate.js index 13b5ac74..3bfa8777 100644 --- a/src/utils/validate.js +++ b/src/utils/validate.js @@ -6,14 +6,6 @@ export const validateEmail = (email) => !email || validator.isEmail(email); export const validateURL = (url) => !url || validator.isURL(url); const checkURLActive = firebase.functions().httpsCallable('checkURLActive'); -checkURLActive('www.google.com') - .then((result) => { - const {data} = result.data; - console.log(data) - }) - .catch((err) => { - console.log(err) - }) // See https://stackoverflow.com/a/48524047/7416701 export const doiRegexp = /^(https:\/\/doi.org\/)?10\.\d{4,9}\/[-._;()/:A-Z0-9]+$/i; @@ -248,29 +240,6 @@ const validators = { }, }; - const warnings = { - distribution: { - tab: "resources", - validation: (val) => - Array.isArray(val) && - val.filter( async (dist) => { - if (dist.url || validator.isURL(dist.url)) - {const status = await checkURLActive(dist.url) - console.log(`warnings status log: ${JSON.stringify(status)}`) - return !status} - return true - }) > 0, - - - error: { - en: - "Must have at least one resource. If a URL is included it must be valid.", - fr: - "Doit avoir au moins une ressource. Vérifiez si votre URL est valide.", - }, - }, - }; - export const validateField = (record, fieldName) => { const valueToValidate = record[fieldName]; // if no validator funciton exists, then it is not a required field @@ -295,28 +264,40 @@ export const getErrorsByTab = (record) => { }, {}); }; -export const validateFieldWarning = (record, fieldName) => { - const valueToValidate = record[fieldName]; - console.log(`validateFieldWarning: ${JSON.stringify(valueToValidate)}`) +export const warnings = { + distribution: { + tab: "resources", + validation: async (val) => { + val = await Promise.all( + val.map(async (dist) => { + const res = await checkURLActive(dist.url); + return {...dist, status:res.data}; + })); + const filterVal = val.filter((dist) => !dist.status) + return filterVal.length + }, + error: { + en: + "Resource URL is not accessible. This could be because it has not been created yet or is otherwise unreachable", + fr: + "L'URL de la ressource n'est pas accessible. Cela peut être dû au fait qu'il n'a pas encore été créé ou qu'il est autrement inaccessible.", + }, + }, +}; + + +export const validateFieldWarning = async (record, fieldName) => { + const valueToValidate = record[fieldName]; // if no validator funciton exists, then it is not a required field const validationFunction = (warnings[fieldName] && warnings[fieldName].validation) || (() => true); - - return validationFunction && validationFunction(valueToValidate, record); + + const res = await validationFunction(valueToValidate, record); + return validationFunction && res; }; -// needs to be async... -export const getUrlWarningsByTab = (record) => { - // get warnings - const fieldWarnings = Object.keys(warnings); - // get inactive urls - maybe from validateField function or write a similar function? - const inactiveUrls = fieldWarnings.filter( async (field) => { const urlWarning = await validateFieldWarning(record, field) - return urlWarning;}); - console.log(`inactive urls: ${JSON.stringify(inactiveUrls)}`); - return inactiveUrls; -} export const percentValid = (record) => { const fields = Object.keys(validators); From 8671b68f08b0d00d04c6cdba26589d3ad8526a96 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 12:22:30 -0800 Subject: [PATCH 18/31] remove unused custom debounce util func --- src/utils/misc.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/utils/misc.js b/src/utils/misc.js index 2ce9ce75..558045e1 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -5,35 +5,6 @@ export function deepEquals(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } -/** - * Creates a debounced version of a function. - * - * @param {Function} mainFunction - The function to debounce. - * @param {number} delay - The amount of time (in milliseconds) to delay. - * @return {Function} A debounced version of the specified function. - */ -export const debounce = (mainFunction, delay) => { - let timer; - - const debouncedFunction = (...args) => { - // Return a promise that resolves or rejects based on the mainFunction's execution - return new Promise((resolve, reject) => { - // Clear existing timer to reset the debounce period - clearTimeout(timer); - - // Set a new timer to delay the execution of mainFunction - timer = setTimeout(() => { - // Execute mainFunction and handle its promise - Promise.resolve(mainFunction(...args)) - .then(resolve) - .catch(reject); - }, delay); - }); - }; - - return debouncedFunction; -} - /* Convert firebase to javascript, mostly just used to get real array elements */ From e98c5f94cb1352bac002bcf7b73f7b865d7ed784 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 12:23:02 -0800 Subject: [PATCH 19/31] reduce debounce time from 3s to 1s --- src/components/FormComponents/Resources.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 896c4efe..18c2014f 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -30,7 +30,7 @@ const Resources = ({ updateResources, resources, disabled }) => { const response = await checkURLActive(resource.url) console.log(response.data); setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) - }, 3000); + }, 1000); useEffect( () => { From 9d243dfa0b293c3c14f3e1634b1a5117a8b50c57 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 12:49:26 -0800 Subject: [PATCH 20/31] fix 'record' naming collision as per linter error --- src/components/Tabs/SubmitTab.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index 5cf4cd8f..5a123c70 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -36,11 +36,11 @@ const SubmitTab = ({ record, submitRecord }) => { // we could move this function outside of useEffect if we wrap it in a useCallback // see https://devtrium.com/posts/async-functions-useeffect#what-if-you-need-to-extract-the-function-outside-useeffect - const getUrlWarningsByTab = async (record) => { + const getUrlWarningsByTab = async (recordObj) => { const fields = Object.keys(warnings); const validatedFields = {}; for (const field of fields) { - const res = await validateFieldWarning(record, field); + const res = await validateFieldWarning(recordObj, field); validatedFields[field]= res; }; const inactiveUrls = fields.filter((field) => { From 510cf098aa48c876fce165a423f372cdcd84da64 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 13:07:00 -0800 Subject: [PATCH 21/31] fix linter warning Assignment to function parameter 'val' --- src/utils/validate.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/validate.js b/src/utils/validate.js index 3bfa8777..240f25ac 100644 --- a/src/utils/validate.js +++ b/src/utils/validate.js @@ -269,12 +269,13 @@ export const warnings = { distribution: { tab: "resources", validation: async (val) => { - val = await Promise.all( + const processedVal = await Promise.all( val.map(async (dist) => { const res = await checkURLActive(dist.url); return {...dist, status:res.data}; - })); - const filterVal = val.filter((dist) => !dist.status) + }) + ); + const filterVal = processedVal.filter((dist) => !dist.status) return filterVal.length }, error: { From c1b8cf4e69352bb710296c6563890040547853b7 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 13:17:25 -0800 Subject: [PATCH 22/31] Refactor to replace for...of loop with array methods as per linter warnings --- src/components/Tabs/SubmitTab.jsx | 76 +++++++++++++++++-------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index 5a123c70..bab36484 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -16,7 +16,12 @@ import { useParams } from "react-router-dom"; import { paperClass } from "../FormComponents/QuestionStyles"; import { En, Fr, I18n } from "../I18n"; -import { getErrorsByTab, recordIsValid, warnings, validateFieldWarning } from "../../utils/validate"; +import { + getErrorsByTab, + recordIsValid, + warnings, + validateFieldWarning, +} from "../../utils/validate"; import tabs from "../../utils/tabs"; import GetRegionInfo from "../FormComponents/Regions"; @@ -38,33 +43,40 @@ const SubmitTab = ({ record, submitRecord }) => { // see https://devtrium.com/posts/async-functions-useeffect#what-if-you-need-to-extract-the-function-outside-useeffect const getUrlWarningsByTab = async (recordObj) => { const fields = Object.keys(warnings); - const validatedFields = {}; - for (const field of fields) { - const res = await validateFieldWarning(recordObj, field); - validatedFields[field]= res; - }; + + const validationPromises = fields.map((field) => + validateFieldWarning(recordObj, field) + ); + + const validationResults = await Promise.all(validationPromises); + + const validatedFields = fields.reduce((acc, field, index) => { + acc[field] = validationResults[index]; + return acc; + }, {}); + const inactiveUrls = fields.filter((field) => { - return validatedFields[field]} - ); + return validatedFields[field]; + }); const fieldWarningInfo = inactiveUrls.map((field) => { const { error, tab } = warnings[field]; return { error, tab }; }); - console.log(`inactive urls: ${JSON.stringify(inactiveUrls)}`); - - const fieldWarningInfoReduced = fieldWarningInfo.reduce((acc, { error, tab }) => { - if (!acc[tab]) acc[tab] = []; - acc[tab].push(error); - return acc; - }, {}); + const fieldWarningInfoReduced = fieldWarningInfo.reduce( + (acc, { error, tab }) => { + if (!acc[tab]) acc[tab] = []; + acc[tab].push(error); + return acc; + }, + {} + ); setValidationWarnings(fieldWarningInfoReduced); - } + }; getUrlWarningsByTab(record); - - }, [record]) + }, [record]); return ( @@ -194,33 +206,35 @@ const SubmitTab = ({ record, submitRecord }) => { )} - {validationWarnings && Object.keys(validationWarnings).length > 0 ? ( + {validationWarnings && + Object.keys(validationWarnings).length > 0 ? ( <> Some warnings were generated for the following fields. - Please review and fix the warnings as needed befor submitting - the record. + Please review and fix the warnings as needed befor + submitting the record. - Certains avertissements ont été générés pour les champs suivants. - Veuillez examiner et corriger les avertissements si nécessaire - avant de soumettre l'enregistrement. + Certains avertissements ont été générés pour les champs + suivants. Veuillez examiner et corriger les + avertissements si nécessaire avant de soumettre + l'enregistrement. - {Object.keys(validationWarnings).map((tab) => ( + {Object.keys(validationWarnings).map((tab) => (
{tabs[tab][language]} - {validationWarnings[tab].map( + {validationWarnings[tab].map( ({ [language]: error }, i) => ( @@ -232,15 +246,11 @@ const SubmitTab = ({ record, submitRecord }) => { ))} - - ): - (" ")} + ) : ( + " " + )} )} - - - - ); From e5b7188908dc954077e8bf809ea965a08bfe2177 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 14:05:36 -0800 Subject: [PATCH 23/31] fix memory leak errors --- src/components/FormComponents/ContactEditor.jsx | 16 ++++++++++++---- src/components/FormComponents/Resources.jsx | 5 +++-- src/components/Tabs/SubmitTab.jsx | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/components/FormComponents/ContactEditor.jsx b/src/components/FormComponents/ContactEditor.jsx index 49501806..e6e6653a 100644 --- a/src/components/FormComponents/ContactEditor.jsx +++ b/src/components/FormComponents/ContactEditor.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { TextField, @@ -45,6 +45,7 @@ const ContactEditor = ({ updateContactRor, updateContactOrcid, }) => { + const mounted = useRef(false); const orgEmailValid = validateEmail(value.orgEmail); const indEmailValid = validateEmail(value.indEmail); const orgURLValid = validateURL(value.orgURL); @@ -64,19 +65,26 @@ const ContactEditor = ({ newInputValue.startsWith("http") && !newInputValue.includes("ror.org") ) { - setRorSearchActive(false); + if (mounted.current) setRorSearchActive(false); } else { fetch(`https://api.ror.org/organizations?query=${newInputValue}`) .then((response) => response.json()) - .then((response) => setRorOptions(response.items)) - .then(() => setRorSearchActive(false)); + .then((response) => {if (mounted.current) setRorOptions(response.items)}) + .then(() => {if (mounted.current) setRorSearchActive(false)}); } } useEffect(() => { + + mounted.current = true; + if (debouncedRorInputValue) { updateRorOptions(debouncedRorInputValue); } + + return () => { + mounted.current = false; + }; }, [debouncedRorInputValue]); return ( diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 18c2014f..915d2429 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -29,11 +29,12 @@ const Resources = ({ updateResources, resources, disabled }) => { const debounced = useDebouncedCallback(async (resource) => { const response = await checkURLActive(resource.url) console.log(response.data); - setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) + if (mounted.current) + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) }, 1000); useEffect( () => { - + mounted.current = true mounted.current = true; console.log(`resources array: ${JSON.stringify(resources)}`) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index bab36484..84288900 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Paper, @@ -27,6 +27,7 @@ import tabs from "../../utils/tabs"; import GetRegionInfo from "../FormComponents/Regions"; const SubmitTab = ({ record, submitRecord }) => { + const mounted = useRef(false); const [isSubmitting, setSubmitting] = useState(false); const [validationWarnings, setValidationWarnings] = useState(false); @@ -37,6 +38,9 @@ const SubmitTab = ({ record, submitRecord }) => { const regionInfo = GetRegionInfo(); useEffect(() => { + + mounted.current = true + // some good info https://devtrium.com/posts/async-functions-useeffect // we could move this function outside of useEffect if we wrap it in a useCallback @@ -71,11 +75,16 @@ const SubmitTab = ({ record, submitRecord }) => { }, {} ); - - setValidationWarnings(fieldWarningInfoReduced); + if (mounted.current) + setValidationWarnings(fieldWarningInfoReduced); }; getUrlWarningsByTab(record); + + return () => { + mounted.current = false; + }; + }, [record]); return ( From 423361f7a325989023b719efadbb12ba3192b93a Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 14:06:17 -0800 Subject: [PATCH 24/31] reduce debounce time --- src/components/FormComponents/Resources.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 915d2429..15c9a9df 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -31,7 +31,7 @@ const Resources = ({ updateResources, resources, disabled }) => { console.log(response.data); if (mounted.current) setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) - }, 1000); + }, 500); useEffect( () => { mounted.current = true From e2515177f4db06603f1e22665154f19213cb7fff Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 14:11:35 -0800 Subject: [PATCH 25/31] cleanup logs and unused code --- src/components/FormComponents/Resources.jsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 15c9a9df..f8cecaa9 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -28,28 +28,19 @@ const Resources = ({ updateResources, resources, disabled }) => { // Debounce callback for URL updates const debounced = useDebouncedCallback(async (resource) => { const response = await checkURLActive(resource.url) - console.log(response.data); if (mounted.current) setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) }, 500); useEffect( () => { mounted.current = true - mounted.current = true; - console.log(`resources array: ${JSON.stringify(resources)}`) resources.forEach( (resource) => { - // Check if the URL is not empty and valid + if (resource.url && validateURL(resource.url)) { debounced(resource); } - // else if (!resource.url || !validateURL(resource.url)) { - // // Handles both invalid and empty URLs - // setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: false })); - // console.log(`URL is invalid or empty, marked as inactive: ${resource.url}`); - - // } }); return () => { @@ -77,8 +68,6 @@ const Resources = ({ updateResources, resources, disabled }) => { const nameLabel = ; const descriptionLabel = ; - console.log(`url is active object: ${JSON.stringify(urlIsActive)}`) - return (
{resources.map((resourceItem = deepCopy(emptyResource), i) => { From ea8303fc062d2d7420cea6a855bf335fede741a5 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 14:13:04 -0800 Subject: [PATCH 26/31] remove irrelevant comment --- src/components/Tabs/SubmitTab.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index 84288900..0de4b486 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -41,10 +41,6 @@ const SubmitTab = ({ record, submitRecord }) => { mounted.current = true - // some good info https://devtrium.com/posts/async-functions-useeffect - - // we could move this function outside of useEffect if we wrap it in a useCallback - // see https://devtrium.com/posts/async-functions-useeffect#what-if-you-need-to-extract-the-function-outside-useeffect const getUrlWarningsByTab = async (recordObj) => { const fields = Object.keys(warnings); From 1f9378807c963164f198d3b92296dd04ced54d7e Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 15:38:31 -0800 Subject: [PATCH 27/31] attempt to solve debounce bug --- src/components/FormComponents/Resources.jsx | 36 +++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index f8cecaa9..6fd4aade 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -22,6 +22,7 @@ const Resources = ({ updateResources, resources, disabled }) => { const mounted = useRef(false); const { checkURLActive } = useContext(UserContext); const [urlIsActive, setUrlIsActive] = useState({}); + const [isInitialLoad, setIsInitialLoad] = useState(true); const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; @@ -30,23 +31,44 @@ const Resources = ({ updateResources, resources, disabled }) => { const response = await checkURLActive(resource.url) if (mounted.current) setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) - }, 500); + }, 3000); useEffect( () => { mounted.current = true - resources.forEach( (resource) => { + const checkURLStatus = async (resource) => { + const response = await checkURLActive(resource.url); + if (mounted.current) + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })); + }; + + // if its not initial load, use debounced function + if (!isInitialLoad) { + resources.forEach((resource) => { + if (resource.url && validateURL(resource.url)) { + debounced(resource); + } + }); + } + + // After the first render, set isInitialLoad to false + if (isInitialLoad) { + + // if initial load, don't use debounce + resources.forEach((resource) => { + if (resource.url && validateURL(resource.url)) { + checkURLStatus(resource); + } + }); - if (resource.url && validateURL(resource.url)) { - debounced(resource); - } + setIsInitialLoad(false); - }); + } return () => { mounted.current = false; }; - }, [resources, checkURLActive, debounced]); + }, [resources, checkURLActive, debounced, isInitialLoad]); function addResource() { updateResources(resources.concat(deepCopy(emptyResource))); From 6a248dce5fc8d60efb105c9d55a7d2383dd61626 Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 15:47:53 -0800 Subject: [PATCH 28/31] reduce debounce time --- src/components/FormComponents/Resources.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index 6fd4aade..d25cc761 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -31,7 +31,7 @@ const Resources = ({ updateResources, resources, disabled }) => { const response = await checkURLActive(resource.url) if (mounted.current) setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) - }, 3000); + }, 500); useEffect( () => { mounted.current = true From deb498235d35590c871a270efd86ca214d18233b Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Thu, 25 Jan 2024 17:02:25 -0800 Subject: [PATCH 29/31] add headings for errors & warnings on submit page --- src/components/Tabs/SubmitTab.jsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/Tabs/SubmitTab.jsx b/src/components/Tabs/SubmitTab.jsx index 0de4b486..275fe4de 100644 --- a/src/components/Tabs/SubmitTab.jsx +++ b/src/components/Tabs/SubmitTab.jsx @@ -174,6 +174,16 @@ const SubmitTab = ({ record, submitRecord }) => { ) : ( <> + {/* Errors Section */} + + + + Errors + Erreurs + + + + @@ -214,6 +224,16 @@ const SubmitTab = ({ record, submitRecord }) => { {validationWarnings && Object.keys(validationWarnings).length > 0 ? ( <> + {/* Warnings Section Heading */} + + + + Warnings + Avertissements + + + + From c10710609d0a130ba1d023ab85137285f812ac10 Mon Sep 17 00:00:00 2001 From: Matthew Foster Date: Thu, 25 Jan 2024 20:12:05 -0800 Subject: [PATCH 30/31] fix debounce on resouces --- package.json | 1 + src/components/FormComponents/Resources.jsx | 26 ++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 508adbea..5b0965ee 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "firebase-admin": "^11.5.0", "firebase-functions": "^4.2.1", "javascript-time-ago": "^2.3.4", + "just-debounce-it": "1.0.1", "leaflet": "^1.6.0", "leaflet-draw": "^1.0.4", "nunjucks": "^3.2.3", diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index f8cecaa9..ca95ca76 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -7,7 +7,7 @@ import { } from "@material-ui/icons"; import { Button, Grid, Paper, TextField } from "@material-ui/core"; import validator from "validator"; -import { useDebouncedCallback } from 'use-debounce'; +import debounce from "just-debounce-it"; import { En, Fr, I18n } from "../I18n"; import BilingualTextInput from "./BilingualTextInput"; @@ -24,29 +24,29 @@ const Resources = ({ updateResources, resources, disabled }) => { const [urlIsActive, setUrlIsActive] = useState({}); const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; - + const debouncePool = useRef({}); // Debounce callback for URL updates - const debounced = useDebouncedCallback(async (resource) => { - const response = await checkURLActive(resource.url) - if (mounted.current) - setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) - }, 500); useEffect( () => { mounted.current = true - - resources.forEach( (resource) => { - + resources.forEach( (resource, idx, arr) => { if (resource.url && validateURL(resource.url)) { - debounced(resource); + if (!debouncePool.current[idx]){ + debouncePool.current[idx] = debounce( async (resource) => { + const response = await checkURLActive(resource.url) + if (mounted.current){ + setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) + } + }, 500); + } + debouncePool.current[idx](resource); } - }); return () => { mounted.current = false; }; - }, [resources, checkURLActive, debounced]); + }, [resources, checkURLActive]); function addResource() { updateResources(resources.concat(deepCopy(emptyResource))); From 2c97aba2cf87b2f0a6948e0615348c5778c1278a Mon Sep 17 00:00:00 2001 From: Austen Sorochak Date: Fri, 26 Jan 2024 13:46:13 -0800 Subject: [PATCH 31/31] cleanup useEffect --- package-lock.json | 11 +++++++++++ src/components/FormComponents/Resources.jsx | 19 ++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b999f47..d280eec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "firebase-admin": "^11.5.0", "firebase-functions": "^4.2.1", "javascript-time-ago": "^2.3.4", + "just-debounce-it": "1.0.1", "leaflet": "^1.6.0", "leaflet-draw": "^1.0.4", "nunjucks": "^3.2.3", @@ -12697,6 +12698,11 @@ "node": ">=4.0" } }, + "node_modules/just-debounce-it": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-1.0.1.tgz", + "integrity": "sha512-82mt758EWKlkdGHodniikRrbtcUfYYH91d5Vt1sVYHt+RQqvv0EqQXpiZ3Q3BasqH59YaxUXPpPjUPOL2NRF5g==" + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -32397,6 +32403,11 @@ "object.assign": "^4.1.0" } }, + "just-debounce-it": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-1.0.1.tgz", + "integrity": "sha512-82mt758EWKlkdGHodniikRrbtcUfYYH91d5Vt1sVYHt+RQqvv0EqQXpiZ3Q3BasqH59YaxUXPpPjUPOL2NRF5g==" + }, "jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", diff --git a/src/components/FormComponents/Resources.jsx b/src/components/FormComponents/Resources.jsx index aab27600..05682365 100644 --- a/src/components/FormComponents/Resources.jsx +++ b/src/components/FormComponents/Resources.jsx @@ -22,32 +22,29 @@ const Resources = ({ updateResources, resources, disabled }) => { const mounted = useRef(false); const { checkURLActive } = useContext(UserContext); const [urlIsActive, setUrlIsActive] = useState({}); - const [isInitialLoad, setIsInitialLoad] = useState(true); const emptyResource = { url: "", name: "", description: { en: "", fr: "" } }; const debouncePool = useRef({}); - // Debounce callback for URL updates useEffect( () => { + mounted.current = true - resources.forEach( (resource, idx, arr) => { + + resources.forEach( (resource, idx) => { + if (resource.url && validateURL(resource.url)) { if (!debouncePool.current[idx]){ - debouncePool.current[idx] = debounce( async (resource) => { - const response = await checkURLActive(resource.url) + debouncePool.current[idx] = debounce( async (innerResource) => { + const response = await checkURLActive(innerResource.url) if (mounted.current){ - setUrlIsActive((prevStatus) => ({ ...prevStatus, [resource.url]: response.data })) + setUrlIsActive((prevStatus) => ({ ...prevStatus, [innerResource.url]: response.data })) } }, 500); } debouncePool.current[idx](resource); } }); - - setIsInitialLoad(false); - - } - + return () => { mounted.current = false; };