Skip to content

Commit da6af5f

Browse files
committed
Implementing add-attribute form and its submission.
1 parent 1075a77 commit da6af5f

File tree

9 files changed

+379
-47
lines changed

9 files changed

+379
-47
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Form, Field, FormSpy } from 'react-final-form';
4+
import { FormattedMessage } from 'react-intl';
5+
6+
import { CloseIcon, LoadingIcon, SaveIcon } from '../../icons';
7+
import Button, { TheButtonGroup } from '../../widgets/TheButton';
8+
import { TextField, StandaloneRadioField } from '../fields';
9+
import Explanation from '../../widgets/Explanation';
10+
import { lruMemoize } from 'reselect';
11+
import { EMPTY_OBJ } from '../../../helpers/common';
12+
import Callout from '../../widgets/Callout';
13+
14+
const empty = values => {
15+
const mode = values.mode === 'other' ? 'key' : values.mode;
16+
return !values[mode];
17+
};
18+
19+
const validate = lruMemoize(attributes => values => {
20+
const errors = {};
21+
if (values.mode === 'course') {
22+
if (values.course && !/^[A-Z0-9]{3,9}$/.test(values.course)) {
23+
errors.course = (
24+
<FormattedMessage
25+
id="app.addAttributeForm.validate.course"
26+
defaultMessage="Course identifier can contain only uppercase letters and digits and must have adequate length."
27+
/>
28+
);
29+
}
30+
} else if (values.mode === 'term') {
31+
if (values.term && !/^20[0-9]{2}-[12]$/.test(values.term)) {
32+
errors.term = (
33+
<FormattedMessage
34+
id="app.addAttributeForm.validate.term"
35+
defaultMessage="Semester must be in the format YYYY-T, where YYYY is the year and T is the term number (1-2)."
36+
/>
37+
);
38+
}
39+
} else if (values.mode === 'group') {
40+
if (values.group && !/^[a-zA-Z0-9]{8,16}$/.test(values.group)) {
41+
errors.group = (
42+
<FormattedMessage
43+
id="app.addAttributeForm.validate.group"
44+
defaultMessage="The identifier can contain only letters and digits and must be 8-16 characters long."
45+
/>
46+
);
47+
}
48+
} else if (values.mode === 'other') {
49+
if (values.key && !/^[-_a-zA-Z0-9]+$/.test(values.key)) {
50+
errors.key = (
51+
<FormattedMessage
52+
id="app.addAttributeForm.validate.key"
53+
defaultMessage="The key can contain only letters, digits, dash, and underscore."
54+
/>
55+
);
56+
}
57+
}
58+
59+
if (Object.keys(errors).length === 0) {
60+
const key = values.mode === 'other' ? values.key : values.mode;
61+
const value = values.mode === 'other' ? values.value : values[key];
62+
if (key && attributes && attributes[key] && attributes[key].includes(value)) {
63+
errors[values.mode === 'other' ? 'value' : key] = (
64+
<FormattedMessage
65+
id="app.addAttributeForm.validate.duplicate"
66+
defaultMessage="The attribute [{key}: {value}] is already associated with this group."
67+
values={{ key, value }}
68+
/>
69+
);
70+
}
71+
}
72+
return errors;
73+
});
74+
75+
const AddAttributeForm = ({ initialValues, onSubmit, onClose, attributes = EMPTY_OBJ }) => {
76+
return (
77+
<Form
78+
onSubmit={onSubmit}
79+
initialValues={initialValues}
80+
validate={validate(attributes)}
81+
render={({ handleSubmit, submitting, submitError }) => (
82+
<form onSubmit={handleSubmit}>
83+
<table className="mb-2">
84+
<tbody>
85+
<FormSpy subscription={{ values: true }}>
86+
{({ values: { mode } }) => (
87+
<>
88+
<tr className={mode === 'course' ? 'bg-success bg-opacity-10' : ''}>
89+
<td className="align-middle ps-3">
90+
<StandaloneRadioField name="mode" value="course" />
91+
</td>
92+
<td colSpan={2} className="w-100 px-3">
93+
<Field
94+
component={TextField}
95+
name="course"
96+
ignoreDirty
97+
disabled={mode !== 'course'}
98+
maxLength={9}
99+
placeholder="NPRG001"
100+
label={
101+
<>
102+
<FormattedMessage id="app.addAttributeForm.course" defaultMessage="Course" />:
103+
<Explanation id="course-explanation">
104+
<FormattedMessage
105+
id="app.addAttributeForm.course.explanation"
106+
defaultMessage="Associating course identifier enables bindings and group creations for SIS events of that course in the whole sub-tree."
107+
/>
108+
</Explanation>
109+
</>
110+
}
111+
/>
112+
</td>
113+
</tr>
114+
115+
<tr className={mode === 'term' ? 'bg-success bg-opacity-10' : ''}>
116+
<td className="align-middle ps-3">
117+
<StandaloneRadioField name="mode" value="term" />
118+
</td>
119+
<td colSpan={2} className="w-100 px-3">
120+
<Field
121+
component={TextField}
122+
name="term"
123+
ignoreDirty
124+
disabled={mode !== 'term'}
125+
maxLength={6}
126+
placeholder="2025-1"
127+
label={
128+
<>
129+
<FormattedMessage id="app.addAttributeForm.term" defaultMessage="Semester" />:
130+
<Explanation id="term-explanation">
131+
<FormattedMessage
132+
id="app.addAttributeForm.term.explanation"
133+
defaultMessage="Associating term (semester) identifier enables bindings and group creations for SIS events of that term in the whole sub-tree."
134+
/>
135+
</Explanation>
136+
</>
137+
}
138+
/>
139+
</td>
140+
</tr>
141+
142+
<tr className={mode === 'group' ? 'bg-success bg-opacity-10' : ''}>
143+
<td className="align-middle ps-3">
144+
<StandaloneRadioField name="mode" value="group" />
145+
</td>
146+
<td colSpan={2} className="w-100 px-3">
147+
<Field
148+
component={TextField}
149+
name="group"
150+
ignoreDirty
151+
disabled={mode !== 'group'}
152+
maxLength={20}
153+
placeholder="25aNPRG058x01"
154+
label={
155+
<>
156+
<FormattedMessage id="app.addAttributeForm.group" defaultMessage="SIS Scheduling Event" />
157+
:
158+
<Explanation id="group-explanation">
159+
<FormattedMessage
160+
id="app.addAttributeForm.group.explanation"
161+
defaultMessage="Association between groups and SIS events is usually done by binding or creating new groups from SIS events. This circumvents traditional checks, so any SIS event ID can be associated with this group. Please, handle with extreme care."
162+
/>
163+
</Explanation>
164+
</>
165+
}
166+
/>
167+
</td>
168+
</tr>
169+
170+
<tr className={mode === 'other' ? 'bg-success bg-opacity-10' : ''}>
171+
<td className="align-middle ps-3">
172+
<StandaloneRadioField name="mode" value="other" />
173+
</td>
174+
<td className="w-50 ps-3">
175+
<Field
176+
component={TextField}
177+
name="key"
178+
ignoreDirty
179+
disabled={mode !== 'other'}
180+
maxLength={32}
181+
label={
182+
<>
183+
<FormattedMessage id="app.addAttributeForm.key" defaultMessage="Custom Key" />:
184+
<Explanation id="other-explanation">
185+
<FormattedMessage
186+
id="app.addAttributeForm.other.explanation"
187+
defaultMessage="Creating custom attributes is intended to simplify preparations for future features. Avoid creating attributes unless you are absolutely certain what you are doing."
188+
/>
189+
</Explanation>
190+
</>
191+
}
192+
/>
193+
</td>
194+
<td className="w-50 pe-3">
195+
<Field
196+
component={TextField}
197+
name="value"
198+
ignoreDirty
199+
disabled={mode !== 'other'}
200+
maxLength={250}
201+
label={
202+
<>
203+
<FormattedMessage id="app.addAttributeForm.value" defaultMessage="Value" />:
204+
</>
205+
}
206+
/>
207+
</td>
208+
</tr>
209+
</>
210+
)}
211+
</FormSpy>
212+
</tbody>
213+
</table>
214+
215+
<FormSpy subscription={{ errors: true }}>
216+
{({ errors: { students } }) => students && <Callout variant="danger">{students}</Callout>}
217+
</FormSpy>
218+
219+
{submitError && <Callout variant="danger">{submitError}</Callout>}
220+
221+
<div className="text-center">
222+
<TheButtonGroup>
223+
<FormSpy subscription={{ values: true, valid: true }}>
224+
{({ values, valid }) => (
225+
<Button type="submit" variant="success" disabled={submitting || !valid || empty(values)}>
226+
{submitting ? <LoadingIcon gapRight /> : <SaveIcon gapRight />}
227+
<FormattedMessage id="generic.create" defaultMessage="Create" />
228+
</Button>
229+
)}
230+
</FormSpy>
231+
232+
{onClose && (
233+
<Button onClick={onClose} variant="secondary" disabled={submitting}>
234+
<CloseIcon gapRight />
235+
<FormattedMessage id="generic.cancel" defaultMessage="Cancel" />
236+
</Button>
237+
)}
238+
</TheButtonGroup>
239+
</div>
240+
</form>
241+
)}
242+
/>
243+
);
244+
};
245+
246+
AddAttributeForm.propTypes = {
247+
initialValues: PropTypes.object.isRequired,
248+
onSubmit: PropTypes.func.isRequired,
249+
onClose: PropTypes.func.isRequired,
250+
attributes: PropTypes.object,
251+
};
252+
253+
export default AddAttributeForm;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import AddAttributeForm from './AddAttributeForm.js';
2+
export default AddAttributeForm;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Field } from 'react-final-form';
4+
5+
const StandaloneRadioField = ({ name, value }) => {
6+
return (
7+
<div className="radio-container">
8+
<label>
9+
<Field name={name} component="input" type="radio" value={value} />
10+
<span className="radiomark"></span>
11+
</label>
12+
</div>
13+
);
14+
};
15+
16+
StandaloneRadioField.propTypes = {
17+
name: PropTypes.string.isRequired,
18+
value: PropTypes.string.isRequired,
19+
};
20+
21+
export default StandaloneRadioField;

src/components/forms/fields/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export { default as CheckboxField } from './CheckboxField.js';
22
export { default as DatetimeField } from './DatetimeField.js';
33
export { default as NumericTextField } from './NumericTextField.js';
44
export { default as SelectField } from './SelectField.js';
5+
export { default as StandaloneRadioField } from './StandaloneRadioField.js';
56
export { default as TextField } from './TextField.js';

src/locales/cs.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
{
2+
"app.addAttributeForm.course": "Předmět",
3+
"app.addAttributeForm.course.explanation": "Přídání atributu předmětu umožní vazby a vytváření skupin pro SIS události tohoto předmětu v celém podstromu.",
4+
"app.addAttributeForm.group": "SIS Rozvrhový lístek",
5+
"app.addAttributeForm.group.explanation": "Navazování rozvrhových lístků se obvykle řešeí na stránce Vytváření skupin. Vytvoření zde umožnuje obejít tradiční kontroly, takže jakýkoli SIS rozvrhový lístek může být spojen s touto skupinou. Prosíme, zacházejte s tímto s extrémní opatrností.",
6+
"app.addAttributeForm.key": "Vlastní klíč",
7+
"app.addAttributeForm.other.explanation": "Vytváření vlastních atributů je zmýšlenoo jako zjednodušení příprav na budoucí funkce. Vyvarujte se vytváření atributů, pokud si nejste naprosto jisti, co děláte.",
8+
"app.addAttributeForm.term": "Semestr",
9+
"app.addAttributeForm.term.explanation": "Asociování identifikátoru semestru umožňuje vazby a vytváření skupin pro SIS události tohoto semestru v celém podstromu.",
10+
"app.addAttributeForm.validate.course": "Identifikátor předmětu může obsahovat pouze velká písmena a číslice a musí mít přiměřenou délku.",
11+
"app.addAttributeForm.validate.duplicate": "Atribut [{key}: {value}] je již spojen s touto skupinou.",
12+
"app.addAttributeForm.validate.group": "Identifikátor může obsahovat pouze písmena a číslice a musí být dlouhý 8-16 znaků.",
13+
"app.addAttributeForm.validate.key": "Klíč může obsahovat pouze písmena, číslice, pomlčky a podtržítka.",
14+
"app.addAttributeForm.validate.term": "Semestr musí být ve formátu YYYY-S, kde YYYY je rok a S je číslo semestru (1-2).",
15+
"app.addAttributeForm.value": "Hodnota",
216
"app.apiErrorCodes.400": "Špatný požadavek",
317
"app.apiErrorCodes.400-001": "Uživatel má více registrovaných e-malových adres, které odpovídají více než jednomu existujícímu účtu. Asociace účtů není možná z důvodu nejednoznačnosti.",
418
"app.apiErrorCodes.400-003": "Název nahrávaného souboru obsahuje nepovolené znaky.",
@@ -89,6 +103,7 @@
89103
"app.groupsStudent.noActiveTerms": "V tuto chvíli nejsou studentům dostupné žádné semestry.",
90104
"app.groupsStudent.notStudent": "Tato stránka je dostupná pouze studentům.",
91105
"app.groupsStudent.title": "Připojení ke skupinám jako student",
106+
"app.groupsSupervisor.addAttributeModal.existingAttributes": "Existující atributy",
92107
"app.groupsSupervisor.addAttributeModal.title": "Přidat atribut ke skupině",
93108
"app.groupsSupervisor.currentlyManagedGroups": "Skupiny",
94109
"app.groupsSupervisor.notSuperadmin": "Tato stránka je k dispozici pouze administrátorům ReCodExu.",
@@ -257,4 +272,4 @@
257272
"generic.reset": "Resetovat",
258273
"generic.save": "Uložit",
259274
"generic.search": "Vyhledat"
260-
}
275+
}

src/locales/en.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
{
2+
"app.addAttributeForm.course": "Course",
3+
"app.addAttributeForm.course.explanation": "Associating course identifier enables bindings and group creations for SIS events of that course in the whole sub-tree.",
4+
"app.addAttributeForm.group": "SIS Scheduling Event",
5+
"app.addAttributeForm.group.explanation": "Association between groups and SIS events is usually done by binding or creating new groups from SIS events. This circumvents traditional checks, so any SIS event ID can be associated with this group. Please, handle with extreme care.",
6+
"app.addAttributeForm.key": "Custom Key",
7+
"app.addAttributeForm.other.explanation": "Creating custom attributes is intended to simplify preparations for future features. Avoid creating attributes unless you are absolutely certain what you are doing.",
8+
"app.addAttributeForm.term": "Semester",
9+
"app.addAttributeForm.term.explanation": "Associating term (semester) identifier enables bindings and group creations for SIS events of that term in the whole sub-tree.",
10+
"app.addAttributeForm.validate.course": "Course identifier can contain only uppercase letters and digits and must have adequate length.",
11+
"app.addAttributeForm.validate.duplicate": "The attribute [{key}: {value}] is already associated with this group.",
12+
"app.addAttributeForm.validate.group": "The identifier can contain only letters and digits and must be 8-16 characters long.",
13+
"app.addAttributeForm.validate.key": "The key can contain only letters, digits, dash, and underscore.",
14+
"app.addAttributeForm.validate.term": "Semester must be in the format YYYY-T, where YYYY is the year and T is the term number (1-2).",
15+
"app.addAttributeForm.value": "Value",
216
"app.apiErrorCodes.400": "Bad request",
317
"app.apiErrorCodes.400-001": "The user has multiple e-mail addresses and multiple matching accounts already exist. Accounts cannot be associated due to ambiguity.",
418
"app.apiErrorCodes.400-003": "Uploaded file name contains invalid characters.",
@@ -89,6 +103,7 @@
89103
"app.groupsStudent.noActiveTerms": "There are currently no terms available for students.",
90104
"app.groupsStudent.notStudent": "This page is available only to students.",
91105
"app.groupsStudent.title": "Joining Groups as Student",
106+
"app.groupsSupervisor.addAttributeModal.existingAttributes": "Existing attributes",
92107
"app.groupsSupervisor.addAttributeModal.title": "Add Attribute to Group",
93108
"app.groupsSupervisor.currentlyManagedGroups": "Groups",
94109
"app.groupsSupervisor.notSuperadmin": "This page is available to ReCodEx administrators only.",
@@ -235,7 +250,7 @@
235250
"app.user.sisUserFailedCallout": "SIS data (re)loading failed.",
236251
"app.user.sisUserLoadedCallout": "The SIS user data were successfully (re)loaded.",
237252
"app.user.syncButton": "Update ReCodEx profile with data from SIS",
238-
"app.user.syncButtonConfirmEmail": "You are about to update your e-mail address so you will be required to verify it afterwards in ReCodEx. The e-mail address is also used as your local login if you already have local account (yout local password will not be changed).",
253+
"app.user.syncButtonConfirmEmail": "You are about to update your e-mail address so you will be required to verify it afterwards in ReCodEx. The e-mail address is also used as your local login if you already have local account (your local password will not be changed).",
239254
"app.user.title": "Personal Data",
240255
"app.user.userSyncCanceledCallout": "User sync operation was canceled, because the ReCodEx profile data were outdated and needed to be reloaded. Please, re-start the operation if it is still desired.",
241256
"app.user.userSyncFailedCallout": "User sync operation failed. Reload the page and try again later.",

0 commit comments

Comments
 (0)