Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
48ae5a7
In theory this only opens the registration modal in the right circums…
gregorydlogan May 2, 2026
c12f608
Revert me for further work
gregorydlogan May 7, 2026
45f695b
Removing summary, since it's not needed here
gregorydlogan May 8, 2026
92f70b3
Switching to useEffect, suggested in the review
gregorydlogan May 8, 2026
094d207
Removing these since this part of the service does not care about this.
gregorydlogan May 8, 2026
298d1a8
Modal now appears properly, and creates 'Registration' notifications …
gregorydlogan Jun 10, 2026
6950c89
Merge remote-tracking branch 'upstream/r/19.x' into t/registration-modal
gregorydlogan Jul 13, 2026
7bbefff
Combining useEffects
gregorydlogan Jul 13, 2026
833d154
Moving these dispatches out to prevent possible infinite loops
gregorydlogan Jul 15, 2026
4720639
Fixing some tabs vs spaces
gregorydlogan Jul 15, 2026
f22acfe
Adding translation strings to notifications
gregorydlogan Jul 15, 2026
f1197ba
Replacing tabs with spaces
gregorydlogan Jul 15, 2026
2d69560
Renaming dumb type to something better
gregorydlogan Jul 15, 2026
8bfd332
Removing leftovers
gregorydlogan Jul 15, 2026
e025708
Renaming isRegistering to something a little more sane. The variable…
gregorydlogan Jul 15, 2026
20b62d1
These are not needed
gregorydlogan Jul 15, 2026
3247ac4
Only opening the modal if the registration data has loaded, otherwise…
gregorydlogan Jul 15, 2026
ecace10
Combining the last of the old utils script into the slice, moving sel…
gregorydlogan Jul 23, 2026
5a53c12
Harmonizing formatting to existing file structures
gregorydlogan Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 79 additions & 14 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import languages from "../i18n/languages";
import opencastLogo from "../img/opencast-white.svg?url";
import { setSpecificServiceFilter } from "../slices/tableFilterSlice";
import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors";
import {
getRegistrationLoaded,
getRegistration,
getAbleToRegister,
getAgreedLatestToU,
} from "../selectors/registrationSelectors";
import {
getOrgProperties,
getUserBasicInfo,
Expand All @@ -20,6 +26,11 @@ import HotKeyCheatSheet from "./shared/HotKeyCheatSheet";
import { useHotkeys } from "react-hotkeys-hook";
import { useAppDispatch, useAppSelector } from "../store";
import { HealthStatus, fetchHealthStatus } from "../slices/healthSlice";
import {
fetchRegistration,
fetchLatestToU,
fetchIsUpToDate,
} from "../slices/registrationSlice";
import { UserInfoState } from "../slices/userInfoSlice";
import { Tooltip } from "./shared/Tooltip";
import { HiOutlineTranslate } from "react-icons/hi";
Expand Down Expand Up @@ -54,7 +65,11 @@ const Header = () => {

const healthStatus = useAppSelector(state => getHealthStatus(state));
const errorCounter = useAppSelector(state => getErrorCount(state));
const isAbleToRegister = useAppSelector(state => getAbleToRegister(state));
const agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state));
const user = useAppSelector(state => getUserInformation(state));
const registrationLoaded = useAppSelector(state => getRegistrationLoaded(state));
const registration = useAppSelector(state => getRegistration(state));
const orgProperties = useAppSelector(state => getOrgProperties(state));
const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true";

Expand All @@ -66,6 +81,10 @@ const Header = () => {
setMenuHelp(false);
};

const hideNotificationMenu = () => {
setMenuNotify(false);
};

const showRegistrationModal = () => {
registrationModalRef.current?.open();
};
Expand Down Expand Up @@ -138,18 +157,24 @@ const Header = () => {
}, []);

useEffect(() => {
if (!user) { return; }

const isAdmin = user.isAdmin || user.isOrgAdmin;
const isLocalhost = window.location.hostname === "localhost";
const lastDismissed = localStorage.getItem("adopterModalDismissed");
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
const dismissedLongEnough = !lastDismissed || Date.now() - parseInt(lastDismissed) > THIRTY_DAYS;

if (isAdmin && !isLocalhost && dismissedLongEnough) {
showRegistrationModal();
}
}, [user]);
dispatch(fetchRegistration());
dispatch(fetchLatestToU());
dispatch(fetchIsUpToDate());
}, [dispatch]);

useEffect(() => {
if (!user) { return; }

const isAdmin = user.isAdmin || user.isOrgAdmin;
const isLocalhost = window.location.hostname === "localhost";
const lastDismissed = localStorage.getItem("adopterModalDismissed");
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
const dismissedLongEnough = !lastDismissed || Date.now() - parseInt(lastDismissed) > THIRTY_DAYS;

if (isAdmin && !isLocalhost && dismissedLongEnough && registrationLoaded && registration == null) {
showRegistrationModal();
}
}, [user, registration, registrationLoaded]);
return (
<>
<header className="primary-header">
Expand Down Expand Up @@ -214,9 +239,9 @@ const Header = () => {
<Tooltip active={!displayMenuNotify} title={t("SYSTEM_NOTIFICATIONS")}>
<BaseButton onClick={() => setMenuNotify(!displayMenuNotify)} className="nav-dd-element">
<LuBell className="header-icon"/>
{errorCounter !== 0 && (
{(errorCounter !== 0 || !agreedLatestToU || !isAbleToRegister) && (
<span id="error-count" className="badge">
{errorCounter}
{errorCounter + (!agreedLatestToU || !isAbleToRegister ? 1 : 0)}
</span>
)}
</BaseButton>
Expand All @@ -225,6 +250,10 @@ const Header = () => {
{displayMenuNotify && (
<MenuNotify
healthStatus={healthStatus}
registering={isAbleToRegister}
updatedToU={agreedLatestToU}
showRegistrationModal={showRegistrationModal}
hideNotificationMenu={hideNotificationMenu}
/>
)}
</div>
Expand Down Expand Up @@ -326,9 +355,18 @@ const MenuLang = ({ handleChangeLanguage }: { handleChangeLanguage: (code: strin

const MenuNotify = ({
healthStatus,
registering,
updatedToU,
showRegistrationModal,
hideNotificationMenu,
}: {
healthStatus: HealthStatus[],
registering: boolean,
updatedToU: boolean,
showRegistrationModal: () => void,
hideNotificationMenu: () => void,
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const navigate = useNavigate();

Expand All @@ -338,6 +376,13 @@ const MenuNotify = ({
navigate("/systems/services");
};

// show Adopter Registration Modal and hide drop down
const showAdoptersRegistrationModal = () => {
showRegistrationModal();
hideNotificationMenu();
};


return (
<ul className="dropdown-ul">
{/* For each service in the serviceList (Background Services) one list item */}
Expand All @@ -361,6 +406,26 @@ const MenuNotify = ({
)}
</li>
))}
{!registering &&
<li>
<ButtonLikeAnchor
onClick={() => showAdoptersRegistrationModal()}
>
<span className="wide-text">Registration</span>
<span className="multi-value multi-value-yellow">{t("ADOPTER_REGISTRATION.NOTIFICATION.UNREGISTERED")}</span>
</ButtonLikeAnchor>
</li>
}
{registering && !updatedToU &&
<li>
<ButtonLikeAnchor
onClick={() => showAdoptersRegistrationModal()}
>
<span className="wide-text">Registration</span>
<span className="multi-value multi-value-yellow">{t("ADOPTER_REGISTRATION.NOTIFICATION.UPDATED_TOU")}</span>
</ButtonLikeAnchor>
</li>
}
</ul>
);
};
Expand Down
70 changes: 42 additions & 28 deletions src/components/shared/RegistrationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Formik } from "formik";
import { useAppDispatch, useAppSelector } from "../../store";
import { Field } from "./Field";
import TermsOfUsePage from "./modals/TermsOfUsePage";
import { countries, states, systemTypes } from "../../configs/adopterRegistrationConfig";
import cn from "classnames";
import { AdopterRegistrationSchema } from "../../utils/validate";
import {
Registration,
Statistics,
fetchRegistration,
fetchStatistics,
postAdopterRegistration,
deleteAdopterRegistration,
fetchAdopterRegistration,
fetchAdopterStatisticsSummary,
postRegistration,
} from "../../utils/adopterRegistrationUtils";
} from "../../slices/registrationSlice";
import {
getRegistrationLoaded,
getRegistration,
getStatisticsLoaded,
getStatistics,
} from "../../selectors/registrationSelectors";
import ModalContent from "./modals/ModalContent";
import { Modal, ModalHandle } from "./modals/Modal";
import { ParseKeys } from "i18next";
Expand Down Expand Up @@ -47,11 +55,17 @@ const RegistrationModal = ({

const RegistrationModalContent = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const registrationLoaded = useAppSelector(state => getRegistrationLoaded(state));
const registration = useAppSelector(state => getRegistration(state));
const statisticsLoaded = useAppSelector(state => getStatisticsLoaded(state));
const statistics = useAppSelector(state => getStatistics(state));

// current state of the modal that is shown
const [state, setState] = useState<keyof typeof states>("information");
// initial values for Formik
const [initialValues, setInitialValues] = useState<Registration & { agreedToPolicy: boolean, registered: boolean }>({
const [initialValues, setInitialValues] = useState<Registration & { registered: boolean, statistics: Statistics | null }>({
contactMe: false,
systemType: "",
allowsStatistics: false,
Expand All @@ -66,20 +80,27 @@ const RegistrationModalContent = () => {
street: "",
streetNo: "",
email: "",
termsVersionAgreed: "",
agreedToPolicy: false,
dateModified: "",
dateCreated: "",
registered: false,
statistics: null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding statistics to formik? I don't think we want to let the user change them? And it makes handling formik values more annoying, because now we have to do stuff like filter the statistics variable away (e.g. below in the summary)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will swap this back!

});

const [statisticsSummary, setStatisticsSummary] = useState<{
general: { [key: string]: unknown },
statistics: { [key: string]: unknown },
}>();
useEffect(() => {
if (!registrationLoaded) {
dispatch(fetchRegistration());
}
if (!statisticsLoaded) {
dispatch(fetchStatistics());
}
}, [registrationLoaded, statisticsLoaded, dispatch]);
Comment on lines +91 to +98

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure on the logic here. When registration has loaded but statistics has not yet loaded, we want to fetch statistics again?

If you just want to fetch things once on component mount, you'd do

useEffect(() => {
	dispatch(fetchRegistration());
	dispatch(fetchStatistics());
}, [dispatch]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad rebase, I'll fix it. I was having issues with Formik and thought this was related.


useEffect(() => {
fetchRegistrationInfos().then(r => console.log(r));
fetchStatisticSummary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
setInitialValues(initialValues => ({ ...initialValues, ...registration, statistics: { ...statistics } }));
}, [registration, statistics]);


const onClickContinue = () => {
// if state is deleteSubmit then delete infos about adaptor else show next state
Expand All @@ -90,23 +111,13 @@ const RegistrationModalContent = () => {
}
};

const fetchRegistrationInfos = async () => {
const registrationInfo = await fetchAdopterRegistration();

// merge response into initial values for formik
setInitialValues({ ...initialValues, ...registrationInfo });
};

const fetchStatisticSummary = async () => {
const info = await fetchAdopterStatisticsSummary();

setStatisticsSummary(info);
};

const handleSubmit = (values: Registration) => {
// post request for adopter information
postRegistration(values)
postAdopterRegistration(values)
.then(() => {
// Refetch the registration data since otherwise what we just submitted does not show up if the user immediately returns to the modal
dispatch(fetchRegistration());
Comment on lines +119 to +120

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like bad practice to me. We need to fetch registration data on modal open anyway, right? So if submitting closes the modal, and the user then reopens the modal, the data should be fetched anyway?

Unless we allow the user to submit without closing the modal, and the backend modifies the data we want to display?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an artifact of the registrationLoaded boolean, since it does not refetch without clearing that.

That fflag is there because the endpoint returns a bare null if there is no registration data, which makes differentiating between not-loaded and loaded-but-blank states hard. I hadn't started the backend pr changed when I added the loaded flags, so maybe I should change that in the backend pr...

// show thank you state
return setState(states[state].nextState[0] as keyof typeof states);
})
Expand Down Expand Up @@ -616,7 +627,10 @@ const RegistrationModalContent = () => {
<p>{t("ADOPTER_REGISTRATION.MODAL.SUMMARY_STATE.GENERAL_HEADER")}</p>
<div className="scrollbox">
<pre>
{JSON.stringify(formik.values, null, "\t")}
{JSON.stringify(Object.fromEntries(
Object.entries(formik.values)
.filter(([key, _]) => key != "statistics"))
, null, "\t")}
</pre>
</div>
<br />
Expand All @@ -626,7 +640,7 @@ const RegistrationModalContent = () => {
<p>{t("ADOPTER_REGISTRATION.MODAL.SUMMARY_STATE.STATS_HEADER")}</p>
<div className="scrollbox">
<pre>
{JSON.stringify(statisticsSummary?.statistics, null, "\t")}
{JSON.stringify(formik.values.statistics, null, "\t")}
</pre>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@
"DELETE_SUBMIT_STATE": {
"TEXT": "Are you sure you want to delete your registration data?"
}
},
"NOTIFICATION": {
"UNREGISTERED": "Unregistered",
"UPDATED_TOU": "Updated ToU"
}
},
"EVENTS": {
Expand Down
19 changes: 19 additions & 0 deletions src/selectors/registrationSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { RootState } from "../store";

/**
* This file contains selectors regarding information about the registration status
*/
export const getRegistrationLoaded = (state: RootState) => state.registration.registrationLoaded;
// Are we registered at all
export const getRegistration = (state: RootState) => state.registration.registration;

export const getStatisticsLoaded = (state: RootState) => state.registration.statisticsLoaded;
// Gather the system statistics reportable to the registration server
export const getStatistics = (state: RootState) => state.registration.statistics;

// Are we able to talk to register.opencast.org
export const getAbleToRegister = (state: RootState) => state.registration.registrationLoaded &&
state.registration.ableToRegister;
export const getAgreedLatestToU = (state: RootState) => state.registration.registrationLoaded &&
state.registration.registration != null && // this is correct because the *endpoint* returns a bare 'null' when the cluster isn't registered
state.registration.registration.termsVersionAgreed == state.registration.latestToU;
Loading
Loading