From b3cbef370eea10d0921f83f608a1ab963d0eca31 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Sun, 23 Nov 2025 11:37:55 +0100 Subject: [PATCH 1/3] Example solution for #564 --- client/src/locale/en.js | 1 + client/src/pages/RoleForm.jsx | 47 +++++++++++++++++++++++++--------- client/src/pages/RoleForm.scss | 18 +++++++++++++ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/client/src/locale/en.js b/client/src/locale/en.js index e3d285e3..dfadbd07 100644 --- a/client/src/locale/en.js +++ b/client/src/locale/en.js @@ -316,6 +316,7 @@ const en = { no: "No", ok: "OK", or: "or ", + fixed: "or set a fixed date", edit: "Edit", reset: "Reset", cancel: "Cancel", diff --git a/client/src/pages/RoleForm.jsx b/client/src/pages/RoleForm.jsx index bab2405c..77059e18 100644 --- a/client/src/pages/RoleForm.jsx +++ b/client/src/pages/RoleForm.jsx @@ -30,6 +30,8 @@ import SwitchField from "../components/SwitchField"; import {dateFromEpoch, displayExpiryDate, futureDate} from "../utils/Date"; import DOMPurify from "dompurify"; import WarningIndicator from "../components/WarningIndicator"; +import DatePicker from "react-datepicker"; +import {MinimalDateField} from "../components/MinimalDateField"; const DEFAULT_EXPIRY_DAYS = 365; const CUT_OFF_DELETED_USER = 5; @@ -446,18 +448,39 @@ export const RoleForm = () => { })} last={customRoleExpiryDate} /> - {customRoleExpiryDate && { - const val = parseInt(e.target.value); - const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0; - setRole( - {...role, defaultExpiryDays: defaultExpiryDays}) - }} - toolTip={I18n.t("tooltips.defaultExpiryDays")} - customClassName="inner-switch" - />} + {customRoleExpiryDate && +
+ { + const val = parseInt(e.target.value); + const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0; + setRole( + {...role, defaultExpiryDays: defaultExpiryDays}) + }} + toolTip={I18n.t("tooltips.defaultExpiryDays")} + customClassName="inner-switch"/> + {I18n.t("forms.fixed")} + true} + showWeekNumbers + isClearable={true} + showIcon={true} + showYearDropdown={true} + weekLabel="Week" + disabled={false} + todayButton={null} + maxDate={null} + minDate={new Date()} + /> +
+ +} {(!initial && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) && Date: Mon, 24 Nov 2025 16:28:57 +0100 Subject: [PATCH 2/3] WIP for #564 --- .gitignore | 2 +- client/src/locale/en.js | 7 +- client/src/locale/nl.js | 5 + client/src/pages/RoleForm.jsx | 115 ++++++++++++------ client/src/pages/RoleForm.scss | 29 ++++- .../java/invite/api/InvitationOperations.java | 21 +++- .../java/invite/api/UserRoleController.java | 2 +- .../main/java/invite/model/Invitation.java | 8 +- server/src/main/java/invite/model/Role.java | 15 +++ .../main/java/invite/model/RoleRequest.java | 3 + .../V51_0__role_default_end_date.sql | 2 + .../java/invite/api/RoleControllerTest.java | 13 +- 12 files changed, 162 insertions(+), 60 deletions(-) create mode 100644 server/src/main/resources/db/mysql/migration/V51_0__role_default_end_date.sql diff --git a/.gitignore b/.gitignore index 5687c5ff..126723cd 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,4 @@ spieldata teams-api-calls.md .run/ client/locale_parser.js - +PlayGroundTest.java diff --git a/client/src/locale/en.js b/client/src/locale/en.js index dfadbd07..388b380c 100644 --- a/client/src/locale/en.js +++ b/client/src/locale/en.js @@ -266,8 +266,13 @@ const en = { expiryDate: "Valid till", acceptedAt: "Date accepted", roleExpiryDate: "Role expiry date", - roleExpiryDateQuestion: "Set a custom role expiration period", + roleExpiryDateQuestion: "Set a custom role expiration", roleExpiryDateInfo: "This role will be removed from the user {{expiry}}", + roleExpiryDateInfoDefault: "By default this role will be removed from a user after {{days}} days", + removeRole: "Remove role", + after: "After", + on: "On", + days: "Days", customInviterDisplayNameQuestion: "Set a custom inviter name", inviterDisplayName: "Custom inviter display name for invitations", inviterDisplayNamePlaceholder: "e.g. working@home.university.nl", diff --git a/client/src/locale/nl.js b/client/src/locale/nl.js index 502eee04..3385e8e5 100644 --- a/client/src/locale/nl.js +++ b/client/src/locale/nl.js @@ -268,6 +268,11 @@ const nl = { roleExpiryDate: "Verloopdatum rol", roleExpiryDateQuestion: "Zet een specifieke verloopdatum voor de rol", roleExpiryDateInfo: "Deze rol wordt verwijderd van de gebruiker {{expiry}}", + roleExpiryDateInfoDefault: "Default wordt deze rol verwijderd van de gebruiker na 365 dagen", + removeRole: "Verwijder rol", + after: "Na", + on: "Op", + days: "dagen", customInviterDisplayNameQuestion: "Zet een specifieke naam voor de uitnodiger", inviterDisplayName: "Specifieke naam uitnodiger voor in de uitnodiging", inviterDisplayNamePlaceholder: "Bijv. werken@thuis.universiteit.nl", diff --git a/client/src/pages/RoleForm.jsx b/client/src/pages/RoleForm.jsx index 77059e18..fac6917e 100644 --- a/client/src/pages/RoleForm.jsx +++ b/client/src/pages/RoleForm.jsx @@ -3,6 +3,7 @@ import {useLocation, useNavigate, useParams} from "react-router-dom"; import {useAppStore} from "../stores/AppStore"; import I18n from "../locale/I18n"; import {AUTHORITIES, isUserAllowed, urnFromRole} from "../utils/UserRole"; +import Select from "react-select"; import { allProviders, consequencesRoleDeletion, @@ -30,12 +31,13 @@ import SwitchField from "../components/SwitchField"; import {dateFromEpoch, displayExpiryDate, futureDate} from "../utils/Date"; import DOMPurify from "dompurify"; import WarningIndicator from "../components/WarningIndicator"; -import DatePicker from "react-datepicker"; -import {MinimalDateField} from "../components/MinimalDateField"; +import {DateField} from "../components/DateField"; const DEFAULT_EXPIRY_DAYS = 365; const CUT_OFF_DELETED_USER = 5; +const removeByOptions = ["after", "on"].map(val => ({value: val, label: I18n.t(`invitations.${val}`)})) + export const RoleForm = () => { const location = useLocation(); const navigate = useNavigate(); @@ -64,6 +66,7 @@ export const RoleForm = () => { const [applications, setApplications] = useState([]); const [provisionings, setProvisionings] = useState({}); const [deletedUserRoles, setDeletedUserRoles] = useState(null); + const [removeRoleBy, setRemoveRoleBy] = useState(removeByOptions[0]); const allowedToEditApplication = useState(isUserAllowed(AUTHORITIES.INSTITUTION_ADMIN, user)); @@ -83,6 +86,10 @@ export const RoleForm = () => { } Promise.all(promises).then(res => { if (!newRole) { + if (res[0].defaultExpiryDate !== 0) { + res[0].defaultExpiryDate = new Date(res[0].defaultExpiryDate * 1000); + setRemoveRoleBy(removeByOptions[1]); + } setRole(res[0]); setCustomRoleExpiryDate(res[0].defaultExpiryDays !== DEFAULT_EXPIRY_DAYS) setCustomInviterDisplayName(!isEmpty(res[0].inviterDisplayName)) @@ -180,6 +187,15 @@ export const RoleForm = () => { }) } + const toggleRemoveBy = option => { + setRemoveRoleBy(option); + if (option.value === "after") { + setRole({...role, defaultExpiryDays: DEFAULT_EXPIRY_DAYS, defaultExpiryDate: null}); + } else { + setRole({...role, defaultExpiryDays: 0, defaultExpiryDate: futureDate(365, new Date())}); + } + } + const updateUserIfNecessary = (path, flashMessage) => { if (user.userRoles.some(userRole => userRole.role.id === role.id)) { //We need to refresh the roles of the User to ensure 100% consistency @@ -245,7 +261,7 @@ export const RoleForm = () => { && validOrganizationGUID && !isEmpty(applications[0]) && applications.every(app => !app || (!app.invalid && !isEmpty(app.landingPage))) - && role.defaultExpiryDays > 0 + && (role.defaultExpiryDays > 0 || role.defaultExpiryDate !== null) && (!isEmpty(role.inviterDisplayName) || !customInviterDisplayName); } @@ -279,6 +295,12 @@ export const RoleForm = () => { setApplications([...applications]); } + const deriveExpiryDate = () => { + console.log(`defaultExpityDate ${role.defaultExpiryDate}, defaultExpiryDays: ${role.defaultExpiryDays}`) + const expiryDate = isEmpty(role.defaultExpiryDate) ? futureDate(role.defaultExpiryDays, new Date()) : role.defaultExpiryDate; + return displayExpiryDate(expiryDate); + + }; const renderForm = () => { const valid = isValid(); const disabledSubmit = (!valid && !initial) || !validOrganizationGUID; @@ -441,48 +463,63 @@ export const RoleForm = () => { setCustomRoleExpiryDate(!customRoleExpiryDate)} - label={I18n.t("invitations.roleExpiryDateQuestion")} - info={I18n.t("invitations.roleExpiryDateInfo", { - expiry: displayExpiryDate(futureDate(role.defaultExpiryDays, new Date())) + onChange={() => { + if (customRoleExpiryDate) { + setRole({ + ...role, + defaultExpiryDays: DEFAULT_EXPIRY_DAYS, + defaultExpiryDate: null + }); + setRemoveRoleBy(removeByOptions[0]); + } + setCustomRoleExpiryDate(!customRoleExpiryDate); + }} + label={I18n.t(`invitations.roleExpiryDateQuestion`)} + info={I18n.t(`invitations.roleExpiryDateInfo${role.defaultExpiryDays === DEFAULT_EXPIRY_DAYS ? "Default" : ""}`, { + expiry: deriveExpiryDate(), + days: DEFAULT_EXPIRY_DAYS })} last={customRoleExpiryDate} /> {customRoleExpiryDate && -
- { - const val = parseInt(e.target.value); - const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0; - setRole( - {...role, defaultExpiryDays: defaultExpiryDays}) - }} - toolTip={I18n.t("tooltips.defaultExpiryDays")} - customClassName="inner-switch"/> - {I18n.t("forms.fixed")} - true} - showWeekNumbers - isClearable={true} - showIcon={true} - showYearDropdown={true} - weekLabel="Week" - disabled={false} - todayButton={null} - maxDate={null} - minDate={new Date()} - /> -
+
+

{I18n.t("invitations.removeRole")}

+
+