Skip to content

Commit 6768475

Browse files
committed
Adding interface on EditUser page to handle users' calendar tokens.
1 parent 19ad1fe commit 6768475

File tree

9 files changed

+365
-29
lines changed

9 files changed

+365
-29
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { Table, OverlayTrigger, Tooltip } from 'react-bootstrap';
5+
import { CopyToClipboard } from 'react-copy-to-clipboard';
6+
7+
import Button from '../../widgets/TheButton';
8+
import DateTime from '../../widgets/DateTime';
9+
import InsetPanel from '../../widgets/InsetPanel';
10+
import Icon, { CopyIcon, InfoIcon, LoadingIcon, WarningIcon, RefreshIcon } from '../../icons';
11+
12+
import { API_BASE } from '../../../helpers/config';
13+
14+
class CalendarTokens extends Component {
15+
state = { createPending: false, copiedCalendar: null };
16+
17+
createButtonHandler = () => {
18+
const { create, calendars, setExpired } = this.props;
19+
const activeCalendars = calendars.some(calendar => calendar && !calendar.expiredAt);
20+
this.setState({ createPending: true });
21+
22+
if (activeCalendars && setExpired) {
23+
return Promise.all(calendars.filter(c => c && !c.expiredAt).map(c => setExpired(c.id)))
24+
.then(create)
25+
.then(() => this.setState({ createPending: false }));
26+
} else {
27+
return create().then(() => this.setState({ createPending: false }));
28+
}
29+
};
30+
31+
calendarCopied = copiedCalendar => {
32+
this.setState({ copiedCalendar });
33+
if (this.resetCalendarTimer) {
34+
clearTimeout(this.resetCalendarTimer);
35+
}
36+
this.resetCalendarTimer = setTimeout(() => {
37+
this.setState({ copiedCalendar: null });
38+
this.resetCalendarTimer = undefined;
39+
}, 2000);
40+
};
41+
42+
componentWillUnmount() {
43+
if (this.resetCalendarTimer) {
44+
clearTimeout(this.resetCalendarTimer);
45+
this.resetCalendarTimer = undefined;
46+
}
47+
}
48+
49+
render() {
50+
const { calendars = null, create = null, setExpired = null, reload = null } = this.props;
51+
const baseUrl = `${API_BASE}/users/ical/`;
52+
const activeCalendars = calendars.some(calendar => calendar && !calendar.expiredAt);
53+
54+
return (
55+
<>
56+
<p className="mt-4 mx-4 mb-2">
57+
<InfoIcon gapRight className="text-muted" />
58+
<FormattedMessage
59+
id="app.calendarTokens.explain"
60+
defaultMessage="ReCodEx API can feed iCal data to your calendar. It will export deadline events for all assignments in all groups related to you. Anyone with the iCal identifier will be able to read the calendar and the calendar is read-only. When activated, you will get a calendar URL in the following format."
61+
/>
62+
</p>
63+
64+
<InsetPanel className="mt-2 mx-3 mb-3" size="small">
65+
<code>
66+
{baseUrl}
67+
<em>&lt;identifier&gt;</em>
68+
</code>
69+
</InsetPanel>
70+
71+
{calendars && calendars.length > 0 && (
72+
<Table hover size="sm" className="mb-0">
73+
<thead>
74+
<tr>
75+
<th className="px-3">
76+
<FormattedMessage id="app.calendarTokens.id" defaultMessage="Existing iCal identifiers" />
77+
</th>
78+
<th className="text-nowrap px-3">
79+
<FormattedMessage id="generic.createdAt" defaultMessage="Created at" />
80+
</th>
81+
<th className="text-nowrap px-3">
82+
<FormattedMessage id="app.calendarTokens.expiredAt" defaultMessage="Expired at" />
83+
</th>
84+
<th />
85+
</tr>
86+
</thead>
87+
88+
<tbody>
89+
{calendars.map((calendar, idx) =>
90+
calendar ? (
91+
<tr key={calendar.id} className={calendar.expiredAt ? 'text-muted' : ''}>
92+
<td className="full-width px-3">
93+
<code className={calendar.expiredAt ? 'text-muted' : ''}>{calendar.id}</code>
94+
{!calendar.expiredAt &&
95+
(this.state.copiedCalendar === calendar.id ? (
96+
<Icon icon="clipboard-check" gapLeft className="text-success" />
97+
) : (
98+
<OverlayTrigger
99+
placement="right"
100+
overlay={
101+
<Tooltip id={calendar.id}>
102+
<FormattedMessage
103+
id="app.calendarTokens.copyToClipboard"
104+
defaultMessage="Copy the URL into clipboard"
105+
/>
106+
</Tooltip>
107+
}>
108+
<CopyToClipboard
109+
text={`${baseUrl}${calendar.id}`}
110+
onCopy={() => this.calendarCopied(calendar.id)}>
111+
<CopyIcon timid gapLeft className="clickable" />
112+
</CopyToClipboard>
113+
</OverlayTrigger>
114+
))}
115+
</td>
116+
117+
<td className="text-nowrap px-3">
118+
<DateTime unixts={calendar.createdAt} />
119+
</td>
120+
121+
<td className="text-nowrap px-3">
122+
<DateTime unixts={calendar.expiredAt} />
123+
</td>
124+
125+
<td className="text-nowrap px-3">
126+
{!calendar.expiredAt && setExpired && calendar.expiring !== false && (
127+
<Button
128+
variant="danger"
129+
size="xs"
130+
onClick={() => setExpired(calendar.id)}
131+
disabled={calendar.expiring || this.state.createPending}>
132+
<Icon icon={['far', 'calendar-xmark']} gapRight />
133+
<FormattedMessage id="app.calendarTokens.setExpired" defaultMessage="Set expired" />
134+
{calendar.expiring && <LoadingIcon gapLeft />}
135+
</Button>
136+
)}
137+
{calendar.expiring === false && (
138+
<span>
139+
<WarningIcon className="text-danger" gapRight />
140+
<FormattedMessage
141+
id="app.calendarTokens.setExpiredFailed"
142+
defaultMessage="operation failed"
143+
/>
144+
{reload && !this.state.createPending && (
145+
<RefreshIcon gapLeft className="text-primary" onClick={reload} />
146+
)}
147+
</span>
148+
)}
149+
</td>
150+
</tr>
151+
) : (
152+
<tr key={`loading-${idx}`}>
153+
<td colSpan={4} className="text-center">
154+
<LoadingIcon />
155+
</td>
156+
</tr>
157+
)
158+
)}
159+
</tbody>
160+
</Table>
161+
)}
162+
163+
<hr className="m-0" />
164+
165+
{create && (
166+
<div className="text-center my-3">
167+
<Button
168+
variant={activeCalendars ? 'warning' : 'success'}
169+
onClick={this.createButtonHandler}
170+
disabled={this.state.createPending}>
171+
<Icon icon={['far', 'calendar-plus']} gapRight />
172+
{activeCalendars ? (
173+
<FormattedMessage id="app.calendarTokens.refresh" defaultMessage="Expire old and create a new one" />
174+
) : (
175+
<FormattedMessage id="app.calendarTokens.activate" defaultMessage="Activate calendar" />
176+
)}
177+
{this.state.createPending && <LoadingIcon gapLeft />}
178+
</Button>
179+
</div>
180+
)}
181+
</>
182+
);
183+
}
184+
}
185+
186+
CalendarTokens.propTypes = {
187+
calendars: PropTypes.array,
188+
create: PropTypes.func,
189+
setExpired: PropTypes.func,
190+
reload: PropTypes.func,
191+
};
192+
193+
export default CalendarTokens;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import CalendarTokens from './CalendarTokens';
2+
export default CalendarTokens;

src/components/layout/HeaderNotification/HeaderNotification.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class HeaderNotification extends Component {
7878
</span>
7979
<span className="fa">
8080
<span className={styles.copy} ref={copy => (this.copy = copy)}>
81-
<CopyToClipboard text={msg} onCopy={() => this.onCopy()}>
81+
<CopyToClipboard text={msg} onCopy={this.onCopy}>
8282
<CopyIcon gapRight fixedWidth />
8383
</CopyToClipboard>
8484
</span>

src/locales/cs.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@
179179
"app.broker.stats": "Aktuální statistiky",
180180
"app.broker.title": "ZeroMQ Broker",
181181
"app.broker.unfreeze": "Rozmrazit",
182+
"app.calendarTokens.activate": "Aktivovat kalendář",
183+
"app.calendarTokens.copyToClipboard": "Zkopírovat URL do schránky",
184+
"app.calendarTokens.expiredAt": "Konec platnosti",
185+
"app.calendarTokens.explain": "API ReCodExu může exportovat data pro váš iCal kalendář. Exportovány budou koncové termíny všech zadaných úloh ve všech vašich skupinách. Ke kalendářovým datům bude mít přístup každý, kdo zná platný iCal identifikátor a tato data jsou pouze pro čtení. Pokud aktivujete kalendář, dostanete URL v následujícím formátu.",
186+
"app.calendarTokens.id": "Existující iCal identifikátory",
187+
"app.calendarTokens.refresh": "Zneplatnit starý a vytvořit nový kalendář",
188+
"app.calendarTokens.setExpired": "Zneplatnit",
189+
"app.calendarTokens.setExpiredFailed": "operace selhala",
182190
"app.changePassword.description": "Zapomenuté heslo lze změnit v tomto formuláři",
183191
"app.changePassword.requestAnotherLink": "Prosíme zažádejte o (další) odkaz s unikátním tokenem.",
184192
"app.changePassword.title": "Změna zapomenutého hesla",
@@ -567,6 +575,7 @@
567575
"app.editTestsTest.weight": "Váha testu:",
568576
"app.editUser.emailStillNotVerified": "Vaše e-mailová adresa dosud nebyla ověřena. ReCodEx se potřebuje spolehnout na platnost adres, protože řada notifikací je zasílána e-mailem. Pomocí tlačítka níže si můžete nechat opětovně zaslat ověřovací e-mail. Ten obsahuje odkaz, který potvrzuje platnost adresy. Prosíme, ověřte vaši adresu co nejdříve.",
569577
"app.editUser.emailStillNotVerifiedTitle": "E-mailová adresa není ověřena",
578+
"app.editUser.icalTitle": "Export termínů úloh do iCal formátu",
570579
"app.editUser.isEmailAlreadyVefiried": "Pokud jste právě ověřili vaši adresu a stále vidíte tuto hlášku, prosím občerstvěte stránku.",
571580
"app.editUser.makeLocal": "Vytvořit lokální účet",
572581
"app.editUser.title": "Upravit uživatelský profil",

src/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@
179179
"app.broker.stats": "Current Statistics",
180180
"app.broker.title": "ZeroMQ Broker",
181181
"app.broker.unfreeze": "Unfreeze",
182+
"app.calendarTokens.activate": "Activate calendar",
183+
"app.calendarTokens.copyToClipboard": "Copy the URL into clipboard",
184+
"app.calendarTokens.expiredAt": "Expired at",
185+
"app.calendarTokens.explain": "ReCodEx API can feed iCal data to your calendar. It will export deadline events for all assignments in all groups related to you. Anyone with the iCal identifier will be able to read the calendar and the calendar is read-only. When activated, you will get a calendar URL in the following format.",
186+
"app.calendarTokens.id": "Existing iCal identifiers",
187+
"app.calendarTokens.refresh": "Expire old and create a new one",
188+
"app.calendarTokens.setExpired": "Set expired",
189+
"app.calendarTokens.setExpiredFailed": "operation failed",
182190
"app.changePassword.description": "You can change your forgotten password in this form",
183191
"app.changePassword.requestAnotherLink": "Please request (another) link with an unique token.",
184192
"app.changePassword.title": "Change Forgotten Password",
@@ -567,6 +575,7 @@
567575
"app.editTestsTest.weight": "Test weight:",
568576
"app.editUser.emailStillNotVerified": "Your email addres has not been verified yet. ReCodEx needs to rely on vaild addresses since many notifications are sent via email. You may send yourself a validation email using the button below and then use a link from that email to verify its acceptance. Please validate your address as soon as possible.",
569577
"app.editUser.emailStillNotVerifiedTitle": "Email Address Is Not Verified",
578+
"app.editUser.icalTitle": "Deadlines Export to iCal",
570579
"app.editUser.isEmailAlreadyVefiried": "If you have just verified your email and still see the message, please refresh the page.",
571580
"app.editUser.makeLocal": "Create local account",
572581
"app.editUser.title": "Edit User's Profile",

0 commit comments

Comments
 (0)