Skip to content

Commit 4619ed4

Browse files
SemaiCZEMartin Kruliš
authored andcommitted
System messages global edit page
1 parent d055c69 commit 4619ed4

File tree

18 files changed

+628
-2
lines changed

18 files changed

+628
-2
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
4+
import { defaultMemoize } from 'reselect';
5+
6+
import SortableTable, { SortableTableColumnDescriptor } from '../../widgets/SortableTable';
7+
import { getLocalizedText } from '../../../helpers/localizedData';
8+
import DateTime from '../../widgets/DateTime';
9+
import { roleLabels } from '../../helpers/usersRoles';
10+
import UsersNameContainer from '../../../containers/UsersNameContainer';
11+
12+
class MessagesList extends Component {
13+
prepareColumnDescriptors = defaultMemoize((systemMessages, locale) => {
14+
const columns = [
15+
new SortableTableColumnDescriptor(
16+
'text',
17+
<FormattedMessage id="app.systemMessagesList.text" defaultMessage="Text" />,
18+
{
19+
className: 'text-left',
20+
comparator: ({ text: t1 }, { text: t2 }) =>
21+
getLocalizedText(t1, locale).localeCompare(getLocalizedText(t2, locale), locale),
22+
cellRenderer: text => text && <i>{getLocalizedText(text, locale)}</i>,
23+
}
24+
),
25+
26+
new SortableTableColumnDescriptor(
27+
'visibleFrom',
28+
<FormattedMessage id="app.systemMessagesList.visibleFrom" defaultMessage="Visible From" />,
29+
{
30+
comparator: ({ visibleFrom: f1 }, { visibleFrom: f2 }) => f2 - f1,
31+
cellRenderer: visibleFrom => visibleFrom && <DateTime unixts={visibleFrom} />,
32+
}
33+
),
34+
35+
new SortableTableColumnDescriptor(
36+
'visibleTo',
37+
<FormattedMessage id="app.systemMessagesList.visibleTo" defaultMessage="Visible To" />,
38+
{
39+
comparator: ({ visibleTo: t1 }, { visibleTo: t2 }) => t2 - t1,
40+
cellRenderer: visibleTo => visibleTo && <DateTime unixts={visibleTo} />,
41+
}
42+
),
43+
44+
new SortableTableColumnDescriptor('authorId', <FormattedMessage id="generic.name" defaultMessage="Name" />, {
45+
cellRenderer: authorId => authorId && <UsersNameContainer userId={authorId} />,
46+
}),
47+
48+
new SortableTableColumnDescriptor(
49+
'role',
50+
<FormattedMessage id="app.systemMessagesList.role" defaultMessage="Role" />,
51+
{
52+
comparator: ({ role: r1 }, { role: r2 }) => r1.localeCompare(r2, locale),
53+
cellRenderer: role => role && roleLabels[role],
54+
}
55+
),
56+
57+
new SortableTableColumnDescriptor(
58+
'type',
59+
<FormattedMessage id="app.systemMessagesList.type" defaultMessage="Type" />,
60+
{
61+
comparator: ({ type: t1 }, { type: t2 }) => t1.localeCompare(t2, locale),
62+
cellRenderer: type => type && <span>{type}</span>,
63+
}
64+
),
65+
66+
new SortableTableColumnDescriptor('buttons', '', {
67+
className: 'text-right',
68+
}),
69+
];
70+
71+
return columns;
72+
});
73+
74+
prepareData = defaultMemoize(systemMessages => {
75+
const { renderActions } = this.props;
76+
77+
return systemMessages.map(message => {
78+
const data = {
79+
text: { localizedTexts: message.localizedTexts },
80+
visibleFrom: message.visibleFrom,
81+
visibleTo: message.visibleTo,
82+
authorId: message.authorId,
83+
role: message.role,
84+
type: message.type,
85+
buttons: renderActions && renderActions(message),
86+
};
87+
return data;
88+
});
89+
});
90+
91+
render() {
92+
const {
93+
systemMessages,
94+
intl: { locale },
95+
} = this.props;
96+
97+
return (
98+
<SortableTable
99+
hover
100+
columns={this.prepareColumnDescriptors(systemMessages, locale)}
101+
defaultOrder="visibleTo"
102+
data={this.prepareData(systemMessages)}
103+
empty={
104+
<div className="text-center text-muted">
105+
<FormattedMessage
106+
id="app.systemMessagesList.noMessages"
107+
defaultMessage="There are currently no system messages."
108+
/>
109+
</div>
110+
}
111+
/>
112+
);
113+
}
114+
}
115+
116+
MessagesList.propTypes = {
117+
systemMessages: PropTypes.array.isRequired,
118+
intl: intlShape.isRequired,
119+
renderActions: PropTypes.func,
120+
};
121+
122+
export default injectIntl(MessagesList);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import MessagesList from './MessagesList';
2+
export default MessagesList;
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { reduxForm, Field, FieldArray } from 'redux-form';
4+
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
5+
import { Alert, Modal, Button } from 'react-bootstrap';
6+
7+
import { SelectField, DatetimeField } from '../Fields';
8+
9+
import SubmitButton from '../SubmitButton';
10+
import LocalizedTextsFormField from '../LocalizedTextsFormField';
11+
import { validateLocalizedTextsFormData } from '../../../helpers/localizedData';
12+
import withLinks from '../../../helpers/withLinks';
13+
import { CloseIcon } from '../../icons';
14+
import { roleLabelsSimpleMessages } from '../../helpers/usersRoles';
15+
16+
const EditSystemMessageForm = ({
17+
initialValues,
18+
error,
19+
dirty,
20+
submitting,
21+
handleSubmit,
22+
submitFailed,
23+
submitSucceeded,
24+
invalid,
25+
asyncValidating,
26+
isOpen,
27+
onClose,
28+
intl: { formatMessage },
29+
}) => (
30+
<Modal show={isOpen} backdrop="static" size="lg" onHide={onClose}>
31+
<Modal.Header closeButton>
32+
<Modal.Title>
33+
<FormattedMessage id="app.editSystemMessageForm.title" defaultMessage="Edit System Message" />
34+
</Modal.Title>
35+
</Modal.Header>
36+
<Modal.Body>
37+
{submitFailed && (
38+
<Alert bsStyle="danger">
39+
<FormattedMessage id="generic.savingFailed" defaultMessage="Saving failed. Please try again later." />
40+
</Alert>
41+
)}
42+
43+
<FieldArray name="localizedTexts" component={LocalizedTextsFormField} fieldType="systemMessage" />
44+
45+
<Field
46+
name="type"
47+
component={SelectField}
48+
options={[
49+
{ key: 'info', name: 'Info' },
50+
{ key: 'warning', name: 'Warning' },
51+
{ key: 'danger', name: 'Danger' },
52+
]}
53+
addEmptyOption
54+
label={<FormattedMessage id="app.editSystemMessageForm.type" defaultMessage="Type of the notification." />}
55+
/>
56+
57+
<Field
58+
name="role"
59+
component={SelectField}
60+
options={Object.keys(roleLabelsSimpleMessages).map(role => ({
61+
key: role,
62+
name: formatMessage(roleLabelsSimpleMessages[role]),
63+
}))}
64+
addEmptyOption
65+
label={
66+
<FormattedMessage
67+
id="app.editSystemMessageForm.role"
68+
defaultMessage="Users with this role and its children can see notification."
69+
/>
70+
}
71+
/>
72+
73+
<Field
74+
name="visibleFrom"
75+
component={DatetimeField}
76+
label={
77+
<FormattedMessage
78+
id="app.editSystemMessageForm.visibleFrom"
79+
defaultMessage="Date from which is notification visible."
80+
/>
81+
}
82+
/>
83+
84+
<Field
85+
name="visibleTo"
86+
component={DatetimeField}
87+
label={
88+
<FormattedMessage
89+
id="app.editSystemMessageForm.isLocked"
90+
defaultMessage="Date to which is notification visible."
91+
/>
92+
}
93+
/>
94+
95+
{error && dirty && <Alert bsStyle="danger">{error}</Alert>}
96+
</Modal.Body>
97+
<Modal.Footer>
98+
<div className="text-center">
99+
<SubmitButton
100+
id="editSystemMessage"
101+
invalid={invalid}
102+
submitting={submitting}
103+
dirty={dirty}
104+
hasSucceeded={submitSucceeded}
105+
hasFailed={submitFailed}
106+
handleSubmit={handleSubmit}
107+
asyncValidating={asyncValidating}
108+
messages={{
109+
submit: <FormattedMessage id="generic.save" defaultMessage="Save" />,
110+
submitting: <FormattedMessage id="generic.saving" defaultMessage="Saving..." />,
111+
success: <FormattedMessage id="generic.saved" defaultMessage="Saved" />,
112+
validating: <FormattedMessage id="generic.validating" defaultMessage="Validating..." />,
113+
}}
114+
/>
115+
116+
<Button bsStyle="default" className="btn-flat" onClick={onClose}>
117+
<CloseIcon gapRight />
118+
<FormattedMessage id="generic.close" defaultMessage="Close" />
119+
</Button>
120+
</div>
121+
</Modal.Footer>
122+
</Modal>
123+
);
124+
125+
EditSystemMessageForm.propTypes = {
126+
error: PropTypes.any,
127+
initialValues: PropTypes.object.isRequired,
128+
values: PropTypes.object,
129+
handleSubmit: PropTypes.func.isRequired,
130+
dirty: PropTypes.bool,
131+
submitting: PropTypes.bool,
132+
submitFailed: PropTypes.bool,
133+
submitSucceeded: PropTypes.bool,
134+
invalid: PropTypes.bool,
135+
asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
136+
links: PropTypes.object,
137+
isOpen: PropTypes.bool.isRequired,
138+
onClose: PropTypes.func.isRequired,
139+
intl: intlShape.isRequired,
140+
};
141+
142+
const validate = ({ localizedTexts }) => {
143+
const errors = {};
144+
validateLocalizedTextsFormData(errors, localizedTexts, ({ text }) => {
145+
const textErrors = {};
146+
if (!text.trim()) {
147+
textErrors.text = (
148+
<FormattedMessage
149+
id="app.editSystemMessageForm.validation.localizedText.text"
150+
defaultMessage="Please fill the description."
151+
/>
152+
);
153+
}
154+
155+
return textErrors;
156+
});
157+
158+
return errors;
159+
};
160+
161+
export default withLinks(
162+
reduxForm({
163+
form: 'editSystemMessage',
164+
validate,
165+
enableReinitialize: true,
166+
keepDirtyOnReinitialize: false,
167+
})(injectIntl(EditSystemMessageForm))
168+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import EditSystemMessageForm from './EditSystemMessageForm';
2+
export default EditSystemMessageForm;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Well } from 'react-bootstrap';
4+
import { FormattedMessage } from 'react-intl';
5+
import { Field } from 'redux-form';
6+
7+
import { MarkdownTextAreaField, CheckboxField } from '../Fields';
8+
9+
const LocalizedSystemMessageFormField = ({ prefix, data: enabled }) => (
10+
<Well>
11+
<Field
12+
name={`${prefix}._enabled`}
13+
component={CheckboxField}
14+
onOff
15+
label={
16+
<FormattedMessage
17+
id="app.editLocalizedTextForm.localeEnabledCheckbox"
18+
defaultMessage="Enable this localization"
19+
/>
20+
}
21+
/>
22+
23+
<Field
24+
name={`${prefix}.text`}
25+
component={MarkdownTextAreaField}
26+
disabled={!enabled}
27+
label={
28+
<FormattedMessage
29+
id="app.editLocalizedTextForm.localized.description"
30+
defaultMessage="Text of system message:"
31+
/>
32+
}
33+
/>
34+
</Well>
35+
);
36+
37+
LocalizedSystemMessageFormField.propTypes = {
38+
prefix: PropTypes.string.isRequired,
39+
data: PropTypes.bool.isRequired,
40+
};
41+
42+
export default LocalizedSystemMessageFormField;

src/components/forms/LocalizedTextsFormField/LocalizedTextsFormField.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import LocalizedAssignmentFormField from './LocalizedAssignmentFormField';
88
import LocalizedShadowAssignmentFormField from './LocalizedShadowAssignmentFormField';
99
import LocalizedExerciseFormField from './LocalizedExerciseFormField';
1010
import LocalizedGroupFormField from './LocalizedGroupFormField';
11+
import LocalizedSystemMessageFormField from './LocalizedSystemMessageFormField';
1112
import { WarningIcon } from '../../icons';
1213
import { knownLocalesNames } from '../../../helpers/localizedData';
1314

@@ -16,6 +17,7 @@ const fieldTypes = {
1617
shadowAssignment: LocalizedShadowAssignmentFormField,
1718
exercise: LocalizedExerciseFormField,
1819
group: LocalizedGroupFormField,
20+
systemMessage: LocalizedSystemMessageFormField,
1921
};
2022

2123
const renderTitle = ({ locale, _enabled }) => (

src/components/helpers/usersRoles.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormattedMessage } from 'react-intl';
3+
import { FormattedMessage, defineMessages } from 'react-intl';
44
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
55
import { SuperadminIcon, EmpoweredSupervisorIcon, SupervisorIcon, SupervisorStudentIcon, UserIcon } from '../icons';
66

@@ -23,6 +23,14 @@ export const roleLabels = {
2323
[SUPERADMIN_ROLE]: <FormattedMessage id="app.roles.superadmin" defaultMessage="Main Administrator" />,
2424
};
2525

26+
export const roleLabelsSimpleMessages = defineMessages({
27+
[STUDENT_ROLE]: { id: 'app.roles.student', defaultMessage: 'Student' },
28+
[SUPERVISOR_STUDENT_ROLE]: { id: 'app.roles.supervisorStudent', defaultMessage: 'Supervisor-student' },
29+
[SUPERVISOR_ROLE]: { id: 'app.roles.supervisor', defaultMessage: 'Supervisor' },
30+
[EMPOWERED_SUPERVISOR_ROLE]: { id: 'app.roles.empoweredSupervisor', defaultMessage: 'Empowered Supervisor' },
31+
[SUPERADMIN_ROLE]: { id: 'app.roles.superadmin', defaultMessage: 'Main Administrator' },
32+
});
33+
2634
export const roleLabelsPlural = {
2735
[STUDENT_ROLE]: <FormattedMessage id="app.roles.students" defaultMessage="Students" />,
2836
[SUPERVISOR_STUDENT_ROLE]: (

0 commit comments

Comments
 (0)