Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Servers/UI/OJS.Servers.Ui/ClientApp/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ interface IContestsSortAndFilterOptions {
page: number;
itemsPerPage?: number;
category?: number | null;
includeHidden?: boolean;
}

// TODO: Unify these types, some are called params, others options
Expand Down Expand Up @@ -268,6 +269,8 @@ interface IIndexContestsType {
officialParticipants: number;
requirePasswordForCompete: boolean;
requirePasswordForPractice: boolean;
isVisible: boolean;
visibleFrom: Date | null;
}

interface IIndexProblemsType {
Expand Down Expand Up @@ -893,6 +896,8 @@ interface IProfilePageContests {
officialParticipants: number;
requirePasswordForCompete: boolean;
requirePasswordForPractice: boolean;
isVisible: boolean;
visibleFrom: Date | null;
}

interface IExcelFilter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
margin: spacings.$sp-8 0;
padding: spacings.$sp-16 spacings.$sp-4 spacings.$sp-16 spacings.$sp-18;

&.nonVisible {
opacity: 0.7;
}

.contestDetailsFragmentsWrapper {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -53,6 +57,10 @@
color: colors.$secondary-green;
}

.redColor {
color: colors.$primary-red;
}

.hasUnderLine {
cursor: pointer;
text-decoration: underline;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import React from 'react';
import { concatClassnames } from 'react-alice-carousel/lib/utils';
import { IconType } from 'react-icons';
import { BiTransfer } from 'react-icons/bi';
import { FaCalendarAlt, FaClock, FaFile, FaUser } from 'react-icons/fa';
import { FaCalendarAlt, FaClock, FaEye, FaEyeSlash, FaFile, FaUser } from 'react-icons/fa';
import { IoIosLock } from 'react-icons/io';
import { Link } from 'react-router-dom';
import EditIcon from '@mui/icons-material/Edit';
import { Tooltip } from '@mui/material';
import isNil from 'lodash/isNil';

enum ContestDetailColor {
Default = 'default',
Green = 'green',
Red = 'red',
}

import { ContestParticipationType } from '../../../common/constants';
import { getCompeteResultsAreVisibleInContestCards, getPracticeResultsAreVisibleInContestCards } from '../../../common/contest-helpers';
import { IIndexContestsType } from '../../../common/types';
Expand Down Expand Up @@ -88,7 +94,7 @@ const ContestCard = (props: IContestCardProps) => {
Icon: IconType,
text: string | number | undefined,
tooltipTitle?: string,
isGreenColor?: boolean,
color?: ContestDetailColor,
hasUnderLine?: boolean,
participationType?: string,
) => {
Expand All @@ -110,11 +116,20 @@ const ContestCard = (props: IContestCardProps) => {
</>
;

const getColorClass = () => {
switch (color) {
case ContestDetailColor.Green:
return styles.greenColor;
case ContestDetailColor.Red:
return styles.redColor;
default:
return '';
}
};

const content = participationType
? <Link
className={`${styles.contestDetailsFragment} ${isGreenColor
? styles.greenColor
: ''}`}
className={`${styles.contestDetailsFragment} ${getColorClass()}`}
to={getContestsResultsPageUrl({
contestName: name,
contestId: id,
Expand All @@ -128,9 +143,7 @@ const ContestCard = (props: IContestCardProps) => {
{renderBody()}
</Link>

: <div className={`${styles.contestDetailsFragment} ${isGreenColor
? styles.greenColor
: ''}`}
: <div className={`${styles.contestDetailsFragment} ${getColorClass()}`}
>
{renderBody()}
</div>
Expand Down Expand Up @@ -191,7 +204,7 @@ const ContestCard = (props: IContestCardProps) => {
};

return (
<div className={`${backgroundColorClassName} ${textColorClass} ${styles.contestCardWrapper}`}>
<div className={`${backgroundColorClassName} ${textColorClass} ${styles.contestCardWrapper} ${!contest.isVisible && styles.nonVisible}`}>
<div>
<div className={styles.actionsWrapper}>
<Link
Expand Down Expand Up @@ -237,7 +250,7 @@ const ContestCard = (props: IContestCardProps) => {
FaUser,
`Practice results: ${practiceResults}`,
undefined,
false,
ContestDetailColor.Default,
true,
ContestParticipationType.Practice,
)
Expand All @@ -248,7 +261,7 @@ const ContestCard = (props: IContestCardProps) => {
FaUser,
`Compete results: ${competeResults}`,
undefined,
true,
ContestDetailColor.Green,
true,
ContestParticipationType.Compete,
)
Expand All @@ -261,7 +274,23 @@ const ContestCard = (props: IContestCardProps) => {
FaClock,
`Remaining time: ${remainingTimeFormatted}`,
'Remaining time',
ContestDetailColor.Default,
false,
)}
{!contest.isVisible &&
renderContestDetailsFragment(
FaEyeSlash,
'Hidden',
'Non visible for users',
ContestDetailColor.Red,
false,
)}
{!contest.isVisible && contest.visibleFrom &&
renderContestDetailsFragment(
FaEye,
`${preciseFormatDate(contest.visibleFrom, dateTimeFormatWithSpacing)}`,
`Visible from: ${preciseFormatDate(contest.visibleFrom, dateTimeFormatWithSpacing)}`,
ContestDetailColor.Default,
false,
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
margin-left: 50px;
}

.showHiddenButton {
float: right;
}

.headingActions {
align-items: center;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FaEye, FaEyeSlash } from 'react-icons/fa';
import { useParams } from 'react-router-dom';
import { CATEGORY_ID_PARAM, CONTEST_CATEGORIES_HIERARCHY_PATH, CONTESTS_PATH, OPEN_CREATE_PARAM } from 'src/common/urls/administration-urls';
import {
CATEGORY_ID_PARAM,
CONTEST_CATEGORIES_HIERARCHY_PATH,
CONTESTS_PATH,
OPEN_CREATE_PARAM,
} from 'src/common/urls/administration-urls';
import AdministrationLink from 'src/components/guidelines/buttons/AdministrationLink';
import Button, { ButtonSize } from 'src/components/guidelines/buttons/Button';
import { CONTESTS_BULK_EDIT } from 'src/utils/constants';

import { SortType } from '../../common/contest-types';
Expand All @@ -28,7 +35,9 @@ import { flexCenterObjectStyles } from '../../utils/object-utils';
import styles from './ContestsPage.module.scss';

const ContestsPage = () => {
const [ includeHidden, setIncludeHidden ] = useState(false);
const dispatch = useAppDispatch();
const { internalUser: user } = useAppSelector((state) => state.authorization);
const { categoryId } = useParams();
const { themeColors, getColorClassName } = useTheme();
const {
Expand Down Expand Up @@ -60,12 +69,16 @@ const ContestsPage = () => {
params.category = selectedCategory.id;
}

if (includeHidden) {
params.includeHidden = includeHidden;
}

if (selectedStrategy) {
params.strategy = selectedStrategy.id;
}

return params;
}, [ selectedCategory, selectedStrategy, selectedPage ]);
}, [ selectedCategory, selectedStrategy, selectedPage, includeHidden ]);

const {
data: allContests,
Expand Down Expand Up @@ -94,7 +107,7 @@ const ContestsPage = () => {
}, [ allContests, dispatch ]);

const renderContest = useCallback(
(contest: IIndexContestsType) =>
(contest: IIndexContestsType) =>
<ContestCard contest={contest} />
, [],
);
Expand Down Expand Up @@ -168,7 +181,7 @@ const ContestsPage = () => {
: 'All Categories'}
</div>
<div className={styles.headingActions}>
{selectedCategory?.id && selectedCategory?.children.length === 0 &&
{selectedCategory?.id && selectedCategory?.children.length === 0 &&
<>
<AdministrationLink
text="Edit Contests"
Expand All @@ -187,16 +200,34 @@ const ContestsPage = () => {
</div>
</div>
<div className={styles.contestsListContainer}>
<PaginationControls
isDataFetching={areContestsFetching}
count={contests?.pagesCount || 0}
page={selectedPage}
onChange={(page:number) => {
searchParams.set('page', page.toString());
setSearchParams(searchParams);
}}
className={styles.paginationControlsUpper}
/>
<div>
{user.canAccessAdministration &&
<Button
className={styles.showHiddenButton}
size={ButtonSize.small}
onClick={() => {
setIncludeHidden(!includeHidden);
if (selectedPage > 1) {
searchParams.set('page', '1');
setSearchParams(searchParams);
}
}}>
{includeHidden
? <FaEyeSlash title='Hide hidden' />
: <FaEye title='Show hidden' />}
</Button>
}
<PaginationControls
isDataFetching={areContestsFetching}
count={contests?.pagesCount || 0}
page={selectedPage}
onChange={(page:number) => {
searchParams.set('page', page.toString());
setSearchParams(searchParams);
}}
className={styles.paginationControlsUpper}
/>
</div>
{renderContests()}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
IRegisterUserForContestParams,
} from '../../common/url-types';


export const contestsService = createApi({
reducerPath: 'contestService',
baseQuery: fetchBaseQuery({
Expand Down Expand Up @@ -58,13 +57,14 @@ export const contestsService = createApi({
// baseQuery: getCustomBaseQuery('contests'),
endpoints: (builder) => ({
getAllContests: builder.query<IPagedResultType<IIndexContestsType>, IContestsSortAndFilterOptions>({
query: ({ sortType, page, category, strategy }) => ({
query: ({ sortType, page, category, strategy, includeHidden }) => ({
url: '/Contests/GetAll',
params: {
sortType,
page,
category,
strategy,
includeHidden,
},
}),
}),
Expand Down Expand Up @@ -178,7 +178,6 @@ export const contestsService = createApi({
}),
});


export const {
useGetAllContestsQuery,
useGetContestCategoriesQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class ContestFiltersRequestModel : IMapExplicitly

public string? SortTypeDirection { get; set; } = null!;

public bool IncludeHidden { get; set; }

public void RegisterMappings(IProfileExpression configuration)
=> configuration
.CreateMap<ContestFiltersRequestModel, ContestFiltersServiceModel>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ public class ContestForListingResponseModel : IMapFrom<ContestForListingServiceM
public bool RequirePasswordForCompete { get; set; }

public bool RequirePasswordForPractice { get; set; }

public bool IsVisible { get; set; }

public DateTime? VisibleFrom { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,13 @@ public async Task<PagedResult<ContestForListingServiceModel>> GetAllByFiltersAnd
ContestFiltersServiceModel? model)
{
model = await this.GetNestedFilterCategoriesIfAny(model);
var user = userProviderService.GetCurrentUser();
var includeHidden = model.IncludeHidden && user.IsAdmin;

var pagedContests =
await contestsData.GetAllAsPageByFiltersAndSorting<ContestForListingServiceModel>(model);
await contestsData.GetAllAsPageByFiltersAndSorting<ContestForListingServiceModel>(model, includeHidden);

var participantResultsByContest = new Dictionary<int, List<ParticipantResultServiceModel>>();
var user = userProviderService.GetCurrentUser();
if (user.IsAuthenticated)
{
participantResultsByContest = await this.GetUserParticipantResultsForContestInPage([.. pagedContests
Expand Down
2 changes: 1 addition & 1 deletion Services/UI/OJS.Services.Ui.Data/IContestsDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Task<PagedResult<TServiceModel>> ApplyFiltersSortAndPagination<TServiceModel>(

Task<IEnumerable<TServiceModel>> GetAllExpired<TServiceModel>();

Task<PagedResult<TServiceModel>> GetAllAsPageByFiltersAndSorting<TServiceModel>(ContestFiltersServiceModel model);
Task<PagedResult<TServiceModel>> GetAllAsPageByFiltersAndSorting<TServiceModel>(ContestFiltersServiceModel model, bool includeHidden = false);

IQueryable<Contest> GetLatestForParticipantByUsername(string username);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ public IQueryable<Contest> GetAllVisible()
=> this.GetQuery(c => c.IsVisible || c.VisibleFrom <= this.dates.GetUtcNow());

public async Task<PagedResult<TServiceModel>> GetAllAsPageByFiltersAndSorting<TServiceModel>(
ContestFiltersServiceModel model)
ContestFiltersServiceModel model,
bool includeHidden = false)
{
var contests = model.CategoryIds.Any()
? this.GetAllVisibleByCategories(model.CategoryIds)
: this.GetAllVisible();
var contests = includeHidden
? this.GetQuery()
: this.GetAllVisible();

if (model.CategoryIds.Any())
{
contests = contests
.Where(c => c.CategoryId.HasValue && model.CategoryIds.Contains(c.CategoryId.Value));
}

return await this.ApplyFiltersSortAndPagination<TServiceModel>(contests, model);
}
Expand Down Expand Up @@ -176,10 +183,6 @@ private async Task<int> GetMaxPointsByIdAndProblemGroupsFilter(int id, Expressio
return maxPoints;
}

private IQueryable<Contest> GetAllVisibleByCategories(IEnumerable<int> categoryIds)
=> this.GetAllVisible()
.Where(c => c.CategoryId.HasValue && categoryIds.Contains(c.CategoryId.Value));

private IQueryable<Contest> GetAllCompetableQuery()
=> this.GetAllVisible()
.Where(this.CanBeCompeted());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public class ContestFiltersServiceModel
public ContestSortType? SortType { get; set; }

public ContestSortTypeDirection SortTypeDirection { get; set; }

public bool IncludeHidden { get; set; }
}
Loading