diff --git a/src/api/api.ts b/src/api/api.ts index 9093bc2..ae19fdf 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -42,7 +42,7 @@ export const APIErrorCode = { ErrCodeInvalidFileData: 2109, ErrCodeInvalidPhoneNumber: 2110, ErrCodePhoneNumberAlreadyImported: 2111, - ErrCodeInvalidUsername: 2112, + ErrCodeInvalidUserID: 2112, ErrCodeNoPhonesDonated: 2113, ErrCodeAgentNotConnected: 2114, ErrCodeInvalidPagination: 2115, @@ -81,7 +81,10 @@ export const APIErrorCode = { ErrCodeExamFinished: 2148, ErrCodeExamQuestionNotFound: 2149, ErrCodeInvalidAnswerOption: 2150, - ErrCodeGivenExamNotFound: 2151 + ErrCodeGivenExamNotFound: 2151, + ErrCodeAccountAlreadyConfirmed: 2152, + ErrCodeEmailAlreadyExists: 2153, + ErrCodeTopicNameExists: 2154 } as const; export type APIErrorCode = typeof APIErrorCode[keyof typeof APIErrorCode]; @@ -466,31 +469,6 @@ export interface ConfirmAccountData { */ 'user_id'?: string; } -/** - * - * @export - * @interface ConfirmAccountV1200Response - */ -export interface ConfirmAccountV1200Response { - /** - * - * @type {EndpointError} - * @memberof ConfirmAccountV1200Response - */ - 'error'?: EndpointError; - /** - * - * @type {boolean} - * @memberof ConfirmAccountV1200Response - */ - 'result'?: boolean; - /** - * - * @type {boolean} - * @memberof ConfirmAccountV1200Response - */ - 'success'?: boolean; -} /** * * @export @@ -927,6 +905,31 @@ export interface CreateUserV1200Response { */ 'success'?: boolean; } +/** + * + * @export + * @interface DeleteTopicV1200Response + */ +export interface DeleteTopicV1200Response { + /** + * + * @type {EndpointError} + * @memberof DeleteTopicV1200Response + */ + 'error'?: EndpointError; + /** + * + * @type {boolean} + * @memberof DeleteTopicV1200Response + */ + 'result'?: boolean; + /** + * + * @type {boolean} + * @memberof DeleteTopicV1200Response + */ + 'success'?: boolean; +} /** * * @export @@ -2675,16 +2678,16 @@ export const CourseApiAxiosParamCreator = function (configuration?: Configuratio /** * Allows a user to get information about a course by its id. * @summary Get course information - * @param {number} id Course ID * @param {string} authorization Authorization token + * @param {number} id Course ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getCourseInfoV1: async (id: number, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('getCourseInfoV1', 'id', id) + getCourseInfoV1: async (authorization: string, id: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'authorization' is not null or undefined assertParamExists('getCourseInfoV1', 'authorization', authorization) + // verify required parameter 'id' is not null or undefined + assertParamExists('getCourseInfoV1', 'id', id) const localVarPath = `/api/v1/course/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -2915,13 +2918,13 @@ export const CourseApiFp = function(configuration?: Configuration) { /** * Allows a user to get information about a course by its id. * @summary Get course information - * @param {number} id Course ID * @param {string} authorization Authorization token + * @param {number} id Course ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getCourseInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseInfoV1(id, authorization, options); + async getCourseInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseInfoV1(authorization, id, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['CourseApi.getCourseInfoV1']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -3006,13 +3009,13 @@ export const CourseApiFactory = function (configuration?: Configuration, basePat /** * Allows a user to get information about a course by its id. * @summary Get course information - * @param {number} id Course ID * @param {string} authorization Authorization token + * @param {number} id Course ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getCourseInfoV1(id: number, authorization: string, options?: any): AxiosPromise { - return localVarFp.getCourseInfoV1(id, authorization, options).then((request) => request(axios, basePath)); + getCourseInfoV1(authorization: string, id: number, options?: any): AxiosPromise { + return localVarFp.getCourseInfoV1(authorization, id, options).then((request) => request(axios, basePath)); }, /** * Allows a user to get all participants of a course. @@ -3084,14 +3087,14 @@ export class CourseApi extends BaseAPI { /** * Allows a user to get information about a course by its id. * @summary Get course information - * @param {number} id Course ID * @param {string} authorization Authorization token + * @param {number} id Course ID * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseApi */ - public getCourseInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig) { - return CourseApiFp(this.configuration).getCourseInfoV1(id, authorization, options).then((request) => request(this.axios, this.basePath)); + public getCourseInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig) { + return CourseApiFp(this.configuration).getCourseInfoV1(authorization, id, options).then((request) => request(this.axios, this.basePath)); } /** @@ -3244,16 +3247,16 @@ export const ExamApiAxiosParamCreator = function (configuration?: Configuration) /** * Allows the user to get information about an exam. * @summary Get information about an exam - * @param {number} id Exam ID * @param {string} authorization Authorization token + * @param {number} id Exam ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getExamInfoV1: async (id: number, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('getExamInfoV1', 'id', id) + getExamInfoV1: async (authorization: string, id: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'authorization' is not null or undefined assertParamExists('getExamInfoV1', 'authorization', authorization) + // verify required parameter 'id' is not null or undefined + assertParamExists('getExamInfoV1', 'id', id) const localVarPath = `/api/v1/exam/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3540,13 +3543,13 @@ export const ExamApiFp = function(configuration?: Configuration) { /** * Allows the user to get information about an exam. * @summary Get information about an exam - * @param {number} id Exam ID * @param {string} authorization Authorization token + * @param {number} id Exam ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getExamInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getExamInfoV1(id, authorization, options); + async getExamInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getExamInfoV1(authorization, id, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['ExamApi.getExamInfoV1']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -3656,13 +3659,13 @@ export const ExamApiFactory = function (configuration?: Configuration, basePath? /** * Allows the user to get information about an exam. * @summary Get information about an exam - * @param {number} id Exam ID * @param {string} authorization Authorization token + * @param {number} id Exam ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getExamInfoV1(id: number, authorization: string, options?: any): AxiosPromise { - return localVarFp.getExamInfoV1(id, authorization, options).then((request) => request(axios, basePath)); + getExamInfoV1(authorization: string, id: number, options?: any): AxiosPromise { + return localVarFp.getExamInfoV1(authorization, id, options).then((request) => request(axios, basePath)); }, /** * Allows the user to get questions of an exam. @@ -3758,14 +3761,14 @@ export class ExamApi extends BaseAPI { /** * Allows the user to get information about an exam. * @summary Get information about an exam - * @param {number} id Exam ID * @param {string} authorization Authorization token + * @param {number} id Exam ID * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof ExamApi */ - public getExamInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig) { - return ExamApiFp(this.configuration).getExamInfoV1(id, authorization, options).then((request) => request(this.axios, this.basePath)); + public getExamInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig) { + return ExamApiFp(this.configuration).getExamInfoV1(authorization, id, options).then((request) => request(this.axios, this.basePath)); } /** @@ -3885,6 +3888,50 @@ export const TopicApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * Allows moderators to delete a topic. All courses and exams related to the topic will be deleted as well. + * @summary Delete a topic + * @param {string} authorization Authorization token + * @param {number} id Topic ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTopicV1: async (authorization: string, id: number, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'authorization' is not null or undefined + assertParamExists('deleteTopicV1', 'authorization', authorization) + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteTopicV1', 'id', id) + const localVarPath = `/api/v1/topic/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + if (authorization != null) { + localVarHeaderParameter['Authorization'] = String(authorization); + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Get all user topic stats * @summary Get all user topic stats @@ -3925,16 +3972,16 @@ export const TopicApiAxiosParamCreator = function (configuration?: Configuration /** * Get topic info * @summary Get topic info - * @param {number} id Topic ID * @param {string} authorization Authorization token + * @param {number} id Topic ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTopicInfoV1: async (id: number, authorization: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('getTopicInfoV1', 'id', id) + getTopicInfoV1: async (authorization: string, id: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'authorization' is not null or undefined assertParamExists('getTopicInfoV1', 'authorization', authorization) + // verify required parameter 'id' is not null or undefined + assertParamExists('getTopicInfoV1', 'id', id) const localVarPath = `/api/v1/topic/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -4076,6 +4123,20 @@ export const TopicApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['TopicApi.createTopicV1']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Allows moderators to delete a topic. All courses and exams related to the topic will be deleted as well. + * @summary Delete a topic + * @param {string} authorization Authorization token + * @param {number} id Topic ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTopicV1(authorization: string, id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTopicV1(authorization, id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['TopicApi.deleteTopicV1']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Get all user topic stats * @summary Get all user topic stats @@ -4092,13 +4153,13 @@ export const TopicApiFp = function(configuration?: Configuration) { /** * Get topic info * @summary Get topic info - * @param {number} id Topic ID * @param {string} authorization Authorization token + * @param {number} id Topic ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTopicInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTopicInfoV1(id, authorization, options); + async getTopicInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTopicInfoV1(authorization, id, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['TopicApi.getTopicInfoV1']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -4152,6 +4213,17 @@ export const TopicApiFactory = function (configuration?: Configuration, basePath createTopicV1(authorization: string, data: CreateNewTopicData, options?: any): AxiosPromise { return localVarFp.createTopicV1(authorization, data, options).then((request) => request(axios, basePath)); }, + /** + * Allows moderators to delete a topic. All courses and exams related to the topic will be deleted as well. + * @summary Delete a topic + * @param {string} authorization Authorization token + * @param {number} id Topic ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTopicV1(authorization: string, id: number, options?: any): AxiosPromise { + return localVarFp.deleteTopicV1(authorization, id, options).then((request) => request(axios, basePath)); + }, /** * Get all user topic stats * @summary Get all user topic stats @@ -4165,13 +4237,13 @@ export const TopicApiFactory = function (configuration?: Configuration, basePath /** * Get topic info * @summary Get topic info - * @param {number} id Topic ID * @param {string} authorization Authorization token + * @param {number} id Topic ID * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTopicInfoV1(id: number, authorization: string, options?: any): AxiosPromise { - return localVarFp.getTopicInfoV1(id, authorization, options).then((request) => request(axios, basePath)); + getTopicInfoV1(authorization: string, id: number, options?: any): AxiosPromise { + return localVarFp.getTopicInfoV1(authorization, id, options).then((request) => request(axios, basePath)); }, /** * Get user topic stat @@ -4218,6 +4290,19 @@ export class TopicApi extends BaseAPI { return TopicApiFp(this.configuration).createTopicV1(authorization, data, options).then((request) => request(this.axios, this.basePath)); } + /** + * Allows moderators to delete a topic. All courses and exams related to the topic will be deleted as well. + * @summary Delete a topic + * @param {string} authorization Authorization token + * @param {number} id Topic ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TopicApi + */ + public deleteTopicV1(authorization: string, id: number, options?: RawAxiosRequestConfig) { + return TopicApiFp(this.configuration).deleteTopicV1(authorization, id, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get all user topic stats * @summary Get all user topic stats @@ -4233,14 +4318,14 @@ export class TopicApi extends BaseAPI { /** * Get topic info * @summary Get topic info - * @param {number} id Topic ID * @param {string} authorization Authorization token + * @param {number} id Topic ID * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TopicApi */ - public getTopicInfoV1(id: number, authorization: string, options?: RawAxiosRequestConfig) { - return TopicApiFp(this.configuration).getTopicInfoV1(id, authorization, options).then((request) => request(this.axios, this.basePath)); + public getTopicInfoV1(authorization: string, id: number, options?: RawAxiosRequestConfig) { + return TopicApiFp(this.configuration).getTopicInfoV1(authorization, id, options).then((request) => request(this.axios, this.basePath)); } /** @@ -4801,7 +4886,7 @@ export const UserApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async confirmAccountV1(confirmAccountData: ConfirmAccountData, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async confirmAccountV1(confirmAccountData: ConfirmAccountData, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.confirmAccountV1(confirmAccountData, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['UserApi.confirmAccountV1']?.[localVarOperationServerIndex]?.url; @@ -4814,7 +4899,7 @@ export const UserApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async confirmChangePasswordV1(confirmChangePasswordData: ConfirmChangePasswordData, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async confirmChangePasswordV1(confirmChangePasswordData: ConfirmChangePasswordData, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.confirmChangePasswordV1(confirmChangePasswordData, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['UserApi.confirmChangePasswordV1']?.[localVarOperationServerIndex]?.url; @@ -4967,7 +5052,7 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? * @param {*} [options] Override http request option. * @throws {RequiredError} */ - confirmAccountV1(confirmAccountData: ConfirmAccountData, options?: any): AxiosPromise { + confirmAccountV1(confirmAccountData: ConfirmAccountData, options?: any): AxiosPromise { return localVarFp.confirmAccountV1(confirmAccountData, options).then((request) => request(axios, basePath)); }, /** @@ -4977,7 +5062,7 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? * @param {*} [options] Override http request option. * @throws {RequiredError} */ - confirmChangePasswordV1(confirmChangePasswordData: ConfirmChangePasswordData, options?: any): AxiosPromise { + confirmChangePasswordV1(confirmChangePasswordData: ConfirmChangePasswordData, options?: any): AxiosPromise { return localVarFp.confirmChangePasswordV1(confirmChangePasswordData, options).then((request) => request(axios, basePath)); }, /** diff --git a/src/apiClient.ts b/src/apiClient.ts index 8fba0df..b3e4503 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -316,6 +316,21 @@ class ExamSphereAPIClient extends UserApi { return searchTopicResult; } + public async deleteTopic(topicId: number): Promise { + if (!this.isLoggedIn()) { + throw new Error("Not logged in"); + } + + let deleteTopicResult = (await this.topicApi.deleteTopicV1(`Bearer ${this.accessToken}`, topicId))?.data.result; + if (!deleteTopicResult) { + // we shouldn't reach here, because if there is an error somewhere, + // it should have already been thrown by the API client + throw new Error("Failed to delete topic"); + } + + return deleteTopicResult; + } + /** * Returns true if we are considered as "logged in" by the API client, * This method only checks if the access token is present, it doesn't diff --git a/src/components/menus/sideMenu.tsx b/src/components/menus/sideMenu.tsx index 50c9338..1086081 100644 --- a/src/components/menus/sideMenu.tsx +++ b/src/components/menus/sideMenu.tsx @@ -69,10 +69,6 @@ const RenderManageTopicsMenu = () => { label={CurrentAppTranslation.SearchTopicsText} href='/searchTopic' > - ) }; diff --git a/src/pages/createTopicPage.tsx b/src/pages/createTopicPage.tsx index 9b7aaf8..4e0490e 100644 --- a/src/pages/createTopicPage.tsx +++ b/src/pages/createTopicPage.tsx @@ -37,7 +37,7 @@ const CreateTopicPage: React.FC = () => { - {CurrentAppTranslation.CreateNewUserText} + {CurrentAppTranslation.CreateNewTopicText} { value={createTopicData.topic_name ?? ''} onChange={(e) => { handleInputChange(e as any) }} required /> - {CurrentAppTranslation.CreateUserButtonText} + {CurrentAppTranslation.CreateTopicButtonText} diff --git a/src/pages/searchTopicPage.tsx b/src/pages/searchTopicPage.tsx index f8d025e..da10bd6 100644 --- a/src/pages/searchTopicPage.tsx +++ b/src/pages/searchTopicPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, Fragment } from 'react'; import { TextField, List, @@ -7,6 +7,7 @@ import { Paper, Grid, Typography, + Button, } from '@mui/material'; import { Box } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; @@ -16,12 +17,78 @@ import apiClient from '../apiClient'; import DashboardContainer from '../components/containers/dashboardContainer'; import { CurrentAppTranslation } from '../translations/appTranslation'; -const RenderTopicsList = (topics: SearchedTopicInfo[] | undefined, forEdit: boolean = false) => { +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useTheme } from '@mui/material/styles'; +import useAppSnackbar from '../components/snackbars/useAppSnackbars'; +import { extractErrorDetails } from '../utils/errorUtils'; + +interface DeleteDialogueProps { + target_topic_id: number; + handleDelete: (topic_id: number) => Promise; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + fullScreen: boolean; +} + +var currentDeleteProps: DeleteDialogueProps | null = null; +var confirmedDeleteTopicId: number = 0; + + +const DeleteDialogueComponent = () => { + const props = currentDeleteProps; + if (!props) { + return null; + } + + const handleClose = () => { + props.setIsOpen(false); + currentDeleteProps = null; + confirmedDeleteTopicId = 0; + }; + + return ( + + + + {CurrentAppTranslation.AreYouSureDeleteTopicText} + + + + {CurrentAppTranslation.DeleteTopicDescriptionText} + + + + + + + + + ); +} + +const RenderTopicsList = ( + topics: SearchedTopicInfo[] | undefined, handleDelete: (topicId: number) => Promise) => { if (!topics || topics.length === 0) { return ( - {forEdit ? CurrentAppTranslation.EnterSearchForEdit : - CurrentAppTranslation.NoResultsFoundText} + {CurrentAppTranslation.SearchSomethingForTopicsText} ); } @@ -32,13 +99,7 @@ const RenderTopicsList = (topics: SearchedTopicInfo[] | undefined, forEdit: bool { - // Redirect to user info page, make sure to query encode it - window.location.href = `/topicInfo?topicId=${encodeURIComponent(topic.topic_id!)}`; - } - }> + sx={{ width: '100%', p: 2, borderRadius: 2 }}> @@ -48,6 +109,15 @@ const RenderTopicsList = (topics: SearchedTopicInfo[] | undefined, forEdit: bool {`${CurrentAppTranslation.topic_name}: ${topic.topic_name}`} + + + @@ -59,11 +129,16 @@ const RenderTopicsList = (topics: SearchedTopicInfo[] | undefined, forEdit: bool const SearchTopicPage = () => { const urlSearch = new URLSearchParams(window.location.search); const providedQuery = urlSearch.get('query'); - const forEdit = (urlSearch.get('edit') ?? "false") === "true"; + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const [query, setQuery] = useState(providedQuery ?? ''); const [topics, setTopics] = useState([]); + const [isDeleteDialogueOpen, setIsDeleteDialogueOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [deleteDialogue, setDeleteDialogue] = useState(null); + const snackbar = useAppSnackbar(); + const handleSearch = async () => { window.history.pushState( @@ -72,10 +147,6 @@ const SearchTopicPage = () => { `/searchTopic?query=${encodeURIComponent(query)}`, ); - if (query === '') { - return; - } - setIsLoading(true); const results = await apiClient.searchTopic({ topic_name: query, @@ -90,6 +161,40 @@ const SearchTopicPage = () => { setIsLoading(false); }; + const handleDelete = async (topicId: number) => { + if (!confirmedDeleteTopicId) { + setIsDeleteDialogueOpen(true); + confirmedDeleteTopicId = topicId; + currentDeleteProps = { + target_topic_id: topicId, + handleDelete: handleDelete, + isOpen: true, + setIsOpen: setIsDeleteDialogueOpen, + fullScreen: fullScreen, + }; + setDeleteDialogue(DeleteDialogueComponent); + return; + } + confirmedDeleteTopicId = 0; + setDeleteDialogue(null); + currentDeleteProps = null; + setIsDeleteDialogueOpen(false); + + setIsLoading(true); + + try { + const deleteResult = await apiClient.deleteTopic(topicId); + if (deleteResult) { + await handleSearch(); + snackbar.success(CurrentAppTranslation.TopicDeletedSuccessfullyText); + } + } catch (error: any) { + const [errCode, errMessage] = extractErrorDetails(error); + snackbar.error(`(${errCode}): ${errMessage}`); + } + + } + useEffect(() => { // if at first the query is not null (e.g. the providedQuery exists), // do the search. @@ -100,6 +205,7 @@ const SearchTopicPage = () => { return ( + {(deleteDialogue && isDeleteDialogueOpen) ? deleteDialogue : null} { }}> - ) : RenderTopicsList(topics, forEdit)} + ) : RenderTopicsList(topics, handleDelete)} diff --git a/src/translations/appTranslation.ts b/src/translations/appTranslation.ts index 08ae00e..1971bc1 100644 --- a/src/translations/appTranslation.ts +++ b/src/translations/appTranslation.ts @@ -37,16 +37,23 @@ export class AppTranslationBase { EditText: string = "Edit"; UserInformationText: string = "User Information"; ConfirmYourAccountText = "Confirm Your Account"; + CreateNewUserText: string = "Create New User"; + CreateNewTopicText: string = "Create New Topic"; + DeleteTopicButtonText: string = "Delete Topic"; + CancelButtonText: string = "Cancel"; // System messages + AreYouSureDeleteTopicText: string = "Are you sure you want to delete this topic?"; + DeleteTopicDescriptionText: string = "This action cannot be undone! All courses and exams related to this topic will be deleted as well."; + TopicDeletedSuccessfullyText: string = "Topic deleted successfully!"; ConfirmationSuccessText: string = "Confirmation was successful!"; PasswordsDoNotMatchText: string = "Passwords do not match!"; FailedToConfirmAccountCreationText: string = "Failed to confirm account creation"; UserNotFoundText: string = "This user doesn't seem to exist..."; - CreateNewUserText: string = "Create New User"; UserCreatedSuccessfullyText: string = "User created successfully"; TopicCreatedSuccessfullyText: string = "Topic created successfully"; NoResultsFoundText: string = "No results found, try changing your search query"; + SearchSomethingForTopicsText: string = "Search a query or enter empty to list all topics"; EnterSearchForEdit: string = "Enter search query to edit the user"; //#endregion diff --git a/src/translations/faTranslation.ts b/src/translations/faTranslation.ts index c9946e8..f9f533e 100644 --- a/src/translations/faTranslation.ts +++ b/src/translations/faTranslation.ts @@ -38,17 +38,23 @@ class FaTranslation extends AppTranslationBase { EditText: string = "ویرایش"; UserInformationText: string = "اطلاعات کاربر"; ConfirmYourAccountText = "حساب کاربری خود را تایید کنید"; - + CreateNewUserText: string = "ایجاد کاربر جدید"; + CreateNewTopicText: string = "ایجاد موضوع جدید"; + DeleteTopicButtonText: string = "حذف موضوع"; + CancelButtonText: string = "لغو"; // System messages + AreYouSureDeleteTopicText: string = "آیا مطمئن هستید که می خواهید این موضوع را حذف کنید؟"; + DeleteTopicDescriptionText: string = "این عملیات قابل بازگشت نیست! تمام دوره ها و آزمون های مرتبط با این موضوع نیز حذف خواهند شد."; + TopicDeletedSuccessfullyText: string = "موضوع با موفقیت حذف شد!"; ConfirmationSuccessText: string = "تایید با موفقیت انجام شد!"; PasswordsDoNotMatchText: string = "رمز عبور ها یکسان نیستند!"; FailedToConfirmAccountCreationText: string = "تایید ایجاد حساب کاربری ناموفق بود"; UserNotFoundText: string = "این کاربر وجود ندارد..."; - CreateNewUserText: string = "ایجاد کاربر جدید"; UserCreatedSuccessfullyText: string = "کاربر با موفقیت ایجاد شد"; TopicCreatedSuccessfullyText: string = "موضوع با موفقیت ایجاد شد"; NoResultsFoundText: string = "نتیجه ای یافت نشد، تلاش کنید تا جستجوی خود را تغییر دهید"; + SearchSomethingForTopicsText: string = "برای جستجو یک کلمه وارد کنید یا خالی بگذارید تا همه موضوعات لیست شوند"; EnterSearchForEdit: string = "برای ویرایش کاربر جستجو کنید"; //#endregion