diff --git a/src/components/sites/editCustomProjectSite/editCustomProjectSite.tsx b/src/components/sites/editCustomProjectSite/editCustomProjectSite.tsx index 03a34b97a2573b0715c8bd7afcd42a445d79d8fb..e9b14f069966b0024d2b05d126c38b75a9334c02 100644 --- a/src/components/sites/editCustomProjectSite/editCustomProjectSite.tsx +++ b/src/components/sites/editCustomProjectSite/editCustomProjectSite.tsx @@ -581,7 +581,7 @@ class editCustomProjectSite extends React.Component<Props & RouteComponentProps< const hasAnyTestFailed = await this.props.runCustomProjectTestAsync(command, this.props.langId) - return hasAnyTestFailed as any //wrong infered because magic redux binding + return hasAnyTestFailed as any //wrong inferred because magic redux binding }} resetCustomTestResults={this.props.resetCustomProjectTestResults} diff --git a/src/components/sites/manageGroupExercisesSite/groupExercisesSite.tsx b/src/components/sites/manageGroupExercisesSite/groupExercisesSite.tsx index bf4d9aa4f8d2f1425a9c6f2f9343c5c576db17d0..5cc4956cd7ce74cf38a02a3e68d34373ffa0287a 100644 --- a/src/components/sites/manageGroupExercisesSite/groupExercisesSite.tsx +++ b/src/components/sites/manageGroupExercisesSite/groupExercisesSite.tsx @@ -12,13 +12,16 @@ import SinglePanelSiteWrapper from '../../singlePanelSiteWrapper' import MaterialSearchInput from '../../material/materialSearchInput' import Spinner from "../../helpers/spinner"; import { - loadManageGroupExercisesSite, + loadManageGroupExercisesSite, set_groupsOrder, setActivePaginationPage, setIsLoading, setPaginationPageSize, setSearchText, setSortByKey } from "../../../state/actions/manageGroupExercisesSite/groupExercisesSiteActions"; -import {EditableExercisePreviewFromBackend} from "../../../types/exercisePreview"; +import { + EditableExercisePreviewFromBackend, + GroupPreviewExerciseTupleFrontendOnly +} from "../../../types/exercisePreview"; import TagFilterPanel from '../../tagComponents/tagsFilterPanel' import {InputOnChangeEvent} from '../../../types/reactEvents' import { @@ -26,6 +29,16 @@ import { getGroupExercisesAsync } from '../../../state/actions/manageGroupExercisesSite/groupExercisesCrudSiteActions' import {ErrorHelper} from '../../../helpers/errorHelper' +import {Icon} from 'semantic-ui-react' +import { + frontendSettings_moveDisplaySortItemAbsolute, + frontendSettings_moveDisplaySortItemDown, + frontendSettings_moveDisplaySortItemUp, + FrontendSettingsManager +} from '../../../helpers/frontendSettingsManager' +import orderBy from 'lodash-es/orderBy' +import {SimpleVDivider} from '../../helpers/simpleVDivider' +// import {orderBy} from 'lodash' //const css = require('./styles.styl'); @@ -62,6 +75,8 @@ const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({ setActivePaginationPage, getGroupExercisesAsync, setIsLoading, + + set_groupsOrder, }, dispatch) @@ -80,7 +95,38 @@ class GroupExercisesSite extends React.Component<Props, any> { } } + + /** + * sorts the groups and groupExercises with the given sorting and applies it to the state + * @param groupIdSorting + */ + updateGroupOrder(groupIdSorting: number[]) { + + const groupList = orderBy(this.props.groups, (val, index) => { + const _index = groupIdSorting.findIndex(p => p === val.id) + return _index + }) + + const groupWithExercisesList = orderBy(this.props.groupExercises, (val, index) => { + const _index = groupIdSorting.findIndex(p => p === val.userGroup.id) + return _index + }) + + this.props.set_groupsOrder(groupList, groupWithExercisesList) + } + render(): JSX.Element { + + //foreach + //via tag search remove groups with no hits... but keep the ones where we entered search text + //if we entered search text then don't hide the group (else we would hide the searched group... and we can no longer access the search input) + let groupList = this.props.groupExercises.filter( + p => p.exercisePreviews.length > 0 || p.pagination.searchText.trim() !== '' || //when we use tags we want to hide groups without matches + (this.props.selectedFilterTagsIds.length === 0 && this.props.selectedNegativeFilterTagsIds.length === 0) //when we have no tag filters we want to display all groups + ) + + //we don't need to sort groupList because we immediately sync the sorting with the sort (which also gives us rerender) + return ( <div className="multiple-single-panels"> @@ -113,11 +159,7 @@ class GroupExercisesSite extends React.Component<Props, any> { { - //foreach - //via tag search remove groups with no hits... but keep the ones where we entered search text - //if we entered search text then don't hide the group (else we would hide the searched group... and we can no longer access the search input) - this.props.groupExercises.filter( - p => p.exercisePreviews.length > 0 || p.pagination.searchText.trim() !== '').map((groupTuple, index) => { + groupList.map((groupTuple, index) => { return ( <SinglePanelSiteWrapper key={groupTuple.userGroup.id}> <div className="view-padding"> @@ -128,6 +170,37 @@ class GroupExercisesSite extends React.Component<Props, any> { </h1> </div> <div className="view-options"> + + <div className="v-centered-in-flex"> + <div className="flexed mar-right hover-child"> + <Icon name="angle down" className="clickable" onClick={() => { + const res = frontendSettings_moveDisplaySortItemDown(FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting, groupTuple.userGroup.id) + if (!res) return + FrontendSettingsManager.set_groupExercisesGroupSorting(res) + this.updateGroupOrder(res) + }}/> + <Icon name="angle double down" className="clickable" onClick={() => { + const res = frontendSettings_moveDisplaySortItemAbsolute(FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting, groupTuple.userGroup.id, false) + if (!res) return + FrontendSettingsManager.set_groupExercisesGroupSorting(res) + this.updateGroupOrder(res) + }}/> + <Icon name="angle up" className="clickable" onClick={() => { + const res = frontendSettings_moveDisplaySortItemUp(FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting, groupTuple.userGroup.id) + if (!res) return + FrontendSettingsManager.set_groupExercisesGroupSorting(res) + this.updateGroupOrder(res) + }}/> + <Icon name="angle double up" className="clickable" onClick={() => { + const res = frontendSettings_moveDisplaySortItemAbsolute(FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting, groupTuple.userGroup.id, true) + if (!res) return + FrontendSettingsManager.set_groupExercisesGroupSorting(res) + this.updateGroupOrder(res) + }}/> + </div> + + </div> + <MaterialSearchInput value={groupTuple.pagination.searchText} onChange={(e: InputOnChangeEvent) => { diff --git a/src/constants.ts b/src/constants.ts index ff3d5ef9e9e43b73d1978a606cd6c5c984a54366..87a9f0bfe379a9d6d7d90f818d9b988dc2b7f0d8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -13,7 +13,7 @@ import Logger from './helpers/logger' * y - breaking changes / new features * z - fixes, small changes */ -export const versionString = '2.5.13' +export const versionString = '2.5.14' export const supportMail = 'yapex@informatik.uni-halle.de' diff --git a/src/helpers/frontendSettingsManager.ts b/src/helpers/frontendSettingsManager.ts index 59f2d7274fdcda046b27c68e586e8afb6ee39953..c3f5bd4cdbbb5de9e6821207f6bc9ed27bc947e6 100644 --- a/src/helpers/frontendSettingsManager.ts +++ b/src/helpers/frontendSettingsManager.ts @@ -23,6 +23,7 @@ interface SortingSettingTuple<T> { readonly sortDirection: boolean | null } +//TODO there is currently no handling when we change this... const currentVersion = '2' /** @@ -41,15 +42,19 @@ type State = { */ version: string + //--- various sorting + + groupExercisesGroupSorting: FrontendSortingElement[] | null + //--- site tags filter displayed - allOpenExercisesSiteTagsFilterIsDisplayed: boolean, + allOpenExercisesSiteTagsFilterIsDisplayed: boolean - allClosedExercisesSiteTagsFilterIsDisplayed: boolean, + allClosedExercisesSiteTagsFilterIsDisplayed: boolean - ownExercisesSiteTagsFilterIsDisplayed: boolean, + ownExercisesSiteTagsFilterIsDisplayed: boolean - groupExercisesSiteTagsFilterIsDisplayed: boolean, + groupExercisesSiteTagsFilterIsDisplayed: boolean //--- store sortings @@ -141,6 +146,7 @@ const initial: State = { version: currentVersion, submissionsSiteSorting: null, + groupExercisesGroupSorting: null, allOpenExercisesSiteTagsFilterIsDisplayed: false, allClosedExercisesSiteTagsFilterIsDisplayed: false, @@ -227,6 +233,12 @@ export class FrontendSettingsManager { return FrontendSettingsManager.isFrozen } + static set_groupExercisesGroupSorting(newSorting: FrontendSortingElement[] | null) { + if (FrontendSettingsManager.getIsFrozen()) return + frontendSettings.groupExercisesGroupSorting = newSorting + forceSave() //no throttling needed + } + //--- page size static set_manageActivatedUsersPageSize(pageSize: number): void { @@ -513,7 +525,7 @@ export class FrontendSettingsManager { for (const key in frontendSettings) { if (maybeSettings.hasOwnProperty(key)) { - //take value + //take old value frontendSettings[key] = maybeSettings[key] } else { @@ -602,3 +614,141 @@ function byteLength(str: string): number { } return s; } + + +/** + * structure to store the display index for other objects (identified by id) + * use the functions {@link frontendSettings_moveDisplaySortItemUp}, {@link frontendSettings_moveDisplaySortItemDown}, {@link frontendSettings_moveDisplaySortItemAbsolute} to change the properties in + * {@link FrontendSettingsManager} + */ +type FrontendSortingElement = number + +//--- use these functions to + +/** + * moves the given id in the {@param sortArray} up if possible + * @param sortArray + * @param id + * @returns null if element with id was not found or out or range, else: new array with the result + */ +export function frontendSettings_moveDisplaySortItemUp(sortArray: FrontendSortingElement[] | null, id: number) : FrontendSortingElement[] | null { + + if (!sortArray) return null + + const index = sortArray.findIndex(p => p === id) + if (index === -1 || index === 0) return null + + const newIndex = index-1 + + let resultArray = Array.from(sortArray) + + const temp = resultArray[index] + resultArray[index] = resultArray[newIndex] + resultArray[newIndex] = temp + + return resultArray +} + +/** + * moves the given id in the {@param sortArray} down if possible + * @param sortArray + * @param id + * @returns null if element with id was not found or out or range, else: new array with the result + */ +export function frontendSettings_moveDisplaySortItemDown(sortArray: FrontendSortingElement[] | null, id: number) : FrontendSortingElement[] | null { + + if (!sortArray) return null + + const index = sortArray.findIndex(p => p === id) + if (index === -1 || index === sortArray.length-1) return null + + const newIndex = index+1 + + let resultArray = Array.from(sortArray) + + const temp = resultArray[index] + resultArray[index] = resultArray[newIndex] + resultArray[newIndex] = temp + + return resultArray +} + +/** + * moves the given id in the {@param sortArray} absolute up/down if possible + * @param sortArray + * @param id + * @param moveToFront true: absolute top (move to front), false: absolute bot (move to back) + * @returns null if element with id was not found or out or range, else: new array with the result + */ +export function frontendSettings_moveDisplaySortItemAbsolute(sortArray: FrontendSortingElement[] | null, id: number, moveToFront: boolean): FrontendSortingElement[] | null { + + if (!sortArray) return null + + const index = sortArray.findIndex(p => p === id) + if (index === -1) return null + + let resultArray = Array.from(sortArray) + + //move to back + const temp = resultArray[index] + resultArray.splice(index, 1) + + if (moveToFront) { + resultArray.unshift(temp) + } else { + resultArray.push(temp) + } + + return resultArray +} + +type SyncResult = { + result: FrontendSortingElement[] + /** + * something was changed, need to be saved into settings + */ + anyChanges: boolean +} +/** + * adds the new {@param ids} to {@param sortArray} and removes unused ids from {@param sortArray} + * @param sortArray + * @param ids this is the ground truth from the backend + */ +export function frontendSettings_syncDisplaySortingItems(sortArray: FrontendSortingElement[] | null, ids: ReadonlyArray<number>): SyncResult { + + let changed = false + let resultArray = !sortArray ? [] : Array.from(sortArray) + + //use a try in case we change the frontend settings and this gets another type... + try { + //add all missing ids to the back of our old sortObj + for(let i = 0; i < ids.length;i++) { + const id = ids[i] + if (resultArray.findIndex(p => p === id) === -1) { + //new id is not found in sortObj + resultArray.push(ids[i]) + changed = true + } + } + + //then remove all old entries in sortArray that are not longer found in ids + for(let i = 0; i < resultArray.length;i++) { + const id = resultArray[i] + if (ids.findIndex(p => p === id) === -1) { + resultArray.splice(i,1) + changed = true + i-- + } + } + } catch(err) { + resultArray = [] + changed = true + } + + + return { + result: resultArray, + anyChanges: changed + } + +} diff --git a/src/state/actions/manageGroupExercisesSite/groupExercisesSiteActions.ts b/src/state/actions/manageGroupExercisesSite/groupExercisesSiteActions.ts index ee6dd390fd8b3cd2e0e6a563811a02e20500e50d..99bff05c82d560ab2266c7b0aed2acd5e8504b9f 100644 --- a/src/state/actions/manageGroupExercisesSite/groupExercisesSiteActions.ts +++ b/src/state/actions/manageGroupExercisesSite/groupExercisesSiteActions.ts @@ -5,9 +5,12 @@ import { ResetAction, SET_isLoadingManageGroupExercisesSiteACtion, SET_isTagsFilterDisplayedAction, SET_sortByKeyAction, - SetActivePaginationPageAction, SetPaginationPageSizeAction, SetSearchTextAction + SetActivePaginationPageAction, SetPaginationPageSizeAction, SetSearchTextAction, SET_groupsOrderAction } from "../../reducers/manageGroupExercisesSite/groupExercisesSiteReducer"; -import {EditableExercisePreviewFromBackend} from "../../../types/exercisePreview"; +import { + EditableExercisePreviewFromBackend, + GroupPreviewExerciseTupleFrontendOnly +} from "../../../types/exercisePreview"; import {ActionType} from "../../reducers/manageGroupExercisesSite/groupExercisesSiteActionTypes"; import {AwaitActions, MultiActions} from "../types"; import {loadManageTagsSite} from "../manageTagsSite/manageTagsActions"; @@ -15,6 +18,7 @@ import {resetTagsFilter} from "../tagsFilterActions"; import {getGroupExercisesAsync, getGroupsForExercisesAsync} from './groupExercisesCrudSiteActions' import debounce from 'lodash-es/debounce' import {searchInputDebounceInMs} from '../../../constants' +import {UserGroupFromBackend} from '../../../types/group' export function setIsLoading(isLoading: boolean): SET_isLoadingManageGroupExercisesSiteACtion { @@ -150,4 +154,12 @@ export function setSearchText(searchText: string, groupId: number): MultiActions debouncedRefresh(dispatch, getState, groupId) } -} \ No newline at end of file +} + +export function set_groupsOrder(groups: ReadonlyArray<UserGroupFromBackend>, groupExercises: ReadonlyArray<GroupPreviewExerciseTupleFrontendOnly>): SET_groupsOrderAction { + return { + type: ActionType.SET_groupsOrder, + groups, + groupExercises + } +} diff --git a/src/state/reducers/manageGroupExercisesSite/getGroupExercisesReducer.ts b/src/state/reducers/manageGroupExercisesSite/getGroupExercisesReducer.ts index 030425042565dc07f1ed2b775d8d63a3be3acd68..117028601e810a7f873dc6bd5f61f4ae213e82a3 100644 --- a/src/state/reducers/manageGroupExercisesSite/getGroupExercisesReducer.ts +++ b/src/state/reducers/manageGroupExercisesSite/getGroupExercisesReducer.ts @@ -13,6 +13,7 @@ import {EditableExercisePreviewFromBackend} from "../../../types/exercisePreview import {initial, State} from "./groupExercisesSiteReducer"; import {PaginatedData} from '../../../types/pagination' import {PaginationHelper} from '../../../helpers/paginationHelper' +import {FrontendSettingsManager, frontendSettings_syncDisplaySortingItems} from '../../../helpers/frontendSettingsManager' interface Meta { readonly groupId: number diff --git a/src/state/reducers/manageGroupExercisesSite/getGroupsForExercisesCrudReducer.ts b/src/state/reducers/manageGroupExercisesSite/getGroupsForExercisesCrudReducer.ts index a574b00a63e23aa5182aa9c736414a2ec312e230..5f9e2cdefc9449167c07eb09cb7ecfaefb7d3055 100644 --- a/src/state/reducers/manageGroupExercisesSite/getGroupsForExercisesCrudReducer.ts +++ b/src/state/reducers/manageGroupExercisesSite/getGroupsForExercisesCrudReducer.ts @@ -5,7 +5,8 @@ import {initial, State} from './groupExercisesSiteReducer' import {UserGroupBase, UserGroupFromBackend} from '../../../types/group' import {GroupPreviewExerciseTupleFrontendOnly} from '../../../types/exercisePreview' import {getInitialPaginationData} from '../../../constants' -import {FrontendSettingsManager} from '../../../helpers/frontendSettingsManager' +import {FrontendSettingsManager, frontendSettings_syncDisplaySortingItems} from '../../../helpers/frontendSettingsManager' +import orderBy from 'lodash-es/orderBy' export interface GET_groupsForExercisesAction @@ -54,12 +55,31 @@ export function reducer(state: State = initial, action: AllActions): State { const pageSizeSetting = FrontendSettingsManager.getFrontendSettings().groupExercisesPageSize + //ensure our frontend group sorting is up-to-date + + + const synResult = frontendSettings_syncDisplaySortingItems(FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting, action.payload.map(p => p.id)) + + if (synResult.anyChanges) { + FrontendSettingsManager.set_groupExercisesGroupSorting(synResult.result) + } + + const frontendSorting = FrontendSettingsManager.getFrontendSettings().groupExercisesGroupSorting + + let groupList = action.payload + + if (frontendSorting) { + groupList = orderBy(action.payload, (val, index) => { + const _index = frontendSorting.findIndex(p => p === val.id) + return _index + }) + } return { ...state, isLoading: false, - groups: action.payload, - groupExercises: action.payload.map<GroupPreviewExerciseTupleFrontendOnly>(p => { + groups: groupList, + groupExercises: groupList.map<GroupPreviewExerciseTupleFrontendOnly>(p => { // noinspection TsLint let pageSize = pageSizeSetting[p.id] diff --git a/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteActionTypes.ts b/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteActionTypes.ts index 6e9ecd1790fec25b536d9018fef2e06171bff574..ab71e457db77cdecab0baf898bd7cc32a07557ee 100644 --- a/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteActionTypes.ts +++ b/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteActionTypes.ts @@ -38,6 +38,8 @@ export enum ActionType { SET_isTagsFilterDisplayed = 'groupExercisesReducer_SET_isTagsFilterDisplayed', + SET_groupsOrder = 'groupExercisesReducer_SET_groupsOrder', + RESET = 'groupExercisesReducer_RESET', } diff --git a/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteReducer.ts b/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteReducer.ts index 32f9938e5ad316b4e9ef310c5b8b2b1bb315d91b..905240ce041cbcd1d2431c2028f6629892c36f36 100644 --- a/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteReducer.ts +++ b/src/state/reducers/manageGroupExercisesSite/groupExercisesSiteReducer.ts @@ -80,6 +80,15 @@ export interface SET_isLoadingManageGroupExercisesSiteACtion extends ActionBase readonly isLoading: boolean } +/** + * when we change the group display order we set the new order to the state and we render the correct order + */ +export interface SET_groupsOrderAction extends ActionBase { + readonly type: ActionType.SET_groupsOrder + readonly groups: ReadonlyArray<UserGroupFromBackend> + readonly groupExercises: ReadonlyArray<GroupPreviewExerciseTupleFrontendOnly> +} + export interface ResetAction extends ActionBase { readonly type: ActionType.RESET } @@ -98,6 +107,8 @@ export type AllActions = | DeleteGroupExerciseActions | GetGroupsForExercisesActions + | SET_groupsOrderAction + | RESET_GlobalAction export function reducer(state: State = initial, action: AllActions): State { @@ -112,6 +123,14 @@ export function reducer(state: State = initial, action: AllActions): State { isLoading: action.isLoading } + case ActionType.SET_groupsOrder: + + return { + ...state, + groups: action.groups, + groupExercises: action.groupExercises, + } + case ActionType.SET_isTagsFilterDisplayed: FrontendSettingsManager.setGroupExercisesSiteTagsFilterIsDisplayed(action.isTagsFilterDisplayed) diff --git a/src/styles/common.styl b/src/styles/common.styl index 40effa713d1cdad64f62a59e24d97d5c4fecbec3..b2701b218cd8cafeb9ea8a19037f2ed8d65217da 100644 --- a/src/styles/common.styl +++ b/src/styles/common.styl @@ -293,6 +293,10 @@ html, body { } } +.v-centered-in-flex { + align-self center +} + .div-disabled { opacity 0.2 !important pointer-events none