diff --git a/firebase-functions/functions/index.js b/firebase-functions/functions/index.js
index 0323dceb..d94af8f7 100644
--- a/firebase-functions/functions/index.js
+++ b/firebase-functions/functions/index.js
@@ -1,5 +1,6 @@
const admin = require("firebase-admin");
const { translate } = require("./translate");
+const { checkURLActive } = require("./serverUtils");
const { createDraftDoi, updateDraftDoi, deleteDraftDoi, getDoiStatus } = require("./datacite");
const { notifyReviewer, notifyUser } = require("./notify");
const {
@@ -24,3 +25,4 @@ exports.createDraftDoi = createDraftDoi;
exports.deleteDraftDoi = deleteDraftDoi;
exports.updateDraftDoi = updateDraftDoi;
exports.getDoiStatus = getDoiStatus;
+exports.checkURLActive = checkURLActive;
diff --git a/firebase-functions/functions/serverUtils.js b/firebase-functions/functions/serverUtils.js
new file mode 100644
index 00000000..7ba0676e
--- /dev/null
+++ b/firebase-functions/functions/serverUtils.js
@@ -0,0 +1,26 @@
+const functions = require("firebase-functions");
+const fetch = require('node-fetch');
+
+// Function to check if a given URL is active
+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" });
+ functions.logger.log(`Fetch response status for ${url}:`, response.status);
+ return response.ok; // Return true if response is OK, otherwise false
+ } catch (error) {
+ functions.logger.error('Error in checkURLActive for URL:', url, error);
+ return false; // Return false if an error occurs
+ }
+})
\ No newline at end of file
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/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/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 3030b31e..05682365 100644
--- a/src/components/FormComponents/Resources.jsx
+++ b/src/components/FormComponents/Resources.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useContext, useEffect, useState, useRef } from "react";
import {
Add,
Delete,
@@ -7,18 +7,49 @@ import {
} from "@material-ui/icons";
import { Button, Grid, Paper, TextField } from "@material-ui/core";
import validator from "validator";
+import debounce from "just-debounce-it";
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);
+import { UserContext } from "../../providers/UserProvider";
const Resources = ({ updateResources, resources, disabled }) => {
+
+ const mounted = useRef(false);
+ const { checkURLActive } = useContext(UserContext);
+ const [urlIsActive, setUrlIsActive] = useState({});
const emptyResource = { url: "", name: "", description: { en: "", fr: "" } };
+ const debouncePool = useRef({});
+
+ useEffect( () => {
+
+ mounted.current = true
+
+ resources.forEach( (resource, idx) => {
+
+ if (resource.url && validateURL(resource.url)) {
+ if (!debouncePool.current[idx]){
+ debouncePool.current[idx] = debounce( async (innerResource) => {
+ const response = await checkURLActive(innerResource.url)
+ if (mounted.current){
+ setUrlIsActive((prevStatus) => ({ ...prevStatus, [innerResource.url]: response.data }))
+ }
+ }, 500);
+ }
+ debouncePool.current[idx](resource);
+ }
+ });
+
+ return () => {
+ mounted.current = false;
+ };
+ }, [resources, checkURLActive]);
+
function addResource() {
updateResources(resources.concat(deepCopy(emptyResource)));
}
@@ -35,13 +66,15 @@ const Resources = ({ updateResources, resources, disabled }) => {
resources.splice(newIndex, 0, element);
updateResources(resources);
}
+
const nameLabel =