Skip to content

Commit ac7d42c

Browse files
authored
Feature/allow supervisors to draft theses (#781)
1 parent 520cd2f commit ac7d42c

File tree

21 files changed

+237
-79
lines changed

21 files changed

+237
-79
lines changed

client/src/components/TopicsFilters/TopicsFilters.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Center, Checkbox, Grid, Stack } from '@mantine/core'
1+
import { Center, Checkbox, Grid, Group, SegmentedControl, Stack } from '@mantine/core'
22
import { GLOBAL_CONFIG } from '../../config/global'
3-
import React from 'react'
43
import { useTopicsContext } from '../../providers/TopicsProvider/hooks'
5-
import { formatThesisType } from '../../utils/format'
4+
import { TopicState } from '../../requests/responses/topic'
5+
import { formatThesisType, formatTopicState } from '../../utils/format'
66

77
interface ITopicsFiltersProps {
8-
visible: Array<'type' | 'closed'>
8+
visible: Array<'type' | 'states'>
99
}
1010

1111
const TopicsFilters = (props: ITopicsFiltersProps) => {
@@ -15,17 +15,24 @@ const TopicsFilters = (props: ITopicsFiltersProps) => {
1515

1616
return (
1717
<Stack>
18-
{visible.includes('closed') && (
19-
<Checkbox
20-
label='Show Closed Topics'
21-
checked={!!filters.includeClosed}
22-
onChange={(e) => {
23-
setFilters((prev) => ({
24-
...prev,
25-
includeClosed: e.target.checked,
26-
}))
27-
}}
28-
/>
18+
{visible.includes('states') && (
19+
<Group>
20+
<SegmentedControl
21+
data={Object.values(TopicState).map((state) => ({
22+
label: formatTopicState(state),
23+
value: state,
24+
}))}
25+
onChange={(e) => {
26+
setFilters((prev) => ({
27+
...prev,
28+
states: [e],
29+
}))
30+
}}
31+
size='sm'
32+
transitionDuration={300}
33+
transitionTimingFunction='linear'
34+
/>
35+
</Group>
2936
)}
3037
{visible.includes('type') && (
3138
<Grid grow>

client/src/components/TopicsTable/TopicsTable.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { DataTable, DataTableColumn } from 'mantine-datatable'
22
import { formatDate } from '../../utils/format'
33
import { useTopicsContext } from '../../providers/TopicsProvider/hooks'
4-
import { ITopic } from '../../requests/responses/topic'
4+
import { ITopic, TopicState } from '../../requests/responses/topic'
55
import { useNavigate } from 'react-router'
66
import { Badge, Center, Stack, Text } from '@mantine/core'
77
import AvatarUserList from '../AvatarUserList/AvatarUserList'
8-
import React from 'react'
98
import ThesisTypeBadge from '../../pages/LandingPage/components/ThesisTypBadge/ThesisTypBadge'
109

1110
type TopicColumn =
@@ -35,6 +34,19 @@ const TopicsTable = (props: ITopicsTableProps) => {
3534

3635
const { topics, page, setPage, limit, isLoading } = useTopicsContext()
3736

37+
const getTopicColor = (state: TopicState) => {
38+
switch (state) {
39+
case TopicState.OPEN:
40+
return 'green'
41+
case TopicState.CLOSED:
42+
return 'red'
43+
case TopicState.DRAFT:
44+
return 'yellow'
45+
default:
46+
return 'gray'
47+
}
48+
}
49+
3850
const columnConfig: Record<TopicColumn, DataTableColumn<ITopic>> = {
3951
state: {
4052
accessor: 'state',
@@ -43,7 +55,9 @@ const TopicsTable = (props: ITopicsTableProps) => {
4355
width: 100,
4456
render: (topic) => (
4557
<Center>
46-
{topic.closedAt ? <Badge color='red'>Closed</Badge> : <Badge color='gray'>Open</Badge>}
58+
<Badge color={getTopicColor(topic.state)} radius='sm'>
59+
{topic.state}
60+
</Badge>
4761
</Center>
4862
),
4963
},

client/src/pages/ManageTopicsPage/ManageTopicsPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Button, Group, Stack, Title } from '@mantine/core'
44
import TopicsProvider from '../../providers/TopicsProvider/TopicsProvider'
55
import TopicsTable from '../../components/TopicsTable/TopicsTable'
66
import { ITopic } from '../../requests/responses/topic'
7-
import { Pencil } from '@phosphor-icons/react'
7+
import { PencilIcon } from '@phosphor-icons/react'
88
import CloseTopicButton from './components/CloseTopicButton/CloseTopicButton'
99
import ReplaceTopicModal from './components/ReplaceTopicModal/ReplaceTopicModal'
1010
import TopicsFilters from '../../components/TopicsFilters/TopicsFilters'
@@ -33,7 +33,7 @@ const ManageTopicsPage = () => {
3333
<Button ml='auto' onClick={() => setCreateTopicModal(true)} hiddenFrom='md'>
3434
Create Topic
3535
</Button>
36-
<TopicsFilters visible={['closed']} />
36+
<TopicsFilters visible={['states']} />
3737
<TopicsTable
3838
columns={['state', 'title', 'types', 'supervisor', 'advisor', 'createdAt', 'actions']}
3939
extraColumns={{
@@ -52,7 +52,7 @@ const ManageTopicsPage = () => {
5252
>
5353
{!topic.closedAt && (
5454
<Button size='xs' onClick={() => setEditingTopic(topic)}>
55-
<Pencil />
55+
<PencilIcon />
5656
</Button>
5757
)}
5858
<CloseTopicButton size='xs' topic={topic} />

client/src/pages/ManageTopicsPage/components/CloseTopicButton/CloseTopicButton.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ITopic } from '../../../../requests/responses/topic'
1+
import { ITopic, TopicState } from '../../../../requests/responses/topic'
22
import { X } from '@phosphor-icons/react'
33
import React, { useEffect, useState } from 'react'
44
import { useTopicsContext } from '../../../../providers/TopicsProvider/hooks'
@@ -40,10 +40,12 @@ const CloseTopicButton = (props: ICloseTopicButtonProps) => {
4040
return null
4141
}
4242

43+
const titleName = topic.state === TopicState.DRAFT ? 'Draft' : 'Topic'
44+
4345
return (
4446
<Button onClick={() => setConfirmationModal(true)} size={size}>
4547
<Modal
46-
title='Close Topic'
48+
title={`Close ${titleName}`}
4749
opened={confirmationModal}
4850
onClose={() => setConfirmationModal(false)}
4951
onClick={(e) => e.stopPropagation()}
@@ -76,24 +78,28 @@ const CloseTopicButton = (props: ICloseTopicButtonProps) => {
7678
>
7779
<Stack>
7880
<Text>
79-
Are you sure you want to close this topic? This will reject all applications for it.
81+
{`Are you sure you want to close this ${titleName.toLowerCase()}? ${topic.state === TopicState.DRAFT ? '' : 'This will reject all applications for the topic.'}`}
8082
</Text>
81-
<Select
82-
label='Reason'
83-
required
84-
data={[
85-
{ value: 'TOPIC_FILLED', label: 'Topic was filled' },
86-
{ value: 'TOPIC_OUTDATED', label: 'Topic is outdated' },
87-
]}
88-
{...form.getInputProps('reason')}
89-
/>
90-
<Checkbox
91-
label='Notify Students'
92-
required
93-
{...form.getInputProps('notifyUser', { type: 'checkbox' })}
94-
/>
83+
{topic.state !== TopicState.DRAFT && (
84+
<>
85+
<Select
86+
label='Reason'
87+
required
88+
data={[
89+
{ value: 'TOPIC_FILLED', label: 'Topic was filled' },
90+
{ value: 'TOPIC_OUTDATED', label: 'Topic is outdated' },
91+
]}
92+
{...form.getInputProps('reason')}
93+
/>
94+
<Checkbox
95+
label='Notify Students'
96+
required
97+
{...form.getInputProps('notifyUser', { type: 'checkbox' })}
98+
/>
99+
</>
100+
)}
95101
<Button type='submit' loading={loading} fullWidth>
96-
Close Topic
102+
{`Close ${titleName}`}
97103
</Button>
98104
</Stack>
99105
</form>

client/src/pages/ManageTopicsPage/components/ReplaceTopicModal/ReplaceTopicModal.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Button, Modal, MultiSelect, Select, Stack, TextInput } from '@mantine/core'
2-
import { ITopic } from '../../../../requests/responses/topic'
1+
import { Button, Group, Modal, MultiSelect, Select, Stack, TextInput } from '@mantine/core'
2+
import { ITopic, TopicState } from '../../../../requests/responses/topic'
33
import { isNotEmpty, useForm } from '@mantine/form'
44
import { isNotEmptyUserList } from '../../../../utils/validation'
55
import { useEffect, useState } from 'react'
@@ -144,7 +144,7 @@ const ReplaceTopicModal = (props: ICreateTopicModalProps) => {
144144
)
145145
}, [opened])
146146

147-
const onSubmit = async () => {
147+
const onSubmit = async (isDraft = false) => {
148148
setLoading(true)
149149

150150
try {
@@ -163,6 +163,7 @@ const ReplaceTopicModal = (props: ICreateTopicModalProps) => {
163163
researchGroupId: form.values.researchGroupId,
164164
intendedStart: form.values.intendedStart ?? null,
165165
applicationDeadline: form.values.applicationDeadline ?? null,
166+
isDraft: isDraft,
166167
},
167168
})
168169

@@ -191,7 +192,7 @@ const ReplaceTopicModal = (props: ICreateTopicModalProps) => {
191192
opened={opened}
192193
onClose={onClose}
193194
>
194-
<form onSubmit={form.onSubmit(onSubmit)}>
195+
<form onSubmit={form.onSubmit(() => onSubmit(false))}>
195196
<Stack gap='md'>
196197
<TextInput label='Title' required {...form.getInputProps('title')} />
197198
<MultiSelect
@@ -266,9 +267,25 @@ const ReplaceTopicModal = (props: ICreateTopicModalProps) => {
266267
editMode={true}
267268
{...form.getInputProps('references')}
268269
/>
269-
<Button type='submit' fullWidth disabled={!form.isValid()} loading={loading}>
270-
{topic ? 'Save changes' : 'Create topic'}
271-
</Button>
270+
<Group>
271+
{(!props.topic || props.topic.state === TopicState.DRAFT) && (
272+
<Button
273+
variant='default'
274+
onClick={() => onSubmit(true)}
275+
disabled={!form.isValid()}
276+
loading={loading}
277+
>
278+
{props.topic ? 'Save Changes' : 'Create Draft'}
279+
</Button>
280+
)}
281+
<Button type='submit' flex={1} disabled={!form.isValid()} loading={loading}>
282+
{topic
283+
? topic.state === TopicState.DRAFT
284+
? 'Save & Create Topic'
285+
: 'Save Changes'
286+
: 'Create Topic'}
287+
</Button>
288+
</Group>
272289
</Stack>
273290
</form>
274291
</Modal>

client/src/providers/TopicsProvider/TopicsProvider.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'
22
import { doRequest } from '../../requests/request'
33
import { showSimpleError } from '../../utils/notification'
4-
import { ITopic } from '../../requests/responses/topic'
4+
import { ITopic, TopicState } from '../../requests/responses/topic'
55
import { ITopicsContext, ITopicsFilters, TopicsContext } from './context'
66
import { PaginationResponse } from '../../requests/responses/pagination'
77

88
interface ITopicsProviderProps {
9-
includeClosedTopics?: boolean
109
limit: number
1110
hideIfEmpty?: boolean
1211
researchSpecific?: boolean
1312
initialFilters?: Partial<ITopicsFilters>
13+
states?: TopicState[]
1414
}
1515

1616
const TopicsProvider = (props: PropsWithChildren<ITopicsProviderProps>) => {
1717
const {
1818
children,
19-
includeClosedTopics = false,
2019
limit,
2120
hideIfEmpty = false,
2221
researchSpecific = true,
2322
initialFilters,
23+
states = [],
2424
} = props
2525

2626
const [topics, setTopics] = useState<PaginationResponse<ITopic>>()
2727
const [page, setPage] = useState(0)
2828
const [filters, setFilters] = useState<ITopicsFilters>({
29-
includeClosed: includeClosedTopics,
29+
states: states,
3030
researchSpecific: researchSpecific,
3131
...initialFilters,
3232
})
3333

3434
const [isLoading, setIsLoading] = useState(false)
3535

36-
useEffect(() => {
36+
const fetchTopics = async () => {
3737
setIsLoading(true)
3838

3939
return doRequest<PaginationResponse<ITopic>>(
@@ -45,7 +45,7 @@ const TopicsProvider = (props: PropsWithChildren<ITopicsProviderProps>) => {
4545
page,
4646
limit,
4747
type: filters.types?.join(',') || '',
48-
includeClosed: filters.includeClosed ? 'true' : 'false',
48+
states: filters.states?.join(',') || '',
4949
onlyOwnResearchGroup: filters.researchSpecific ? 'true' : 'false',
5050
search: filters.search ?? '',
5151
researchGroupIds: filters.researchGroupIds?.join(',') || '',
@@ -70,12 +70,16 @@ const TopicsProvider = (props: PropsWithChildren<ITopicsProviderProps>) => {
7070
setTopics(res.data)
7171
},
7272
)
73+
}
74+
75+
useEffect(() => {
76+
fetchTopics()
7377
}, [filters, page, limit])
7478

7579
useEffect(() => {
7680
setFilters((prev) => ({
7781
...prev,
78-
includeClosed: includeClosedTopics,
82+
states: states,
7983
researchSpecific: researchSpecific,
8084
...initialFilters,
8185
}))
@@ -99,10 +103,18 @@ const TopicsProvider = (props: PropsWithChildren<ITopicsProviderProps>) => {
99103

100104
const index = prev.content.findIndex((x) => x.topicId === newTopic.topicId)
101105

106+
let newFetchRequired = false
107+
102108
if (index >= 0) {
109+
newFetchRequired = newTopic.state !== prev.content[index].state
103110
prev.content[index] = newTopic
104111
}
105112

113+
if (newFetchRequired) {
114+
// If state changed, refetch to update based on filters
115+
fetchTopics()
116+
}
117+
106118
return { ...prev }
107119
})
108120
},
@@ -112,9 +124,14 @@ const TopicsProvider = (props: PropsWithChildren<ITopicsProviderProps>) => {
112124
return undefined
113125
}
114126

115-
prev.content = [newTopic, ...prev.content].slice(-limit)
116-
prev.totalElements += 1
117-
prev.totalPages = Math.ceil(prev.totalElements / limit)
127+
const states = filters.states ?? [TopicState.OPEN.toString()]
128+
const newHasState = states.includes(newTopic.state)
129+
130+
if (newHasState) {
131+
prev.content = [newTopic, ...prev.content].slice(-limit)
132+
prev.totalElements += 1
133+
prev.totalPages = Math.ceil(prev.totalElements / limit)
134+
}
118135

119136
return { ...prev }
120137
})

client/src/providers/TopicsProvider/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { PaginationResponse } from '../../requests/responses/pagination'
44

55
export interface ITopicsFilters {
66
types?: string[]
7-
includeClosed?: boolean
7+
states?: string[]
88
researchSpecific?: boolean
99
search?: string
1010
researchGroupIds?: string[]

client/src/requests/responses/topic.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ export interface ITopic {
1010
goals: string
1111
references: string
1212
closedAt: string | null
13+
publishedAt: string | null
1314
updatedAt: string
1415
createdAt: string
1516
intendedStart: string | null
1617
applicationDeadline: string | null
18+
state: TopicState
1719
createdBy: ILightUser
1820
researchGroup: ILightResearchGroup
1921
advisors: ILightUser[]
2022
supervisors: ILightUser[]
2123
}
24+
25+
export enum TopicState {
26+
OPEN = 'OPEN',
27+
DRAFT = 'DRAFT',
28+
CLOSED = 'CLOSED',
29+
}

0 commit comments

Comments
 (0)