|
| 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 | +export const INITIAL_VALUES = {}; |
| 76 | + |
| 77 | +const PlantTermGroupsForm = ({ initialValues, onSubmit, onClose, attributes = EMPTY_OBJ }) => { |
| 78 | + return ( |
| 79 | + <Form |
| 80 | + onSubmit={onSubmit} |
| 81 | + initialValues={initialValues} |
| 82 | + validate={validate(attributes)} |
| 83 | + render={({ handleSubmit, submitting, submitError }) => ( |
| 84 | + <form onSubmit={handleSubmit}> |
| 85 | + <table className="mb-2"> |
| 86 | + <tbody> |
| 87 | + <FormSpy subscription={{ values: true }}> |
| 88 | + {({ values: { mode } }) => ( |
| 89 | + <> |
| 90 | + <tr className={mode === 'course' ? 'bg-success bg-opacity-10' : ''}> |
| 91 | + <td className="align-middle ps-3"> |
| 92 | + <StandaloneRadioField name="mode" value="course" /> |
| 93 | + </td> |
| 94 | + <td colSpan={2} className="w-100 px-3"> |
| 95 | + <Field |
| 96 | + component={TextField} |
| 97 | + name="course" |
| 98 | + ignoreDirty |
| 99 | + disabled={mode !== 'course'} |
| 100 | + maxLength={9} |
| 101 | + placeholder="NPRG001" |
| 102 | + label={ |
| 103 | + <> |
| 104 | + <FormattedMessage id="app.addAttributeForm.course" defaultMessage="Course" />: |
| 105 | + <Explanation id="course-explanation"> |
| 106 | + <FormattedMessage |
| 107 | + id="app.addAttributeForm.course.explanation" |
| 108 | + defaultMessage="Associating course identifier enables bindings and group creations for SIS events of that course in the whole sub-tree." |
| 109 | + /> |
| 110 | + </Explanation> |
| 111 | + </> |
| 112 | + } |
| 113 | + /> |
| 114 | + </td> |
| 115 | + </tr> |
| 116 | + |
| 117 | + <tr className={mode === 'term' ? 'bg-success bg-opacity-10' : ''}> |
| 118 | + <td className="align-middle ps-3"> |
| 119 | + <StandaloneRadioField name="mode" value="term" /> |
| 120 | + </td> |
| 121 | + <td colSpan={2} className="w-100 px-3"> |
| 122 | + <Field |
| 123 | + component={TextField} |
| 124 | + name="term" |
| 125 | + ignoreDirty |
| 126 | + disabled={mode !== 'term'} |
| 127 | + maxLength={6} |
| 128 | + placeholder="2025-1" |
| 129 | + label={ |
| 130 | + <> |
| 131 | + <FormattedMessage id="app.addAttributeForm.term" defaultMessage="Semester" />: |
| 132 | + <Explanation id="term-explanation"> |
| 133 | + <FormattedMessage |
| 134 | + id="app.addAttributeForm.term.explanation" |
| 135 | + defaultMessage="Associating term (semester) identifier enables bindings and group creations for SIS events of that term in the whole sub-tree." |
| 136 | + /> |
| 137 | + </Explanation> |
| 138 | + </> |
| 139 | + } |
| 140 | + /> |
| 141 | + </td> |
| 142 | + </tr> |
| 143 | + |
| 144 | + <tr className={mode === 'group' ? 'bg-success bg-opacity-10' : ''}> |
| 145 | + <td className="align-middle ps-3"> |
| 146 | + <StandaloneRadioField name="mode" value="group" /> |
| 147 | + </td> |
| 148 | + <td colSpan={2} className="w-100 px-3"> |
| 149 | + <Field |
| 150 | + component={TextField} |
| 151 | + name="group" |
| 152 | + ignoreDirty |
| 153 | + disabled={mode !== 'group'} |
| 154 | + maxLength={20} |
| 155 | + placeholder="25aNPRG058x01" |
| 156 | + label={ |
| 157 | + <> |
| 158 | + <FormattedMessage id="app.addAttributeForm.group" defaultMessage="SIS Scheduling Event" /> |
| 159 | + : |
| 160 | + <Explanation id="group-explanation"> |
| 161 | + <FormattedMessage |
| 162 | + id="app.addAttributeForm.group.explanation" |
| 163 | + 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." |
| 164 | + /> |
| 165 | + </Explanation> |
| 166 | + </> |
| 167 | + } |
| 168 | + /> |
| 169 | + </td> |
| 170 | + </tr> |
| 171 | + |
| 172 | + <tr className={mode === 'other' ? 'bg-success bg-opacity-10' : ''}> |
| 173 | + <td className="align-middle ps-3"> |
| 174 | + <StandaloneRadioField name="mode" value="other" /> |
| 175 | + </td> |
| 176 | + <td className="w-50 ps-3"> |
| 177 | + <Field |
| 178 | + component={TextField} |
| 179 | + name="key" |
| 180 | + ignoreDirty |
| 181 | + disabled={mode !== 'other'} |
| 182 | + maxLength={32} |
| 183 | + label={ |
| 184 | + <> |
| 185 | + <FormattedMessage id="app.addAttributeForm.key" defaultMessage="Custom Key" />: |
| 186 | + <Explanation id="other-explanation"> |
| 187 | + <FormattedMessage |
| 188 | + id="app.addAttributeForm.other.explanation" |
| 189 | + defaultMessage="Creating custom attributes is intended to simplify preparations for future features. Avoid creating attributes unless you are absolutely certain what you are doing." |
| 190 | + /> |
| 191 | + </Explanation> |
| 192 | + </> |
| 193 | + } |
| 194 | + /> |
| 195 | + </td> |
| 196 | + <td className="w-50 pe-3"> |
| 197 | + <Field |
| 198 | + component={TextField} |
| 199 | + name="value" |
| 200 | + ignoreDirty |
| 201 | + disabled={mode !== 'other'} |
| 202 | + maxLength={250} |
| 203 | + label={ |
| 204 | + <> |
| 205 | + <FormattedMessage id="app.addAttributeForm.value" defaultMessage="Value" />: |
| 206 | + </> |
| 207 | + } |
| 208 | + /> |
| 209 | + </td> |
| 210 | + </tr> |
| 211 | + </> |
| 212 | + )} |
| 213 | + </FormSpy> |
| 214 | + </tbody> |
| 215 | + </table> |
| 216 | + |
| 217 | + <FormSpy subscription={{ errors: true }}> |
| 218 | + {({ errors: { students } }) => students && <Callout variant="danger">{students}</Callout>} |
| 219 | + </FormSpy> |
| 220 | + |
| 221 | + {submitError && <Callout variant="danger">{submitError}</Callout>} |
| 222 | + |
| 223 | + <div className="text-center"> |
| 224 | + <TheButtonGroup> |
| 225 | + <FormSpy subscription={{ values: true, valid: true }}> |
| 226 | + {({ values, valid }) => ( |
| 227 | + <Button type="submit" variant="success" disabled={submitting || !valid || empty(values)}> |
| 228 | + {submitting ? <LoadingIcon gapRight /> : <SaveIcon gapRight />} |
| 229 | + <FormattedMessage id="generic.create" defaultMessage="Create" /> |
| 230 | + </Button> |
| 231 | + )} |
| 232 | + </FormSpy> |
| 233 | + |
| 234 | + {onClose && ( |
| 235 | + <Button onClick={onClose} variant="secondary" disabled={submitting}> |
| 236 | + <CloseIcon gapRight /> |
| 237 | + <FormattedMessage id="generic.cancel" defaultMessage="Cancel" /> |
| 238 | + </Button> |
| 239 | + )} |
| 240 | + </TheButtonGroup> |
| 241 | + </div> |
| 242 | + </form> |
| 243 | + )} |
| 244 | + /> |
| 245 | + ); |
| 246 | +}; |
| 247 | + |
| 248 | +PlantTermGroupsForm.propTypes = { |
| 249 | + initialValues: PropTypes.object.isRequired, |
| 250 | + onSubmit: PropTypes.func.isRequired, |
| 251 | + onClose: PropTypes.func.isRequired, |
| 252 | + attributes: PropTypes.object, |
| 253 | +}; |
| 254 | + |
| 255 | +export default PlantTermGroupsForm; |
0 commit comments