Skip to content

Commit f7182ba

Browse files
author
Martin Krulis
committed
Group relocation form added on the Edit Group page.
1 parent 1435153 commit f7182ba

File tree

7 files changed

+166
-4
lines changed

7 files changed

+166
-4
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { injectIntl, FormattedMessage, intlShape } from 'react-intl';
4+
import { Alert, Form } from 'react-bootstrap';
5+
import { reduxForm, Field } from 'redux-form';
6+
import { defaultMemoize } from 'reselect';
7+
8+
import { SelectField } from '../Fields';
9+
import SubmitButton from '../SubmitButton';
10+
import Icon, { WarningIcon } from '../../../components/icons';
11+
import { getGroupCanonicalLocalizedName } from '../../../helpers/localizedData';
12+
import { hasPermissions } from '../../../helpers/common';
13+
14+
export const getPossibleParentsOfGroup = defaultMemoize((groups, group) =>
15+
groups.filter(g => g.id !== group.id && hasPermissions(g, 'addSubgroup') && !g.parentGroupsIds.includes(group.id))
16+
);
17+
18+
const RelocateGroupForm = ({
19+
submitting,
20+
handleSubmit,
21+
submitFailed,
22+
submitSucceeded,
23+
invalid,
24+
groups,
25+
groupsAccessor,
26+
intl: { locale },
27+
}) => (
28+
<div>
29+
{submitFailed && (
30+
<Alert bsStyle="danger">
31+
<WarningIcon gapRight />
32+
<FormattedMessage id="generic.savingFailed" defaultMessage="Saving failed. Please try again later." />
33+
</Alert>
34+
)}
35+
<Form>
36+
<Field
37+
name={'groupId'}
38+
component={SelectField}
39+
label={<FormattedMessage id="app.relocateGroupForm.parentGroup" defaultMessage="Parent Group:" />}
40+
options={groups
41+
.map(group => ({
42+
key: group.id,
43+
name: getGroupCanonicalLocalizedName(group, groupsAccessor, locale),
44+
}))
45+
.sort((a, b) => a.name.localeCompare(b.name, locale))}
46+
/>
47+
48+
<div className="text-center">
49+
<SubmitButton
50+
id="relocateGroup"
51+
disabled={invalid}
52+
submitting={submitting}
53+
hasSucceeded={submitSucceeded}
54+
hasFailed={submitFailed}
55+
handleSubmit={handleSubmit}
56+
defaultIcon={<Icon icon="people-carry" gapRight />}
57+
messages={{
58+
submit: <FormattedMessage id="app.relocateGroupForm.submit" defaultMessage="Relocate" />,
59+
submitting: <FormattedMessage id="app.relocateGroupForm.submitting" defaultMessage="Relocating..." />,
60+
success: <FormattedMessage id="app.relocateGroupForm.success" defaultMessage="Group Relocated" />,
61+
}}
62+
/>
63+
</div>
64+
</Form>
65+
</div>
66+
);
67+
68+
RelocateGroupForm.propTypes = {
69+
history: PropTypes.shape({
70+
push: PropTypes.func.isRequired,
71+
replace: PropTypes.func.isRequired,
72+
}),
73+
submitting: PropTypes.bool,
74+
submitFailed: PropTypes.bool,
75+
submitSucceeded: PropTypes.bool,
76+
invalid: PropTypes.bool,
77+
handleSubmit: PropTypes.func.isRequired,
78+
links: PropTypes.object,
79+
groups: PropTypes.array,
80+
groupsAccessor: PropTypes.func.isRequired,
81+
intl: intlShape,
82+
};
83+
84+
export default reduxForm({
85+
form: 'relocateGroup',
86+
enableReinitialize: true,
87+
keepDirtyOnReinitialize: false,
88+
})(injectIntl(RelocateGroupForm));
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import RelocateGroupForm from './RelocateGroupForm';
2+
export default RelocateGroupForm;
3+
export { getPossibleParentsOfGroup } from './RelocateGroupForm';

src/locales/cs.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
"app.editGroup.deleteGroupWarning": "Smazání skupiny způsobí, že všechny navázané entity (zadané úlohy, odevzdaná řešení, ...) nebudou přístupné.",
351351
"app.editGroup.description": "Změnit nastavení skupiny",
352352
"app.editGroup.organizationalExplain": "Běžné skupiny jsou platformou spojující studenty a zadané úlohy. Organizační skupiny slouží pouze k vytváření hierarchie, takže v nich nesmí být přihlášení studenti a nesmí obsahovat zadané úlohy.",
353+
"app.editGroup.relocateGroup": "Přemístit skupinu",
353354
"app.editGroup.title": "Upravit skupinu",
354355
"app.editGroupForm.createGroup": "Vytvořit skupinu",
355356
"app.editGroupForm.description": "Popis skupiny:",
@@ -1030,6 +1031,10 @@
10301031
"app.registrationForm.validation.passwordDontMatch": "Hesla se neshodují.",
10311032
"app.registrationForm.validation.shortFirstName": "Jméno musí obsahovat alespoň 2 znaky.",
10321033
"app.registrationForm.validation.shortLastName": "Příjmení musí obsahovat alespoň 2 znaky.",
1034+
"app.relocateGroupForm.parentGroup": "Rodičovská skupina:",
1035+
"app.relocateGroupForm.submit": "Přemístit",
1036+
"app.relocateGroupForm.submitting": "Přemísťuji...",
1037+
"app.relocateGroupForm.success": "Skupina přemístěna",
10331038
"app.removeFromGroup.confirm": "Opravdu chcete odstranit uživatele z této skupiny?",
10341039
"app.resendEmailVerification.failed": "Opětovné odeslání selhalo. Prosíme opakujte akci později",
10351040
"app.resendEmailVerification.resend": "Znovu odeslat ověřovací email",

src/locales/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,10 +346,11 @@
346346
"app.editGroup.archivedExplain": "Archived groups are containers for students, assignments and results after the course is finished. They are immutable and can be accessed through separate Archive page.",
347347
"app.editGroup.cannotDeleteGroupWithSubgroups": "Group with nested sub-groups cannot be deleted.",
348348
"app.editGroup.cannotDeleteRootGroup": "This is a so-called root group and it cannot be deleted.",
349-
"app.editGroup.deleteGroup": "Delete the group",
349+
"app.editGroup.deleteGroup": "Delete Group",
350350
"app.editGroup.deleteGroupWarning": "Deleting a group will make all attached entities (assignments, solutions, ...) inaccessible.",
351351
"app.editGroup.description": "Change group settings",
352352
"app.editGroup.organizationalExplain": "Regular groups are containers for students and assignments. Organizational groups are intended to create hierarchy, so they are forbidden to hold any students or assignments.",
353+
"app.editGroup.relocateGroup": "Relocate Group",
353354
"app.editGroup.title": "Edit Group",
354355
"app.editGroupForm.createGroup": "Create Group",
355356
"app.editGroupForm.description": "Group description:",
@@ -1030,6 +1031,10 @@
10301031
"app.registrationForm.validation.passwordDontMatch": "Passwords do not match.",
10311032
"app.registrationForm.validation.shortFirstName": "First name must contain at least 2 characters.",
10321033
"app.registrationForm.validation.shortLastName": "Last name must contain at least 2 characters.",
1034+
"app.relocateGroupForm.parentGroup": "Parent Group:",
1035+
"app.relocateGroupForm.submit": "Relocate",
1036+
"app.relocateGroupForm.submitting": "Relocating...",
1037+
"app.relocateGroupForm.success": "Group Relocated",
10331038
"app.removeFromGroup.confirm": "Are you sure you want to remove the user from this group?",
10341039
"app.resendEmailVerification.failed": "Resending failed",
10351040
"app.resendEmailVerification.resend": "Resend verification email",

src/locales/whitelist_en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
"app.editGroup.deleteGroupWarning",
351351
"app.editGroup.description",
352352
"app.editGroup.organizationalExplain",
353+
"app.editGroup.relocateGroup",
353354
"app.editGroup.title",
354355
"app.editGroupForm.createGroup",
355356
"app.editGroupForm.description",
@@ -1030,6 +1031,10 @@
10301031
"app.registrationForm.validation.passwordDontMatch",
10311032
"app.registrationForm.validation.shortFirstName",
10321033
"app.registrationForm.validation.shortLastName",
1034+
"app.relocateGroupForm.parentGroup",
1035+
"app.relocateGroupForm.submit",
1036+
"app.relocateGroupForm.submitting",
1037+
"app.relocateGroupForm.success",
10331038
"app.removeFromGroup.confirm",
10341039
"app.resendEmailVerification.failed",
10351040
"app.resendEmailVerification.resend",

src/pages/EditGroup/EditGroup.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,22 @@ import { reset, formValueSelector } from 'redux-form';
88
import { defaultMemoize } from 'reselect';
99

1010
import Page from '../../components/layout/Page';
11+
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
1112
import EditGroupForm, { EDIT_GROUP_FORM_LOCALIZED_TEXTS_DEFAULT } from '../../components/forms/EditGroupForm';
13+
import RelocateGroupForm, { getPossibleParentsOfGroup } from '../../components/forms/RelocateGroupForm';
1214
import OrganizationalGroupButtonContainer from '../../containers/OrganizationalGroupButtonContainer';
1315
import ArchiveGroupButtonContainer from '../../containers/ArchiveGroupButtonContainer';
1416
import DeleteGroupButtonContainer from '../../containers/DeleteGroupButtonContainer';
1517
import Box from '../../components/widgets/Box';
1618
import { BanIcon, InfoIcon } from '../../components/icons';
1719

18-
import { fetchGroup, fetchGroupIfNeeded, editGroup } from '../../redux/modules/groups';
19-
import { groupSelector, canViewParentDetailSelector } from '../../redux/selectors/groups';
20+
import { fetchGroup, fetchGroupIfNeeded, editGroup, relocateGroup } from '../../redux/modules/groups';
21+
import {
22+
groupSelector,
23+
canViewParentDetailSelector,
24+
notArchivedGroupsSelector,
25+
groupDataAccessorSelector,
26+
} from '../../redux/selectors/groups';
2027
import { loggedInUserIdSelector, selectedInstanceId } from '../../redux/selectors/auth';
2128
import { isSupervisorOf, isLoggedAsSuperAdmin } from '../../redux/selectors/users';
2229
import {
@@ -29,6 +36,10 @@ import withLinks from '../../helpers/withLinks';
2936
import { hasPermissions } from '../../helpers/common';
3037
import GroupArchivedWarning from '../../components/Groups/GroupArchivedWarning/GroupArchivedWarning';
3138

39+
const canRelocate = group => hasPermissions(group, 'relocate') && !group.archived;
40+
41+
const getRelocateFormInitialValues = defaultMemoize(group => ({ groupId: group.parentGroupId }));
42+
3243
class EditGroup extends Component {
3344
componentDidMount = () => this.props.loadAsync();
3445

@@ -57,9 +68,12 @@ class EditGroup extends Component {
5768
},
5869
history: { replace },
5970
group,
71+
groups,
72+
groupsAccessor,
6073
isSuperAdmin,
6174
links: { GROUP_INFO_URI_FACTORY, GROUP_DETAIL_URI_FACTORY, INSTANCE_URI_FACTORY },
6275
editGroup,
76+
relocateGroup,
6377
hasThreshold,
6478
canViewParentDetail,
6579
instanceId,
@@ -156,10 +170,29 @@ class EditGroup extends Component {
156170
/>
157171
)}
158172

173+
{canRelocate(group) && (
174+
<ResourceRenderer resource={groups.toArray()} returnAsArray>
175+
{groups =>
176+
getPossibleParentsOfGroup(groups, group).length > 1 && (
177+
<Box
178+
type="warning"
179+
title={<FormattedMessage id="app.editGroup.relocateGroup" defaultMessage="Relocate Group" />}>
180+
<RelocateGroupForm
181+
initialValues={getRelocateFormInitialValues(group)}
182+
groups={getPossibleParentsOfGroup(groups, group)}
183+
groupsAccessor={groupsAccessor}
184+
onSubmit={relocateGroup}
185+
/>
186+
</Box>
187+
)
188+
}
189+
</ResourceRenderer>
190+
)}
191+
159192
{hasPermissions(group, 'remove') && (
160193
<Box
161194
type="danger"
162-
title={<FormattedMessage id="app.editGroup.deleteGroup" defaultMessage="Delete the group" />}>
195+
title={<FormattedMessage id="app.editGroup.deleteGroup" defaultMessage="Delete Group" />}>
163196
<div>
164197
<p>
165198
<FormattedMessage
@@ -225,9 +258,12 @@ EditGroup.propTypes = {
225258
}).isRequired,
226259
}).isRequired,
227260
group: ImmutablePropTypes.map,
261+
groups: ImmutablePropTypes.map,
262+
groupsAccessor: PropTypes.func.isRequired,
228263
canViewParentDetail: PropTypes.bool.isRequired,
229264
instanceId: PropTypes.string,
230265
editGroup: PropTypes.func.isRequired,
266+
relocateGroup: PropTypes.func.isRequired,
231267
hasThreshold: PropTypes.bool,
232268
isSuperAdmin: PropTypes.bool,
233269
intl: intlShape,
@@ -248,6 +284,8 @@ export default withLinks(
248284
const userId = loggedInUserIdSelector(state);
249285
return {
250286
group: groupSelector(state, groupId),
287+
groups: notArchivedGroupsSelector(state),
288+
groupsAccessor: groupDataAccessorSelector(state),
251289
userId,
252290
isStudentOf: groupId => isSupervisorOf(userId, groupId)(state),
253291
hasThreshold: editGroupFormSelector(state, 'hasThreshold'),
@@ -280,6 +318,7 @@ export default withLinks(
280318
}
281319
return dispatch(editGroup(groupId, transformedData));
282320
},
321+
relocateGroup: ({ groupId: newParentId }) => dispatch(relocateGroup(groupId, newParentId)),
283322
})
284323
)(injectIntl(EditGroup))
285324
);

src/redux/modules/groups.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export const additionalActionTypes = {
6060
SET_ARCHIVED_PENDING: 'recodex/groups/SET_ARCHIVED_PENDING',
6161
SET_ARCHIVED_FULFILLED: 'recodex/groups/SET_ARCHIVED_FULFILLED',
6262
SET_ARCHIVED_REJECTED: 'recodex/groups/SET_ARCHIVED_REJECTED',
63+
RELOCATE: 'recodex/groups/RELOCATE',
64+
RELOCATE_PENDING: 'recodex/groups/RELOCATE_PENDING',
65+
RELOCATE_FULFILLED: 'recodex/groups/RELOCATE_FULFILLED',
66+
RELOCATE_REJECTED: 'recodex/groups/RELOCATE_REJECTED',
6367
};
6468

6569
export const loadGroup = actions.pushResource;
@@ -193,6 +197,13 @@ export const setArchived = (groupId, archived) =>
193197
meta: { groupId },
194198
});
195199

200+
export const relocateGroup = (groupId, newParentId) =>
201+
createApiAction({
202+
type: additionalActionTypes.RELOCATE,
203+
method: 'POST',
204+
endpoint: `/groups/${groupId}/relocate/${newParentId}`,
205+
});
206+
196207
/**
197208
* Reducer
198209
*/
@@ -293,6 +304,12 @@ const reducer = handleActions(
293304
[additionalActionTypes.SET_ARCHIVED_REJECTED]: (state, { payload, meta: { groupId } }) =>
294305
state.deleteIn(['resources', groupId, 'pending-archived']),
295306

307+
[additionalActionTypes.RELOCATE_FULFILLED]: (state, { payload }) =>
308+
payload.reduce(
309+
(state, data) => state.setIn(['resources', data.id], createRecord({ state: resourceStatus.FULFILLED, data })),
310+
state
311+
),
312+
296313
[additionalActionTypes.LOAD_USERS_GROUPS_FULFILLED]: (state, { payload, ...rest }) => {
297314
const groups = [...payload.supervisor, ...payload.student];
298315
return reduceActions[actionTypes.FETCH_MANY_FULFILLED](state, {

0 commit comments

Comments
 (0)