From 27cf7f73c6ad4ae370f97a03dab6f75c9ac5d5b9 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Wed, 24 Jan 2018 19:15:29 +0100 Subject: [PATCH] Group tree view updated to display only groups that can be directly visited by the user (or have children that can be visited). Additionally, groups are now sorted by name. Minor tweaks in appearance. --- src/components/Groups/GroupTree/GroupTree.js | 71 ++++++++++++------- .../forms/EditGroupForm/EditGroupForm.js | 2 +- .../LocalizedNames/LocalizedExerciseName.js | 2 +- .../LocalizedNames/LocalizedGroupName.js | 2 +- src/components/helpers/group.js | 15 ++++ .../widgets/TreeView/TreeViewLeaf.js | 24 +++++++ src/helpers/getLocalizedData.js | 12 ++++ src/locales/cs.json | 5 +- src/locales/en.json | 3 +- 9 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 src/components/helpers/group.js diff --git a/src/components/Groups/GroupTree/GroupTree.js b/src/components/Groups/GroupTree/GroupTree.js index 82cbef8f5..a9f48bafa 100644 --- a/src/components/Groups/GroupTree/GroupTree.js +++ b/src/components/Groups/GroupTree/GroupTree.js @@ -1,12 +1,15 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { LinkContainer } from 'react-router-bootstrap'; import Icon from 'react-fontawesome'; + import Button from '../../widgets/FlatButton'; -import { LinkContainer } from 'react-router-bootstrap'; import { TreeView, TreeViewItem } from '../../widgets/TreeView'; import { isReady, getJsData } from '../../../redux/helpers/resourceManager'; import GroupsName from '../GroupsName'; +import { computeVisibleGroupsMap } from '../../helpers/group.js'; +import { getLocalizedResourceName } from '../../../helpers/getLocalizedData'; import withLinks from '../../../hoc/withLinks'; @@ -42,6 +45,33 @@ class GroupTree extends Component { ); }; + renderChildGroups = ( + { all: allChildGroups, public: publicChildGroups }, + visibleGroupsMap + ) => { + const { level = 0, isOpen = false, groups, intl: { locale } } = this.props; + return allChildGroups + .filter(id => visibleGroupsMap[id]) + .sort((id1, id2) => { + const name1 = getLocalizedResourceName(groups.get(id1), locale); + const name2 = getLocalizedResourceName(groups.get(id2), locale); + return name1 !== undefined && name2 !== undefined + ? name1.localeCompare(name2, locale) + : 0; + }) + .map(id => + = 0} + visibleGroupsMap={visibleGroupsMap} + /> + ); + }; + render() { const { id, @@ -50,6 +80,7 @@ class GroupTree extends Component { isPublic = true, groups, currentGroupId = null, + visibleGroupsMap = null, links: { GROUP_URI_FACTORY } } = this.props; @@ -62,10 +93,15 @@ class GroupTree extends Component { name, localizedTexts, canView, - childGroups: { all: allChildGroups, public: publicChildGroups }, + childGroups, primaryAdminsIds } = getJsData(group); + const actualVisibleGroupsMap = + visibleGroupsMap !== null + ? visibleGroupsMap + : computeVisibleGroupsMap(groups); + return ( {level !== 0 && @@ -78,8 +114,10 @@ class GroupTree extends Component { noLink /> } + id={id} level={level} admins={primaryAdminsIds} + isPublic={isPublic} isOpen={currentGroupId === id || isOpen} actions={ currentGroupId !== id && canView @@ -87,27 +125,10 @@ class GroupTree extends Component { : undefined } > - {allChildGroups.map(id => - = 0} - /> - )} + {this.renderChildGroups(childGroups, actualVisibleGroupsMap)} } {level === 0 && - allChildGroups.map(id => - = 0} - /> - )} + this.renderChildGroups(childGroups, actualVisibleGroupsMap)} ); } @@ -120,7 +141,9 @@ GroupTree.propTypes = { isOpen: PropTypes.bool, isPublic: PropTypes.bool, currentGroupId: PropTypes.string, - links: PropTypes.object + visibleGroupsMap: PropTypes.object, + links: PropTypes.object, + intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; -export default withLinks(GroupTree); +export default withLinks(injectIntl(GroupTree)); diff --git a/src/components/forms/EditGroupForm/EditGroupForm.js b/src/components/forms/EditGroupForm/EditGroupForm.js index a1a394d80..7e502a79f 100644 --- a/src/components/forms/EditGroupForm/EditGroupForm.js +++ b/src/components/forms/EditGroupForm/EditGroupForm.js @@ -111,7 +111,7 @@ const EditGroupForm = ({ label={ } required diff --git a/src/components/helpers/LocalizedNames/LocalizedExerciseName.js b/src/components/helpers/LocalizedNames/LocalizedExerciseName.js index 89ba8e17b..7b870434a 100644 --- a/src/components/helpers/LocalizedNames/LocalizedExerciseName.js +++ b/src/components/helpers/LocalizedNames/LocalizedExerciseName.js @@ -28,7 +28,7 @@ const LocalizedExerciseName = ({ entity, intl: { locale } }) => { } > - +   } diff --git a/src/components/helpers/LocalizedNames/LocalizedGroupName.js b/src/components/helpers/LocalizedNames/LocalizedGroupName.js index df2bcb925..5135a8efb 100644 --- a/src/components/helpers/LocalizedNames/LocalizedGroupName.js +++ b/src/components/helpers/LocalizedNames/LocalizedGroupName.js @@ -28,7 +28,7 @@ const LocalizedGroupName = ({ entity, intl: { locale } }) => { } > - +   } diff --git a/src/components/helpers/group.js b/src/components/helpers/group.js new file mode 100644 index 000000000..d94733b06 --- /dev/null +++ b/src/components/helpers/group.js @@ -0,0 +1,15 @@ +import { defaultMemoize } from 'reselect'; + +export const computeVisibleGroupsMap = defaultMemoize(groups => { + const res = {}; + groups + .filter(group => group.getIn(['data', 'canView'])) + .forEach((group, id) => { + res[id] = true; + group.getIn(['data', 'parentGroupsIds'], []).forEach(parentId => { + res[parentId] = true; + }); + }); + + return res; +}); diff --git a/src/components/widgets/TreeView/TreeViewLeaf.js b/src/components/widgets/TreeView/TreeViewLeaf.js index 6d2a64801..1e35b3643 100644 --- a/src/components/widgets/TreeView/TreeViewLeaf.js +++ b/src/components/widgets/TreeView/TreeViewLeaf.js @@ -1,16 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import Icon from 'react-fontawesome'; + import { LoadingIcon } from '../../icons'; import LevelGap from './LevelGap'; import GroupsName from '../../../components/Groups/GroupsName'; import UsersNameContainer from '../../../containers/UsersNameContainer'; const TreeViewLeaf = ({ + id, loading = false, title, admins, + isPublic, icon = 'square-o', onClick, level, @@ -39,16 +43,36 @@ const TreeViewLeaf = ({ ) } + {isPublic && + + + + } + > + + } {actions} ; TreeViewLeaf.propTypes = { + id: PropTypes.string, loading: PropTypes.bool, title: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage, GroupsName]) }) ]).isRequired, admins: PropTypes.array, + isPublic: PropTypes.bool, icon: PropTypes.string, onClick: PropTypes.func, level: PropTypes.number.isRequired, diff --git a/src/helpers/getLocalizedData.js b/src/helpers/getLocalizedData.js index 143017807..412791ebc 100644 --- a/src/helpers/getLocalizedData.js +++ b/src/helpers/getLocalizedData.js @@ -5,9 +5,21 @@ const getLocalizedX = field => (entity, locale) => { return localizedText ? localizedText[field] : entity[field]; }; +const getLocalizedResourceX = field => (resource, locale) => { + const localizedTexts = resource && resource.getIn(['data', 'localizedTexts']); + const localizedText = + localizedTexts && + localizedTexts.find(text => text.get('locale') === locale); + return localizedText + ? localizedText.get(field) + : resource ? resource.getIn(['data', field]) : undefined; +}; + export const getLocalizedName = getLocalizedX('name'); export const getLocalizedDescription = getLocalizedX('description'); +export const getLocalizedResourceName = getLocalizedResourceX('name'); + export const getOtherLocalizedNames = (entity, locale) => { const name = getLocalizedName(entity, locale); return entity.localizedTexts diff --git a/src/locales/cs.json b/src/locales/cs.json index 077b5cd92..d91ca9912 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -141,8 +141,8 @@ "app.confirm.no": "Ne", "app.confirm.yes": "Ano", "app.createGroup.externalId": "Externí identifikátor skupiny (například ID ze školního informačního systému):", - "app.createGroup.hasThreshold": "Students require cetrain number of points to complete the course", - "app.createGroup.isPublic": "Studenti se mohou sami přidávat k této skupině", + "app.createGroup.hasThreshold": "Studenti potřebují určitý počet bodů pro splnění kurzu", + "app.createGroup.isPublic": "Veřejná (skupinu vidí všichni uživatelé a můžou se do ní přidat)", "app.createGroup.publicStats": "Studenti mohou vidět dosažené body ostatních", "app.createGroup.threshold": "Minimální procentuální hranice potřebná ke splnění tohoto kurzu:", "app.createGroup.validation.thresholdBetweenZeroHundred": "Procentuální hranice musí být celé číslo od 0 do 100.", @@ -685,6 +685,7 @@ "app.groupResultsTableRow.noResults": "Nyní zde nejsou žádné výsledky k zobrazení.", "app.groupTree.detailButton": "Zobrazit stránku skupiny", "app.groupTree.loading": "Načítání...", + "app.groupTree.treeViewLeaf.publicTooltip": "The group is public", "app.groups.joinGroupButton": "Stát se členem", "app.groups.leaveGroupButton": "Opustit skupinu", "app.groups.makeGroupAdminButton": "Změnit na správce skupiny", diff --git a/src/locales/en.json b/src/locales/en.json index 8f0d8b39a..64e8e3082 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -142,7 +142,7 @@ "app.confirm.yes": "Yes", "app.createGroup.externalId": "External ID of the group (e. g. ID of the group in the school IS):", "app.createGroup.hasThreshold": "Students require cetrain number of points to complete the course", - "app.createGroup.isPublic": "Students can join the group themselves", + "app.createGroup.isPublic": "Public (everyone can see and join this group)", "app.createGroup.publicStats": "Students can see statistics of each other", "app.createGroup.threshold": "Minimum percent of the total points count needed to complete the course:", "app.createGroup.validation.thresholdBetweenZeroHundred": "Threshold must be an integer in between 0 and 100.", @@ -685,6 +685,7 @@ "app.groupResultsTableRow.noResults": "There are currently no results available.", "app.groupTree.detailButton": "See group's page", "app.groupTree.loading": "Loading ...", + "app.groupTree.treeViewLeaf.publicTooltip": "The group is public", "app.groups.joinGroupButton": "Join group", "app.groups.leaveGroupButton": "Leave group", "app.groups.makeGroupAdminButton": "Make group admin",