Skip to content

Commit a915048

Browse files
committed
feat: allow sorting participants
1 parent 430a52c commit a915048

File tree

5 files changed

+60
-15
lines changed

5 files changed

+60
-15
lines changed

apps/backend/src/research/participants/participant.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export class ParticipantResponseDto extends ParticipantBaseDto {
2323

2424
export class ParticipantCreationDto extends ParticipantBaseDto {}
2525
export class ParticipantMutationDto extends PartialType(ParticipantBaseDto) {}
26+
27+
export enum ParticipantSortableField {
28+
id = "id",
29+
birthday = "birthday",
30+
}

apps/backend/src/research/participants/participants.controller.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common";
1+
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
22
import { ParticipantsService } from "./participants.service";
33
import {
44
ApiBody,
55
ApiExtraModels,
66
ApiNotFoundResponse,
77
ApiOperation,
8+
ApiQuery,
89
ApiTags,
910
ApiUnprocessableEntityResponse,
1011
getSchemaPath,
1112
} from "@nestjs/swagger";
12-
import { ParticipantCreationDto, ParticipantMutationDto, ParticipantResponseDto } from "./participant.dto";
13+
import { ParticipantCreationDto, ParticipantMutationDto, ParticipantResponseDto, ParticipantSortableField } from "./participant.dto";
1314
import { ErrorResponseDto } from "../../common/dto/error.dto";
1415
import { Roles } from "../../system/users/roles.decorator";
1516
import { UserRole } from "../../system/users/user.entity";
@@ -18,6 +19,7 @@ import { OneOrMany } from "../../types";
1819
import { EntriesService } from "../entries/entries.service";
1920
import { EntryTemplateDto } from "../entries/entry.dto";
2021
import { Serialize } from "../../common/decorators/serialize";
22+
import { SortOrder } from "../../common/dto/sort.dto";
2123

2224
@ApiTags("Participants")
2325
@ApiExtraModels(ParticipantCreationDto)
@@ -59,9 +61,17 @@ export class ParticipantsController {
5961

6062
@Get()
6163
@ApiOperation({ summary: "Get all participants" })
64+
@ApiQuery({
65+
name: "sortBy",
66+
enumName: "ParticipantSortableField",
67+
enum: ParticipantSortableField,
68+
required: false,
69+
description: "Field to sort by",
70+
})
71+
@ApiQuery({ name: "sortOrder", enumName: "SortOrder", enum: SortOrder, required: false, description: "Sort order" })
6272
@Serialize(ParticipantResponseDto)
63-
index(): Promise<ParticipantResponseDto[]> {
64-
return this.participantService.findAll();
73+
index(@Query("sortBy") sortBy?: ParticipantSortableField, @Query("sortOrder") sortOrder?: SortOrder): Promise<ParticipantResponseDto[]> {
74+
return this.participantService.findAll({ sortBy, sortOrder });
6575
}
6676

6777
@Get(":id")

apps/backend/src/research/participants/participants.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { InjectRepository } from "@mikro-orm/nestjs";
22
import { Injectable, UnprocessableEntityException } from "@nestjs/common";
3-
import { EntityManager, EntityRepository, FilterQuery, UniqueConstraintViolationException } from "@mikro-orm/core";
3+
import { EntityManager, EntityRepository, FilterQuery, QueryOrderMap, UniqueConstraintViolationException } from "@mikro-orm/core";
44
import { ParticipantCreationDto, ParticipantMutationDto } from "./participant.dto";
55
import { Participant } from "./participant.entity";
66
import { OneOrMany } from "../../types";
7+
import { SortOrder } from "../../common/dto/sort.dto";
78

89
@Injectable()
910
export class ParticipantsService {
@@ -34,8 +35,10 @@ export class ParticipantsService {
3435
return participants.map((p) => p.toObject());
3536
}
3637

37-
async findAll() {
38-
return (await this.participantRepository.findAll()).map((participant) => participant.toObject());
38+
async findAll({ sortBy, sortOrder }: { sortBy?: keyof QueryOrderMap<Participant>; sortOrder?: SortOrder }) {
39+
return (await this.participantRepository.findAll({ orderBy: sortBy && { [sortBy]: sortOrder } })).map((participant) =>
40+
participant.toObject()
41+
);
3942
}
4043

4144
async findOne(id: number) {

apps/frontend/src/api.gen.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,10 @@ export interface components {
687687
*/
688688
birthday?: string;
689689
};
690+
/** @enum {string} */
691+
ParticipantSortableField: "id" | "birthday";
692+
/** @enum {string} */
693+
SortOrder: "ASC" | "DESC";
690694
EntryLanguageResponseDto: {
691695
/**
692696
* @description The ratio in percent of the entry language
@@ -838,8 +842,6 @@ export interface components {
838842
};
839843
/** @enum {string} */
840844
QuestionnaireSortableField: "createdAt" | "completedAt";
841-
/** @enum {string} */
842-
SortOrder: "ASC" | "DESC";
843845
QuestionnaireDetailResponseDto: {
844846
/**
845847
* Format: date-time
@@ -1575,7 +1577,12 @@ export interface operations {
15751577
};
15761578
ParticipantsController_index: {
15771579
parameters: {
1578-
query?: never;
1580+
query?: {
1581+
/** @description Field to sort by */
1582+
sortBy?: components["schemas"]["ParticipantSortableField"];
1583+
/** @description Sort order */
1584+
sortOrder?: components["schemas"]["SortOrder"];
1585+
};
15791586
header?: never;
15801587
path?: never;
15811588
cookie?: never;
@@ -2221,5 +2228,6 @@ export const userResponseDtoRoleValues: ReadonlyArray<components["schemas"]["Use
22212228
export const userMutationDtoRoleValues: ReadonlyArray<components["schemas"]["UserMutationDto"]["role"]> = ["ASSISTANT", "ADMIN"];
22222229
export const sessionResponseDtoRoleValues: ReadonlyArray<components["schemas"]["SessionResponseDto"]["role"]> = ["ASSISTANT", "ADMIN"];
22232230
export const exportTypeValues: ReadonlyArray<components["schemas"]["ExportType"]> = ["csv", "sql"];
2224-
export const questionnaireSortableFieldValues: ReadonlyArray<components["schemas"]["QuestionnaireSortableField"]> = ["createdAt", "completedAt"];
2231+
export const participantSortableFieldValues: ReadonlyArray<components["schemas"]["ParticipantSortableField"]> = ["id", "birthday"];
22252232
export const sortOrderValues: ReadonlyArray<components["schemas"]["SortOrder"]> = ["ASC", "DESC"];
2233+
export const questionnaireSortableFieldValues: ReadonlyArray<components["schemas"]["QuestionnaireSortableField"]> = ["createdAt", "completedAt"];

apps/frontend/src/routes/_auth/administration/participants/index.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { createFileRoute, Link } from "@tanstack/react-router";
22
import { $api } from "../../../../stores/api";
3-
import { Button, Table } from "@quassel/ui";
3+
import { Button, Group, Table } from "@quassel/ui";
44
import { useSuspenseQuery } from "@tanstack/react-query";
55
import { $session } from "../../../../stores/session";
66
import { useStore } from "@nanostores/react";
77
import { format } from "../../../../stores/i18n";
8+
import { useSort } from "../../../../hooks/useSort";
9+
import { paths } from "../../../../api.gen";
810

911
function AdministrationParticipantsIndex() {
1012
const { time } = useStore(format);
1113
const sessionStore = useStore($session);
12-
const participants = useSuspenseQuery($api.queryOptions("get", "/participants"));
14+
15+
const search = Route.useSearch();
16+
const { ToggleLink } = useSort(Route);
17+
18+
const participants = useSuspenseQuery($api.queryOptions("get", "/participants", { params: { query: search } }));
1319
const deleteParticipantMutation = $api.useMutation("delete", "/participants/{id}", {
1420
onSuccess: () => participants.refetch(),
1521
});
@@ -18,8 +24,18 @@ function AdministrationParticipantsIndex() {
1824
<Table>
1925
<Table.Thead>
2026
<Table.Tr>
21-
<Table.Th>Id</Table.Th>
22-
<Table.Th>Birthday</Table.Th>
27+
<Table.Th>
28+
<Group>
29+
Id
30+
<ToggleLink sortKey="id" />
31+
</Group>
32+
</Table.Th>
33+
<Table.Th>
34+
<Group>
35+
Birthday
36+
<ToggleLink sortKey="birthday" />
37+
</Group>
38+
</Table.Th>
2339
<Table.Th>Actions</Table.Th>
2440
</Table.Tr>
2541
</Table.Thead>
@@ -52,6 +68,8 @@ function AdministrationParticipantsIndex() {
5268
);
5369
}
5470

71+
type SearchParams = NonNullable<paths["/participants"]["get"]["parameters"]["query"]>;
72+
5573
export const Route = createFileRoute("/_auth/administration/participants/")({
5674
beforeLoad: () => ({
5775
actions: [
@@ -65,4 +83,5 @@ export const Route = createFileRoute("/_auth/administration/participants/")({
6583
}),
6684
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/participants")),
6785
component: () => <AdministrationParticipantsIndex />,
86+
validateSearch: (search) => search as SearchParams,
6887
});

0 commit comments

Comments
 (0)