Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .run/FE - Welcome.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="FE - Welcome" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/welcome/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, {useState} from "react";
import SwitchField from "../SwitchField";

export const ExpandableSwitchField = ({
name,
label,
info,
defaultValue = false,
onChange,
disabled = false,
children
}) => {
const [expanded, setExpanded] = useState(defaultValue);

const toggle = () => {
const next = !expanded;
setExpanded(next);
if (onChange) {
onChange(next);
}
};

return (
<>
<SwitchField name={name}
value={expanded}
onChange={toggle}
label={label}
info={info}
last={expanded}
disabled={disabled}/>
{expanded && children}
</>
);
};
1 change: 1 addition & 0 deletions client/src/components/ExpandableSwitchField/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ExpandableSwitchField";
1 change: 1 addition & 0 deletions client/src/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ExpandableSwitchField";
119 changes: 89 additions & 30 deletions client/src/pages/InvitationForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ import ErrorIndicator from "../components/ErrorIndicator";
import SelectField from "../components/SelectField";
import {DateField} from "../components/DateField";
import EmailField from "../components/EmailField";
import {deriveExpirationDate, displayExpiryDate, futureDate} from "../utils/Date";
import {deriveExpirationDate, displayExpiryDate, futureDate, longDateFormat} from "../utils/Date";
import SwitchField from "../components/SwitchField";
import {InvitationRoleCard} from "../components/InvitationRoleCard";
import DOMPurify from "dompurify";
import {applicationName} from "../utils/Manage";
import {ExpandableSwitchField} from "../components";
import Select from "react-select";

const DEFAULT_ROLE_EXPIRY_DAYS = 366;

const removeByOptions = ["after", "on"].map(val => ({value: val, label: I18n.t(`invitations.${val}`)}))

export const InviterContainer = ({isInviter, children}) => {
return isInviter ?
Expand All @@ -56,9 +61,11 @@ export const InvitationForm = () => {
const [roles, setRoles] = useState([]);
const [selectedRoles, setSelectedRoles] = useState([]);
const [originalRoleId, setOriginalRoleId] = useState(null);

const [invitation, setInvitation] = useState({
expiryDate: futureDate(30),
roleExpiryDate: futureDate(366),
roleExpiryDate: null,
roleExpiryDays: 0,
invites: [],
intendedAuthority: AUTHORITIES.GUEST
});
Expand All @@ -68,10 +75,12 @@ export const InvitationForm = () => {
const [customExpiryDate, setCustomExpiryDate] = useState(false);
const [customRoleExpiryDate, setCustomRoleExpiryDate] = useState(false);
const [initial, setInitial] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [eduIDIdP, setEduIDIdP] = useState(null);
const [acrValues, setACRValues] = useState({});
const [language, setLanguage] = useState(I18n.locale === "en" ? languageOptions[0] : languageOptions[1]);
const required = ["intendedAuthority", "invites"];
const [removeRoleBy, setRemoveRoleBy] = useState(removeByOptions[1]);

const isInviter = highestAuthority(user) === AUTHORITIES.INVITER;

Expand Down Expand Up @@ -186,9 +195,13 @@ export const InvitationForm = () => {

const submit = () => {
setInitial(false);
if (isValid()) {
if (isValid() && !isSubmitting) {
setIsSubmitting(true);
const invitationRequest = {
...invitation,
roleExpiryDate: removeRoleBy.value === "after"
? futureDate(invitation.roleExpiryDays || DEFAULT_ROLE_EXPIRY_DAYS)
: invitation.roleExpiryDate,
roleIdentifiers: selectedRoles.map(role => role.value),
language: language.value
};
Expand All @@ -202,6 +215,10 @@ export const InvitationForm = () => {
} else {
navigate(-1);
}
})
.catch(e => {
setIsSubmitting(false);
throw e;
});
}
}
Expand Down Expand Up @@ -231,6 +248,21 @@ export const InvitationForm = () => {
new Date(Math.max(...allDefaultExpiryDates.map(d => d.getTime())));
}

const customRoleExpiryDateInfo = () => {
let postfix = "Default";
if (customRoleExpiryDate) {
postfix = removeRoleBy.value === "on" ? "On" : "";
}
const expiryDate = removeRoleBy.value === "on"
? invitation.roleExpiryDate
: futureDate(invitation.roleExpiryDays || DEFAULT_ROLE_EXPIRY_DAYS);
return I18n.t(`invitations.roleExpiryDateInfo${postfix}`, {
expiry: displayExpiryDate(expiryDate),
date: longDateFormat(invitation.roleExpiryDate),
days: DEFAULT_ROLE_EXPIRY_DAYS
});
}

const eduIDOnlyChanged = val => {
const requestedAuthnContext = val ? invitation.requestedAuthnContext : null;
setInvitation({...invitation, eduIDOnly: val, requestedAuthnContext: requestedAuthnContext})
Expand Down Expand Up @@ -271,7 +303,6 @@ export const InvitationForm = () => {
intendedAuthority: intendedAuthority,
enforceEmailEquality: enforceEmailEquality,
eduIDOnly: eduIDOnly,
roleExpiryDate: defaultRoleExpiryDate(newSelectedOptions)
})
}
}
Expand Down Expand Up @@ -410,6 +441,17 @@ export const InvitationForm = () => {
.map(authority => ({value: authority, label: I18n.t(`access.${authority}`)}));
const overrideSettingsAllowed = selectedRoles.every(role => role.overrideSettingsAllowed);
const skipRoles = [AUTHORITIES.SUPER_USER, AUTHORITIES.INSTITUTION_ADMIN].includes(invitation.intendedAuthority)

const toggleRemoveBy = option => {
setRemoveRoleBy(option);

if (option.value === "after") {
setInvitation({...invitation, roleExpiryDays: DEFAULT_ROLE_EXPIRY_DAYS, roleExpiryDate: null})
} else {
setInvitation({...invitation, roleExpiryDays: 0, roleExpiryDate: futureDate(DEFAULT_ROLE_EXPIRY_DAYS)})
}
}

return (
<>
{isInviter &&
Expand Down Expand Up @@ -493,32 +535,49 @@ export const InvitationForm = () => {
/>

}

{(overrideSettingsAllowed && !skipRoles) &&
<SwitchField name={"roleExpiryDate"}
value={customRoleExpiryDate}
onChange={() => {
setCustomRoleExpiryDate(!customRoleExpiryDate);
setInvitation({
...invitation,
roleExpiryDate: defaultRoleExpiryDate(selectedRoles)
})
}}
label={I18n.t("invitations.roleExpiryDateQuestion")}
info={I18n.t("invitations.roleExpiryDateInfo", {
expiry: displayExpiryDate(invitation.roleExpiryDate)
})}
/>
<ExpandableSwitchField
name={"roleExpiryDate"}
label={I18n.t(`invitations.roleExpiryDateQuestion`)}
info={customRoleExpiryDateInfo()}
defaultValue={customRoleExpiryDate}
onChange={val => setCustomRoleExpiryDate(val)}
>
<div className="role-expiry-date-container">
<p className="label">{I18n.t("invitations.removeRole")}</p>
<div className="role-expiry-date">
<Select className="input-select-inner"
classNamePrefix={"select-inner"}
value={removeRoleBy}
options={removeByOptions}
onChange={toggleRemoveBy}
/>
{removeRoleBy.value === "after" &&
<>
<InputField value={invitation.roleExpiryDays}
isInteger={true}
onChange={e => {
const val = parseInt(e.target.value);
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 1;
setInvitation({...invitation, roleExpiryDays: defaultExpiryDays})
}}
customClassName="inner-switch"/>
<span>{I18n.t("invitations.days")}</span>
</>
}
{removeRoleBy.value === "on" &&
<DateField value={invitation.roleExpiryDate || futureDate(DEFAULT_ROLE_EXPIRY_DAYS)}
onChange={e => setInvitation({...invitation, roleExpiryDate: e})}
showYearDropdown={true}
disabled={selectedRoles.some(role => !role.overrideSettingsAllowed)}
pastDatesAllowed={config.pastDateAllowed}
minDate={futureDate(1, invitation.expiryDate)}/>
}
</div>
</div>
</ExpandableSwitchField>
}
{customRoleExpiryDate &&
<DateField value={invitation.roleExpiryDate}
onChange={e => setInvitation({...invitation, roleExpiryDate: e})}
showYearDropdown={true}
disabled={selectedRoles.some(role => !role.overrideSettingsAllowed)}
pastDatesAllowed={config.pastDateAllowed}
allowNull={overrideSettingsAllowed && invitation.intendedAuthority !== AUTHORITIES.GUEST}
minDate={futureDate(1, invitation.expiryDate)}
name={I18n.t("invitations.roleExpiryDate")}
toolTip={I18n.t("tooltips.roleExpiryDateTooltip")}/>}

<SwitchField name={"expiryDate"}
value={customExpiryDate}
Expand Down Expand Up @@ -548,7 +607,7 @@ export const InvitationForm = () => {
<Button type={ButtonType.Secondary}
txt={I18n.t("forms.cancel")}
onClick={() => navigate(-1)}/>
<Button disabled={disabledSubmit}
<Button disabled={disabledSubmit || isSubmitting}
txt={I18n.t("invitations.invite")}
onClick={submit}/>
</section>
Expand Down Expand Up @@ -577,4 +636,4 @@ export const InvitationForm = () => {
</div>
</div>
);
}
}
38 changes: 37 additions & 1 deletion client/src/pages/InvitationForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,43 @@
margin-left: auto;
}
}

.role-expiry-date-container {
grid-column-start: first;
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--sds--color--gray--200);
padding-bottom: 15px;

p.label {
font-weight: 600;
margin: 12px var(--sds--space--1) 0 0;
}

.role-expiry-date {
display: flex;
gap: 15px;
align-items: center;

.input-select-inner {
width: 150px;

.select-inner__control {
height: 48px;
}
}

.date-field {
margin-top: 0;
}
}

div.input-field.inner-switch {
padding-bottom: 0;
border-bottom: none;
}
}
}


}
}
40 changes: 20 additions & 20 deletions client/src/pages/RoleForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {dateFromEpoch, displayExpiryDate, futureDate, longDateFormat} from "../u
import DOMPurify from "dompurify";
import WarningIndicator from "../components/WarningIndicator";
import {DateField} from "../components/DateField";
import {ExpandableSwitchField} from "../components";

const DEFAULT_EXPIRY_DAYS = 365;
const CUT_OFF_DELETED_USER = 5;
Expand Down Expand Up @@ -508,24 +509,23 @@ export const RoleForm = () => {
info={I18n.t("tooltips.eduIDOnlyTooltip")}
/>

<SwitchField name={"roleExpiryDate"}
value={customRoleExpiryDate}
onChange={() => {
if (customRoleExpiryDate) {
setRole({
...role,
defaultExpiryDays: DEFAULT_EXPIRY_DAYS,
defaultExpiryDate: null
});
setRemoveRoleBy(removeByOptions[0]);
}
setCustomRoleExpiryDate(!customRoleExpiryDate);
}}
label={I18n.t(`invitations.roleExpiryDateQuestion`)}
info={customRoleExpiryDateInfo()}
last={customRoleExpiryDate}
/>
{customRoleExpiryDate &&
<ExpandableSwitchField
name={"roleExpiryDate"}
label={I18n.t(`invitations.roleExpiryDateQuestion`)}
info={customRoleExpiryDateInfo()}
defaultValue={customRoleExpiryDate}
onChange={val => {
if (!val) {
setRole({
...role,
defaultExpiryDays: DEFAULT_EXPIRY_DAYS,
defaultExpiryDate: null
});
setRemoveRoleBy(removeByOptions[0]);
}
setCustomRoleExpiryDate(val);
}}
>
<div className="role-expiry-date-container">
<p className="label">{I18n.t("invitations.removeRole")}</p>
<div className="role-expiry-date">
Expand Down Expand Up @@ -561,7 +561,7 @@ export const RoleForm = () => {

</div>
</div>
}
</ExpandableSwitchField>

{(!initial && removeRoleBy.value === "after" && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
<ErrorIndicator msg={I18n.t("forms.required", {
Expand Down Expand Up @@ -659,4 +659,4 @@ export const RoleForm = () => {
</div>
</div>
);
}
}
7 changes: 4 additions & 3 deletions client/src/utils/Date.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {isEmpty} from "./Utils";

let timeAgoInitialized = false;

export const futureDate = (daysAhead, fromDate = new Date()) => {
const time = fromDate.getTime() + (1000 * 60 * 60 * 24 * daysAhead);
export const futureDate = (daysAhead, fromDate) => {
const baseDate = isEmpty(fromDate) ? new Date() : fromDate;
const time = baseDate.getTime() + (1000 * 60 * 60 * 24 * daysAhead);
return new Date(time);
}

Expand All @@ -27,7 +28,7 @@ export const shortDateFromEpoch = (epoch, needsMultiplier = true) => {
return dateTimeFormat.format(new Date(needsMultiplier ? epoch * 1000 : epoch));
}

const longFormatOptions = {month: "long", weekday: "long", year: "numeric"};
const longFormatOptions = {weekday: "long", day: "numeric", month: "long", year: "numeric"};

export const longDateFormat = date => {
if (isEmpty(date)) {
Expand Down
Loading
Loading