diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index d3f1ad1..20e4725 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -16,4 +16,3 @@ jobs: environment: Production server_image_tag: "latest" client_image_tag: "latest" - \ No newline at end of file diff --git a/client/index.html b/client/index.html index 76db3ab..d2d0931 100644 --- a/client/index.html +++ b/client/index.html @@ -8,9 +8,9 @@ rel="stylesheet" /> - + - MA Mang + Thaii
diff --git a/client/public/robot.png b/client/public/robot.png new file mode 100644 index 0000000..5071dbc Binary files /dev/null and b/client/public/robot.png differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 6b9b9ac..8205c64 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,22 +4,22 @@ import PageLayout from "./components/layout/page-layout.component"; import Chatbot from "./components/chatbot/chatbot.component"; import Pages from "./components/pages/pages.component"; import Login from "./components/login/login.component"; -import ProtectedRoute from "./components/general/protected-route/protected-route.route"; +import ProtectedRoute from "./routes/protected-route/protected-route.route"; import Register from "./components/register/register.component"; import Activation from "./components/register/activation/activation.component"; import AdminTable from "./components/admin-table/admin-table.component"; import Statistics from "./components/statistics/statistics.component"; import NotFound from "./components/not-found/not-found.component"; import InactivityLogout from "./components/general/inactivity-logout/inactivity-logout.component"; -import AdminRoute from "./components/general/admin-route/admin-route.component"; import Documentation from "./components/documentation/documentation.component"; import { useAuthStore, useToolStore } from "./states/global.store"; import { getPagesForInsights } from "./services/pages.service"; import { getLabels } from "./services/label.service"; import { getTags } from "./services/tags.service"; -import { PageDTO, TagDTO } from "./components/pages/types/pages.types"; import { LabelDTO } from "./types/chatbot/chatbot.types"; import { QueryClient, QueryClientProvider } from "react-query"; +import AdminRoute from "./routes/admin-route/admin-route.component"; +import { PageDTO, TagDTO } from "./types/page/page.types"; function App() { const queryClient = new QueryClient() diff --git a/client/src/api/auth.api.ts b/client/src/api/auth.api.ts index c754f79..4119a02 100644 --- a/client/src/api/auth.api.ts +++ b/client/src/api/auth.api.ts @@ -5,6 +5,9 @@ const apiRefresh = axios.create({ baseURL: import.meta.env.VITE_API_URL as string, }); +// Authenticate a registered user into the system +// @user: username: string, password: string +// @response: token and refresh token export const loginUser = async (user: UserBody) => { try { const response = await apiRefresh.post("api/v1/token/", user); @@ -14,6 +17,9 @@ export const loginUser = async (user: UserBody) => { } }; +// Refresh expired token with the provided refresh token +// @refresh: JWT to refresh token +// @response: JWT to authenticate user export const refreshToken = async (refresh: string) => { try { const response = await apiRefresh.post("api/v1/token/refresh/", { @@ -25,6 +31,9 @@ export const refreshToken = async (refresh: string) => { } }; +// Register a new user with a username and a password. The username (email) must be whitelisted before registering +// @user: username: string, password: string +// @response: status 201 if created and an email is sent to the email export const registerUser = async (user: UserBody) => { try { const response = await apiRefresh.post("api/v1/user/register/", user); @@ -34,6 +43,8 @@ export const registerUser = async (user: UserBody) => { } }; +// Activate account by by clicking on a link +// @activation: uique query parameters to activate account of users export const activateUser = async (activation: ActivationBody) => { try { const response = await apiRefresh.post("api/v1/user/activate/", activation); diff --git a/client/src/api/insights.api.ts b/client/src/api/insights.api.ts index 07ad278..7108710 100644 --- a/client/src/api/insights.api.ts +++ b/client/src/api/insights.api.ts @@ -7,7 +7,7 @@ import api from "./interceptor.api"; // - labels: Labels included in analysis // - tags: Tags included in analysis -// Fetch number of total chats +// Fetches the total number of chats based on the provided filter criteria export const fetchTotalChats = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-chats/`, filter); @@ -17,7 +17,7 @@ export const fetchTotalChats = async (filter: FilterBody) => { } }; -// Fetch number of total messages +// Fetches the total number of messages based on the provided filter criteria export const fetchTotalMessages = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-messages/`, filter); @@ -27,6 +27,7 @@ export const fetchTotalMessages = async (filter: FilterBody) => { } }; +// Fetches chats and messages statistics by a specific time unit export const fetchChatsMessagesByTime = async ( filter: FilterBody, item: number @@ -42,6 +43,7 @@ export const fetchChatsMessagesByTime = async ( } }; +// Fetches chats and messages statistics by a specific item export const fetchChatsMessagesByItem = async ( filter: FilterBody, item: number @@ -57,6 +59,7 @@ export const fetchChatsMessagesByItem = async ( } }; +// Fetches the duration of conversations based on the provided filter criteria export const fetchConversationDuration = async (filter: FilterBody) => { try { const response = await api.post( @@ -69,6 +72,7 @@ export const fetchConversationDuration = async (filter: FilterBody) => { } }; +// Fetches conversation duration by specific items export const fetchConversationDurationByItem = async ( filter: FilterBody, item: number @@ -84,6 +88,7 @@ export const fetchConversationDurationByItem = async ( } }; +// Fetches the total emission statistics based on the filter criteria export const fetchTotalEmission = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-emission/`, filter); @@ -93,6 +98,7 @@ export const fetchTotalEmission = async (filter: FilterBody) => { } }; +// Fetches total water usage statistics based on the filter criteria export const fetchTotalWater = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-water/`, filter); @@ -102,6 +108,7 @@ export const fetchTotalWater = async (filter: FilterBody) => { } }; +// Fetches the total cost statistics based on the filter criteria export const fetchTotalCost = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/total-cost/`, filter); @@ -111,6 +118,7 @@ export const fetchTotalCost = async (filter: FilterBody) => { } }; +// Fetches tradeoff indicators by a time unit export const fetchTradeoffIndicatorsByTime = async ( filter: FilterBody, item: number @@ -126,6 +134,7 @@ export const fetchTradeoffIndicatorsByTime = async ( } }; +// Fetches tradeoff indicators by a specific item export const fetchTradeoffIndicatorsByItem = async ( filter: FilterBody, item: number @@ -141,6 +150,7 @@ export const fetchTradeoffIndicatorsByItem = async ( } }; +// Fetches keywords based on the provided filter criteria export const fetchKeywords = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/keywords/`, filter); @@ -150,6 +160,7 @@ export const fetchKeywords = async (filter: FilterBody) => { } }; +// Fetches common nouns from the API based on the filter criteria export const fetchCommonNouns = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/common-nouns/`, filter); @@ -159,6 +170,7 @@ export const fetchCommonNouns = async (filter: FilterBody) => { } }; +// Fetches common verbs from the API based on the filter criteria export const fetchCommonVerbs = async (filter: FilterBody) => { try { const response = await api.post(`/api/v1/insights/common-verbs/`, filter); @@ -168,6 +180,7 @@ export const fetchCommonVerbs = async (filter: FilterBody) => { } }; +// Fetches common adjectives from the API based on the filter criteria export const fetchCommonAdjectives = async (filter: FilterBody) => { try { const response = await api.post( diff --git a/client/src/api/interaction.api.ts b/client/src/api/interaction.api.ts index e7eed90..69e4c71 100644 --- a/client/src/api/interaction.api.ts +++ b/client/src/api/interaction.api.ts @@ -1,6 +1,9 @@ import { EventLogBody } from "../types/interaction/interaction.types"; import api from "./interceptor.api"; +// Creates a new event log by sending a POST request with the event log data +// @param eventlog - An object of type EventLogBody containing event log details +// @returns The response from the API if the event log is created successfully export const createEventLog = async (eventlog: EventLogBody) => { try { const response = await api.post("/api/v1/event-logs/", eventlog); @@ -11,6 +14,8 @@ export const createEventLog = async (eventlog: EventLogBody) => { } }; +// Fetches all event logs from the API as a blob (binary large object) +// @returns The response from the API containing the event logs in binary format export const fetchEventLogs = async () => { try { const response = await api.get(`/api/v1/event-logs/`, { diff --git a/client/src/api/interceptor.api.ts b/client/src/api/interceptor.api.ts index 0ebf928..51c1107 100644 --- a/client/src/api/interceptor.api.ts +++ b/client/src/api/interceptor.api.ts @@ -3,18 +3,23 @@ import { ACCESS_TOKEN } from "../helpers/auth.helpers"; import { isTokenExpired } from "../helpers/token.helpers"; import { refreshToken } from "./auth.api"; +// Creates an axios instance with the base URL set to the environment variable const api = axios.create({ baseURL: import.meta.env.VITE_API_URL as string, }); +// Interceptor for handling requests, particularly for token management api.interceptors.request.use( async (config) => { + // Retrieve the access token from local storage const token = localStorage.getItem(ACCESS_TOKEN); + // If a token exists, attach it to the request's Authorization header if (token) { config.headers.Authorization = `Bearer ${token}`; - + // Check if the token is expired if (isTokenExpired(token)) { const newToken = await refreshToken(token); + // Save the new token to local storage and update the Authorization header if (newToken) { localStorage.setItem(ACCESS_TOKEN, String(newToken)); config.headers.Authorization = `Bearer ${newToken}`; diff --git a/client/src/api/label.api.ts b/client/src/api/label.api.ts index af70b4a..3912cb2 100644 --- a/client/src/api/label.api.ts +++ b/client/src/api/label.api.ts @@ -1,6 +1,8 @@ import { LabelBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; +// Fetches all labels from the API +// @returns The response from the API containing the list of labels export const fetchLabels = async () => { try { const response = await api.get(`/api/v1/labels/`); @@ -11,12 +13,28 @@ export const fetchLabels = async () => { } }; +// Creates a new label by sending a POST request with the label data +// @param label - An object of type LabelBody containing label details +// @returns The response from the API if the label is created successfully export const createLabel = async (label: LabelBody) => { try { const response = await api.post("/api/v1/labels/", label); return response; } catch (error) { - console.error("Error creating user:", error); + console.error("Error creating label:", error); throw error; } }; + +// Delete existing label of user +// @id: id of label to delete +export const deleteLabel = async (id: number) => { + try { + const response = await api.delete(`/api/v1/labels/${id}/`); + return response; + } catch (error) { + console.error("Error deleting label:", error); + throw error; + } +}; + diff --git a/client/src/api/message.api.ts b/client/src/api/message.api.ts index d2ed7b7..6c44f52 100644 --- a/client/src/api/message.api.ts +++ b/client/src/api/message.api.ts @@ -1,6 +1,9 @@ import { MessageBody } from "../types/chatbot/chatbot.types"; import api from "./interceptor.api"; +// Fetches all messages associated with a specific chat by its ID +// @param chatId - The unique identifier of the chat for which messages are to be fetched +// @returns The response from the API containing the list of messages for the specified chat export const fetchMessagesByChatId = async (chatId: number) => { try { const response = await api.get(`/api/v1/messages/${chatId}/`); @@ -10,6 +13,10 @@ export const fetchMessagesByChatId = async (chatId: number) => { } }; +// Creates a new message in a specific chat by sending a POST request with the message data +// @param chatId - The unique identifier of the chat where the message will be created +// @param message - An object of type MessageBody containing message details (e.g., content, sender) +// @returns The response from the API if the message is created successfully export const createMessage = async (chatId: number, message: MessageBody) => { try { const response = await api.post(`/api/v1/messages/${chatId}/`, message); diff --git a/client/src/api/page.api.ts b/client/src/api/page.api.ts index 4be7476..3351f57 100644 --- a/client/src/api/page.api.ts +++ b/client/src/api/page.api.ts @@ -1,6 +1,8 @@ -import { PageBody } from "../components/pages/types/pages.types"; +import { PageBody } from "../types/page/page.types"; import api from "./interceptor.api"; +// Fetches all pages from the API +// @returns The response from the API containing the list of all pages export const fetchPages = async () => { try { const response = await api.get(`/api/v1/pages/`); @@ -10,6 +12,8 @@ export const fetchPages = async () => { } }; +// Fetches insight-related pages from the API +// @returns The response from the API containing the list of pages with insights export const fetchInsightPages = async () => { try { const response = await api.get(`/api/v1/pages/insights/`); @@ -19,6 +23,9 @@ export const fetchInsightPages = async () => { } }; +// Fetches a specific page by its ID from the API +// @param pageId - The unique identifier of the page to be fetched +// @returns The response from the API containing the details of the requested page export const fetchPageById = async (pageId: number) => { try { const response = await api.get(`/api/v1/pages/${pageId}/`); @@ -28,6 +35,9 @@ export const fetchPageById = async (pageId: number) => { } }; +// Creates a new page by sending a POST request with the page data +// @param page - An object of type PageBody containing page details +// @returns The response from the API if the page is created successfully export const createPage = async (page: PageBody) => { try { const response = await api.post("/api/v1/pages/", page); @@ -37,7 +47,10 @@ export const createPage = async (page: PageBody) => { } }; - +// Updates an existing page by sending a PUT request with the updated page data +// @param id - The unique identifier of the page to be updated +// @param page - An object of type PageBody containing updated page details +// @returns The response from the API if the page is updated successfully export const changePage = async (id: number, page: PageBody) => { try { const response = await api.put(`/api/v1/pages/${id}/`, page); @@ -47,6 +60,9 @@ export const changePage = async (id: number, page: PageBody) => { } }; +// Deletes a specific page by its ID from the API +// @param id - The unique identifier of the page to be deleted +// @returns The response from the API if the page is deleted successfully export const deletePage = async (id: number) => { try { const response = await api.delete(`/api/v1/pages/${id}/`); diff --git a/client/src/api/tag.api.ts b/client/src/api/tag.api.ts index 5fbb471..b886750 100644 --- a/client/src/api/tag.api.ts +++ b/client/src/api/tag.api.ts @@ -1,6 +1,8 @@ -import { TagBody } from "../components/pages/types/pages.types"; +import { TagBody } from "../types/page/page.types"; import api from "./interceptor.api"; +// Fetches all tags from the API +// @returns The response from the API containing the list of all tags export const fetchTags = async () => { try { const response = await api.get(`/api/v1/tags/`); @@ -11,12 +13,27 @@ export const fetchTags = async () => { } }; +// Creates a new tag by sending a POST request with the tag data +// @param tag - An object of type TagBody containing tag details +// @returns The response from the API if the tag is created successfully export const createTag = async (tag: TagBody) => { try { const response = await api.post("/api/v1/tags/", tag); return response; } catch (error) { - console.error("Error creating user:", error); + console.error("Error creating tag:", error); + throw error; + } +}; + +// Delete existing tag of user +// @id: id of tag to delete +export const deleteTag = async (id: number) => { + try { + const response = await api.delete(`/api/v1/tags/${id}/`); + return response; + } catch (error) { + console.error("Error deleting tag:", error); throw error; } }; diff --git a/client/src/api/user.api.ts b/client/src/api/user.api.ts index 6b97920..4b6faf9 100644 --- a/client/src/api/user.api.ts +++ b/client/src/api/user.api.ts @@ -1,5 +1,7 @@ import api from "./interceptor.api"; +// Fetches all users from the API +// @returns The response from the API containing the list of all users export const fetchUsers = async () => { try { const response = await api.get(`/api/v1/users/`); @@ -9,6 +11,11 @@ export const fetchUsers = async () => { } }; +// Updates a user's status and role (active and staff) by sending a PUT request +// @param id - The unique identifier of the user to be updated +// @param is_active - A boolean indicating whether the user account is activated +// @param is_staff - A boolean indicating whether the user is a staff member +// @returns The response from the API if the user is updated successfully export const changeUser = async (id: number, is_active: boolean, is_staff: boolean) => { try { const response = await api.put(`/api/v1/users/${id}/`, {is_active, is_staff}); @@ -18,6 +25,8 @@ export const changeUser = async (id: number, is_active: boolean, is_staff: boole } } +// Fetches the whitelist of emails from the API +// @returns The response from the API containing the whitelist of emails export const fetchWhitelist = async () => { try { const response = await api.get(`/api/v1/users/whitelist/`); @@ -27,6 +36,9 @@ export const fetchWhitelist = async () => { } }; +// Adds a new email to the whitelist by sending a POST request +// @param email - The email address to be added to the whitelist +// @returns The response from the API if the email is added successfully export const createWhitelistEmail = async (email: string) => { try { const response = await api.post(`/api/v1/users/whitelist/`, {email}); @@ -36,6 +48,9 @@ export const createWhitelistEmail = async (email: string) => { } }; +// Deletes an email from the whitelist by its ID +// @param id - The unique identifier of the email to be deleted from the whitelist +// @returns The response from the API if the email is deleted successfully export const deleteWhitelist = async (id: number) => { try { const response = await api.delete(`/api/v1/users/whitelist/${id}/`); @@ -45,6 +60,8 @@ export const deleteWhitelist = async (id: number) => { } }; +// Fetches the permissions of the current user from the API +// @returns The response from the API containing the user's permissions export const fetchUserPermission = async () => { try { const response = await api.get(`/api/v1/users/permissions/`); diff --git a/client/src/components/admin-table/admin-table.component.tsx b/client/src/components/admin-table/admin-table.component.tsx index f0e0116..8052c99 100644 --- a/client/src/components/admin-table/admin-table.component.tsx +++ b/client/src/components/admin-table/admin-table.component.tsx @@ -7,7 +7,7 @@ import { Typography, useTheme, } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import AdminTitle from "./admin-title/admin-title.component"; import { getUsers, getWhitelist } from "../../services/user.service"; import { UserDTO, WhitelistDTO } from "../../types/register/register.types"; diff --git a/client/src/components/chatbot/chatbot.component.tsx b/client/src/components/chatbot/chatbot.component.tsx index caf9d03..8067f8a 100644 --- a/client/src/components/chatbot/chatbot.component.tsx +++ b/client/src/components/chatbot/chatbot.component.tsx @@ -9,7 +9,7 @@ import { IconButton, useTheme, } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import "../layout/styles/layout.styles.css"; import { ArrowUpCircle, Edit } from "react-feather"; import { lazy, Suspense, useEffect, useState } from "react"; @@ -21,7 +21,7 @@ import LoadingComponent from "../general/loading-component/loading.component"; import { useToolStore } from "../../states/global.store"; import { getPages } from "../../services/pages.service"; import "./styles/chatbot.styles.css"; -import EmptyChat from "../general/empty-chat/empty-chat.component"; +import EmptyChat from "./empty-chat/empty-chat.component"; import { ErrorBoundary } from "react-error-boundary"; import ErrorBoundaryFallback from "../general/error-boundary/error-boundary.component"; import CreationDialog from "../general/create-dialog/creation-dialog.component"; diff --git a/client/src/components/general/empty-chat/empty-chat.component.tsx b/client/src/components/chatbot/empty-chat/empty-chat.component.tsx similarity index 100% rename from client/src/components/general/empty-chat/empty-chat.component.tsx rename to client/src/components/chatbot/empty-chat/empty-chat.component.tsx diff --git a/client/src/components/documentation/documentation.component.tsx b/client/src/components/documentation/documentation.component.tsx index 675d435..4a1b9b4 100644 --- a/client/src/components/documentation/documentation.component.tsx +++ b/client/src/components/documentation/documentation.component.tsx @@ -1,5 +1,5 @@ import { Box, Typography } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; function Documentation({ open }: SidebarParams) { return ( diff --git a/client/src/components/general/footer/footer.component.tsx b/client/src/components/footer/footer.component.tsx similarity index 100% rename from client/src/components/general/footer/footer.component.tsx rename to client/src/components/footer/footer.component.tsx diff --git a/client/src/components/general/footer/styles/footer.styles.css b/client/src/components/footer/styles/footer.styles.css similarity index 100% rename from client/src/components/general/footer/styles/footer.styles.css rename to client/src/components/footer/styles/footer.styles.css diff --git a/client/src/components/general/anonymous-route/anonymous-route.component.tsx b/client/src/components/general/anonymous-route/anonymous-route.component.tsx deleted file mode 100644 index df49849..0000000 --- a/client/src/components/general/anonymous-route/anonymous-route.component.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Navigate, Outlet } from "react-router-dom"; -import { ACCESS_TOKEN } from "../../../helpers/auth.helpers"; - -function AnonymousRoute() { - const token = localStorage.getItem(ACCESS_TOKEN); - return token ? : ; -} - -export default AnonymousRoute; \ No newline at end of file diff --git a/client/src/components/general/auth-provider/auth-provider.component.tsx b/client/src/components/general/auth-provider/auth-provider.component.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/components/general/create-dialog/creation-dialog.component.tsx b/client/src/components/general/create-dialog/creation-dialog.component.tsx index e083633..61bf35d 100644 --- a/client/src/components/general/create-dialog/creation-dialog.component.tsx +++ b/client/src/components/general/create-dialog/creation-dialog.component.tsx @@ -15,10 +15,9 @@ import { Typography, useTheme, } from "@mui/material"; -import { CreationDialogParams } from "./types/creation-dialog.types"; +import { CreationDialogParams } from "../../../types/create-dialog/creation-dialog.types"; import { PlusCircle, X } from "react-feather"; import React, { lazy, Suspense, useEffect, useState } from "react"; -import { TagDTO } from "../../pages/types/pages.types"; import { getTags } from "../../../services/tags.service"; import { useToolStore } from "../../../states/global.store"; import { LabelDTO } from "../../../types/chatbot/chatbot.types"; @@ -28,6 +27,7 @@ import { getLabels } from "../../../services/label.service"; import LoadingComponent from "../loading-component/loading.component"; import { ErrorBoundary } from "react-error-boundary"; import ErrorBoundaryFallback from "../error-boundary/error-boundary.component"; +import { TagDTO } from "../../../types/page/page.types"; const PageTreeView = lazy( () => import("../../pages/page-tree-view/page-tree-view.component") ); @@ -323,6 +323,8 @@ function CreationDialog({ setCurrentElements={setCurrentElements} selectedTagsOrLabels={selectedTagOrLabel} setSelectedTagsOrLabels={setSelectedTagOrLabel} + fetchTagOrLabelData={fetchTagOrLabelData} + source={source} /> diff --git a/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx b/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx index b21ccec..31f538f 100644 --- a/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-auto-complete/tag-label-auto-complete.component.tsx @@ -2,14 +2,20 @@ import { Autocomplete, Checkbox, Chip, + Grid, + IconButton, TextField, useTheme, } from "@mui/material"; -import { CheckSquare, Square } from "react-feather"; -import { SyntheticEvent } from "react"; -import { TagDTO } from "../../../pages/types/pages.types"; -import { TagLabelListParams } from "../types/creation-dialog.types"; +import { CheckSquare, Square, Trash } from "react-feather"; +import { SyntheticEvent, useState } from "react"; +import { TagLabelListParams } from "../../../../types/create-dialog/creation-dialog.types"; import { LabelDTO } from "../../../../types/chatbot/chatbot.types"; +import { TagDTO } from "../../../../types/page/page.types"; +import CustomSnackbar from "../../snackbar/snackbar.component"; +import LoadingComponent from "../../loading-component/loading.component"; +import { removeTag } from "../../../../services/tags.service"; +import { removeLabel } from "../../../../services/label.service"; function TagLabelAutoComplete({ elements, @@ -17,8 +23,31 @@ function TagLabelAutoComplete({ setCurrentElements, selectedTagsOrLabels, setSelectedTagsOrLabels, + fetchTagOrLabelData, + source, }: TagLabelListParams) { const theme = useTheme(); + const [snackbar, setSnackbar] = useState({ + message: "", + type: "", + open: false, + }); + const [loading, setLoading] = useState(false); + + const handleClose = ( + _event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === "clickaway") { + return; + } + + setSnackbar({ + message: snackbar.message, + type: snackbar.type, + open: false, + }); + }; const handleChange = (_event: SyntheticEvent, value: any) => { setSelectedTagsOrLabels( @@ -27,61 +56,144 @@ function TagLabelAutoComplete({ setCurrentElements(value); }; - return ( - option.label} - renderTags={(value, getTagProps) => - value.map((option, index) => { - const { key, ...tagProps } = getTagProps({ index }); - return ( - - ); + const handleDeleteElement = async (id: number) => { + setLoading(true); + if (source == "chat") { + removeLabel(id) + .then(() => { + setSnackbar({ + message: "Label successfully deleted.", + type: "success", + open: true, + }); + }) + .then(() => {}) + .catch(() => { + setSnackbar({ + message: "Error: Label could not be deleted.", + type: "error", + open: true, + }); + }) + .finally(() => { + fetchTagOrLabelData(); + setLoading(false); + }); + } else { + removeTag(id) + .then(() => { + setSnackbar({ + message: "Tag successfully deleted.", + type: "success", + open: true, + }); }) - } - renderOption={(props, option, { selected }) => ( -
  • - } - checkedIcon={} - style={{ marginRight: 8 }} - checked={selected || selectedTagsOrLabels.includes(option.id)} + .then(() => {}) + .catch(() => { + setSnackbar({ + message: "Error: Tag could not be deleted.", + type: "error", + open: true, + }); + }) + .finally(() => { + fetchTagOrLabelData(); + setLoading(false); + }); + } + }; + + if (loading) { + return ; + } + + return ( + <> + option.label} + renderTags={(value, getTagProps) => + value.map((option, index) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderOption={(props, option, { selected }) => ( +
  • + + + } + checkedIcon={} + style={{ marginRight: 8 }} + checked={selected || selectedTagsOrLabels.includes(option.id)} + /> + {option.label} + + + handleDeleteElement(option.id)} + > + + + + +
  • + )} + renderInput={(params) => ( + - {option.label} - - )} - renderInput={(params) => ( - - )} - /> + )} + /> + + ); } diff --git a/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx b/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx index f6fe8ee..0f747b9 100644 --- a/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-dialog/color-selector/color-selector.component.tsx @@ -1,5 +1,5 @@ import { Avatar, Stack } from "@mui/material"; -import { ColorSelectorParams } from "../../types/creation-dialog.types"; +import { ColorSelectorParams } from "../../../../../types/create-dialog/creation-dialog.types"; import { Check, Circle } from "react-feather"; const colors = [ diff --git a/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx b/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx index 6ff3811..1b7e2d6 100644 --- a/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx +++ b/client/src/components/general/create-dialog/tag-label-dialog/tag-label-dialog.component.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { TagLabelDialogParams } from "../types/creation-dialog.types"; +import { TagLabelDialogParams } from "../../../../types/create-dialog/creation-dialog.types"; import { addTag } from "../../../../services/tags.service"; import { TagBody } from "../../../pages/types/pages.types"; import { diff --git a/client/src/components/general/snackbar/snackbar.component.tsx b/client/src/components/general/snackbar/snackbar.component.tsx index 3c45a4b..af85bd3 100644 --- a/client/src/components/general/snackbar/snackbar.component.tsx +++ b/client/src/components/general/snackbar/snackbar.component.tsx @@ -1,5 +1,5 @@ import { Alert, Snackbar } from "@mui/material"; -import { SnackbarParams } from "./types/snackbar.types"; +import { SnackbarParams } from "../../../types/general/general.types"; function CustomSnackbar({ message, type, open, handleClose }: SnackbarParams) { diff --git a/client/src/components/general/snackbar/types/snackbar.types.ts b/client/src/components/general/snackbar/types/snackbar.types.ts deleted file mode 100644 index 0b3609d..0000000 --- a/client/src/components/general/snackbar/types/snackbar.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type SnackbarParams = { - message: string; - type: string; - open: boolean; - handleClose: (event: React.SyntheticEvent | Event, reason?: string) => void; -}; diff --git a/client/src/components/layout/page-layout.component.tsx b/client/src/components/layout/page-layout.component.tsx index eaf1339..a61421a 100644 --- a/client/src/components/layout/page-layout.component.tsx +++ b/client/src/components/layout/page-layout.component.tsx @@ -1,8 +1,8 @@ import { Grid } from "@mui/material"; import Sidebar from "../sidebar/sidebar.component"; import { Outlet } from "react-router-dom"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; -import Footer from "../general/footer/footer.component"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; +import Footer from "../footer/footer.component"; function PageLayout({ open, setOpen }: SidebarParams) { return ( diff --git a/client/src/components/pages/page-tree-view/page-tree-view.component.tsx b/client/src/components/pages/page-tree-view/page-tree-view.component.tsx index d1b51c4..084defc 100644 --- a/client/src/components/pages/page-tree-view/page-tree-view.component.tsx +++ b/client/src/components/pages/page-tree-view/page-tree-view.component.tsx @@ -8,8 +8,8 @@ import { useTreeViewApiRef } from "@mui/x-tree-view/hooks"; import { SyntheticEvent, useEffect, useState } from "react"; import { useToolStore } from "../../../states/global.store"; import { Alert, Typography } from "@mui/material"; -import { PageDTO } from "../types/pages.types"; import { getPageById } from "../../../services/pages.service"; +import { PageDTO } from "../../../types/page/page.types"; function PageTreeView({ currentPageId, diff --git a/client/src/components/pages/pages.component.tsx b/client/src/components/pages/pages.component.tsx index 97453ed..678338a 100644 --- a/client/src/components/pages/pages.component.tsx +++ b/client/src/components/pages/pages.component.tsx @@ -1,17 +1,17 @@ import { Button, Grid, Typography } from "@mui/material"; -import { SidebarParams } from "../sidebar/types/sidebar.types"; +import { SidebarParams } from "../../types/sidebar/sidebar.types"; import { Edit } from "react-feather"; import { lazy, Suspense, useEffect, useState } from "react"; import CustomSnackbar from "../general/snackbar/snackbar.component"; import { useToolStore } from "../../states/global.store"; -import { PageDTO } from "./types/pages.types"; import { addPage, getPageById, getPages } from "../../services/pages.service"; import CreationDialog from "../general/create-dialog/creation-dialog.component"; import LoadingComponent from "../general/loading-component/loading.component"; import { getChatsByPageId } from "../../services/chats.service"; import { ChatDTO } from "../../types/chatbot/chatbot.types"; import { useQuery } from "react-query"; -import EmptyChat from "../general/empty-chat/empty-chat.component"; +import EmptyChat from "../chatbot/empty-chat/empty-chat.component"; +import { PageDTO } from "../../types/page/page.types"; const PageTitle = lazy(() => import("./page-title/page-title.component")); const ChatTable = lazy(() => import("./chat-table/chat-table.component")); const PageTreeView = lazy( diff --git a/client/src/components/pages/types/pages.types.ts b/client/src/components/pages/types/pages.types.ts deleted file mode 100644 index 444392c..0000000 --- a/client/src/components/pages/types/pages.types.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; - -// DTOs -export type PageDTO = { - id: string; - label: string; - children: string[]; - parent_page: number | null; - level: number; - read_only: boolean; - tags: TagDTO[]; -}; - -export type TagDTO = { - id: string; - label: string; - color: string; -}; - -// Request Bodies -export type PageBody = { - label: string; - parent_page_id: number | null; - tags: string[]; -}; - -export type TagBody = { - label: string; - color: string; -}; - -// List Elements -export type TagList = { - tags: TagDTO[]; - selectedTags: TagDTO[]; - setSelectedTags: Dispatch>; -}; - -// Function Parameter Definitions -export type PageDialogParams = { - open: boolean; - setOpen: (open: boolean) => void; - label: string; - setLabel: (title: string) => void; - parentId: number; - setParentId: (parentId: number) => void; - createPage?: (e: any) => void; - editPage?: (page: PageDTO) => void; - deletePage?: (id: number) => void; - pages: any; - type: string; -}; - -export type PopperParams = { - open: boolean; - setOpen: (open: boolean) => void; - anchor: HTMLButtonElement | null; - fetchTags: () => Promise; - hasValue: (tag_name: string) => boolean; -}; - -export type ColorSelectorParams = { - color: string; - setColor: (color: string) => void; -}; - -export type TreeViewParams = { - currentPageId: string; - setCurrentPageId: (currentPageId: string) => void; - isNewPage: boolean; -}; - -export type PageTitleParams = { - currentPage: PageDTO; - fetchPagesData: () => void; - fetchPageById: (id: number) => void; - setSnackbar: (snackbar: { - message: string; - type: string; - open: boolean; - }) => void; -}; diff --git a/client/src/components/sidebar/download-button/download-button.component.tsx b/client/src/components/sidebar/download-button/download-button.component.tsx index 3ee9547..8ba5b28 100644 --- a/client/src/components/sidebar/download-button/download-button.component.tsx +++ b/client/src/components/sidebar/download-button/download-button.component.tsx @@ -1,8 +1,15 @@ -import { Button, Typography, useTheme } from "@mui/material"; +import { + Button, + IconButton, + Tooltip, + Typography, + useTheme, +} from "@mui/material"; import { getEventLogs } from "../../../services/interactions.service"; import { Download } from "react-feather"; +import { DownloadButtonParams } from "../../../types/sidebar/sidebar.types"; -const DownloadButton = () => { +const DownloadButton = ({ open }: DownloadButtonParams) => { const theme = useTheme(); const handleDownload = async () => { @@ -25,15 +32,25 @@ const DownloadButton = () => { } }; + if (open) { + return ( + + ); + } + return ( - + + + + + ); }; diff --git a/client/src/components/sidebar/sidebar.component.tsx b/client/src/components/sidebar/sidebar.component.tsx index 72fb5d3..f274c62 100644 --- a/client/src/components/sidebar/sidebar.component.tsx +++ b/client/src/components/sidebar/sidebar.component.tsx @@ -9,7 +9,7 @@ import { } from "@mui/material"; import WavingHandIcon from "@mui/icons-material/WavingHand"; import { menu_items } from "./helpers/sidebar.helpers"; -import { MenuItem, SidebarParams } from "./types/sidebar.types"; +import { MenuItem, SidebarParams } from "../../types/sidebar/sidebar.types"; import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; import { Sidebar as SidebarIcon, @@ -160,13 +160,11 @@ function Sidebar({ open, setOpen }: SidebarParams) { xs={12} sx={{ display: "flex", - justifyContent: "flex-start", - ml: "1rem", - mr: "1rem", + justifyContent: "center", mt: "3vh", }} > - + import("./filter/filter.component")); const BehavioralDashboard = lazy( @@ -123,7 +123,9 @@ function Statistics({ open }: SidebarParams) { className="main tabs" onClick={() => { setTab(tabItem.tab); - if ((import.meta.env.VITE_ENABLE_TRACKING as string) == "true") { + if ( + (import.meta.env.VITE_ENABLE_TRACKING as string) == "true" + ) { addEventLog({ location: "Insights - " + tabItem.name, }); diff --git a/client/src/components/general/admin-route/admin-route.component.tsx b/client/src/routes/admin-route/admin-route.component.tsx similarity index 79% rename from client/src/components/general/admin-route/admin-route.component.tsx rename to client/src/routes/admin-route/admin-route.component.tsx index 16f2bbf..01e2320 100644 --- a/client/src/components/general/admin-route/admin-route.component.tsx +++ b/client/src/routes/admin-route/admin-route.component.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { getUserPermissions } from "../../../services/user.service"; -import { ACCESS_TOKEN } from "../../../helpers/auth.helpers"; import { Navigate, Outlet } from "react-router-dom"; +import { ACCESS_TOKEN } from "../../helpers/auth.helpers"; +import { getUserPermissions } from "../../services/user.service"; function AdminRoute() { const [isAdmin, setIsAdmin] = useState(null); diff --git a/client/src/components/general/protected-route/protected-route.route.tsx b/client/src/routes/protected-route/protected-route.route.tsx similarity index 88% rename from client/src/components/general/protected-route/protected-route.route.tsx rename to client/src/routes/protected-route/protected-route.route.tsx index 2b2991f..f00e397 100644 --- a/client/src/components/general/protected-route/protected-route.route.tsx +++ b/client/src/routes/protected-route/protected-route.route.tsx @@ -1,9 +1,9 @@ import { Navigate, Outlet } from "react-router-dom"; import { jwtDecode } from "jwt-decode"; -import { REFRESH_TOKEN, ACCESS_TOKEN } from "../../../helpers/auth.helpers"; +import { REFRESH_TOKEN, ACCESS_TOKEN } from "../../helpers/auth.helpers"; import { useState, useEffect } from "react"; -import { refreshToken } from "../../../api/auth.api"; -import { useAuthStore } from "../../../states/global.store"; +import { refreshToken } from "../../api/auth.api"; +import { useAuthStore } from "../../states/global.store"; function ProtectedRoute() { const [loading, setLoading] = useState(true); diff --git a/client/src/services/label.service.ts b/client/src/services/label.service.ts index 6896a89..411698a 100644 --- a/client/src/services/label.service.ts +++ b/client/src/services/label.service.ts @@ -1,4 +1,4 @@ -import { createLabel, fetchLabels } from "../api/label.api"; +import { createLabel, deleteLabel, fetchLabels } from "../api/label.api"; import { LabelBody } from "../types/chatbot/chatbot.types"; export const getLabels = async () => { @@ -18,3 +18,13 @@ export const addLabel = async (label: LabelBody) => { throw error; } }; + + +export const removeLabel = async (id: number) => { + try { + const response = await deleteLabel(id) + return response.data; + } catch (error: any) { + throw error; + } +}; diff --git a/client/src/services/tags.service.ts b/client/src/services/tags.service.ts index 7764021..6ed63db 100644 --- a/client/src/services/tags.service.ts +++ b/client/src/services/tags.service.ts @@ -1,5 +1,5 @@ -import { createTag, fetchTags } from "../api/tag.api"; -import { TagBody } from "../components/pages/types/pages.types"; +import { createTag, deleteTag, fetchTags } from "../api/tag.api"; +import { TagBody } from "../types/page/page.types"; export const getTags = async () => { try { @@ -18,3 +18,12 @@ export const addTag = async (tag: TagBody) => { throw error; } }; + +export const removeTag = async (id: number) => { + try { + const response = await deleteTag(id) + return response.data; + } catch (error: any) { + throw error; + } +}; diff --git a/client/src/types/chatbot/chatbot.types.ts b/client/src/types/chatbot/chatbot.types.ts index 974dc46..244641a 100644 --- a/client/src/types/chatbot/chatbot.types.ts +++ b/client/src/types/chatbot/chatbot.types.ts @@ -1,4 +1,4 @@ -import { PageDTO } from "../../components/pages/types/pages.types"; +import { PageDTO } from "../page/page.types"; export type LabelDTO = { id: string; diff --git a/client/src/components/general/create-dialog/types/creation-dialog.types.ts b/client/src/types/create-dialog/creation-dialog.types.ts similarity index 87% rename from client/src/components/general/create-dialog/types/creation-dialog.types.ts rename to client/src/types/create-dialog/creation-dialog.types.ts index fdaea5f..6d393e6 100644 --- a/client/src/components/general/create-dialog/types/creation-dialog.types.ts +++ b/client/src/types/create-dialog/creation-dialog.types.ts @@ -1,5 +1,5 @@ -import { LabelDTO } from "../../../../types/chatbot/chatbot.types"; -import { TagDTO } from "../../../pages/types/pages.types"; +import { LabelDTO } from "../chatbot/chatbot.types"; +import { TagDTO } from "../page/page.types"; export type CreationDialogParams = { open: boolean; @@ -25,6 +25,8 @@ export type TagLabelListParams = { setCurrentElements: (currentElement: TagDTO[] | LabelDTO[]) => void; selectedTagsOrLabels: string[]; setSelectedTagsOrLabels: (selectedTagOrLabel: string[]) => void; + fetchTagOrLabelData: () => void; + source: string; }; export type TagLabelDialogParams = { diff --git a/client/src/types/general/general.types.ts b/client/src/types/general/general.types.ts index 64ff67e..2d6f5af 100644 --- a/client/src/types/general/general.types.ts +++ b/client/src/types/general/general.types.ts @@ -1,4 +1,12 @@ export type InputError = { error: boolean; errorMessage: string; -} \ No newline at end of file +} + +export type SnackbarParams = { + message: string; + type: string; + open: boolean; + handleClose: (event: React.SyntheticEvent | Event, reason?: string) => void; + }; + \ No newline at end of file diff --git a/client/src/types/page/page.types.ts b/client/src/types/page/page.types.ts index e69de29..444392c 100644 --- a/client/src/types/page/page.types.ts +++ b/client/src/types/page/page.types.ts @@ -0,0 +1,82 @@ +import { Dispatch, SetStateAction } from "react"; + +// DTOs +export type PageDTO = { + id: string; + label: string; + children: string[]; + parent_page: number | null; + level: number; + read_only: boolean; + tags: TagDTO[]; +}; + +export type TagDTO = { + id: string; + label: string; + color: string; +}; + +// Request Bodies +export type PageBody = { + label: string; + parent_page_id: number | null; + tags: string[]; +}; + +export type TagBody = { + label: string; + color: string; +}; + +// List Elements +export type TagList = { + tags: TagDTO[]; + selectedTags: TagDTO[]; + setSelectedTags: Dispatch>; +}; + +// Function Parameter Definitions +export type PageDialogParams = { + open: boolean; + setOpen: (open: boolean) => void; + label: string; + setLabel: (title: string) => void; + parentId: number; + setParentId: (parentId: number) => void; + createPage?: (e: any) => void; + editPage?: (page: PageDTO) => void; + deletePage?: (id: number) => void; + pages: any; + type: string; +}; + +export type PopperParams = { + open: boolean; + setOpen: (open: boolean) => void; + anchor: HTMLButtonElement | null; + fetchTags: () => Promise; + hasValue: (tag_name: string) => boolean; +}; + +export type ColorSelectorParams = { + color: string; + setColor: (color: string) => void; +}; + +export type TreeViewParams = { + currentPageId: string; + setCurrentPageId: (currentPageId: string) => void; + isNewPage: boolean; +}; + +export type PageTitleParams = { + currentPage: PageDTO; + fetchPagesData: () => void; + fetchPageById: (id: number) => void; + setSnackbar: (snackbar: { + message: string; + type: string; + open: boolean; + }) => void; +}; diff --git a/client/src/components/sidebar/types/sidebar.types.ts b/client/src/types/sidebar/sidebar.types.ts similarity index 80% rename from client/src/components/sidebar/types/sidebar.types.ts rename to client/src/types/sidebar/sidebar.types.ts index 37832ec..341c6d4 100644 --- a/client/src/components/sidebar/types/sidebar.types.ts +++ b/client/src/types/sidebar/sidebar.types.ts @@ -14,3 +14,6 @@ export type StatisticParams = { messages: any } +export type DownloadButtonParams = { + open: boolean; +} diff --git a/server/chat/urls.py b/server/chat/urls.py index 51fa9f4..0937c92 100644 --- a/server/chat/urls.py +++ b/server/chat/urls.py @@ -15,5 +15,6 @@ name="chats", ), path("messages//", views.MessageDetailView.as_view(), name="message"), - path("labels/", views.LabelApiView.as_view(), name="label-create") + path("labels/", views.LabelApiView.as_view(), name="label-create"), + path("labels//", views.LabelApiView.as_view(), name="label-delete") ] \ No newline at end of file diff --git a/server/chat/views.py b/server/chat/views.py index 5c0a51f..3c830c0 100644 --- a/server/chat/views.py +++ b/server/chat/views.py @@ -131,6 +131,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, pk, format=None): try: label = Label.objects.get(id=pk) + label.chats.clear() except Label.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) label.delete() diff --git a/server/insights/services/arrayMerge.py b/server/insights/services/arrayMerge.py index 49ccb8d..0430d5a 100644 --- a/server/insights/services/arrayMerge.py +++ b/server/insights/services/arrayMerge.py @@ -1,3 +1,4 @@ + def mergeByDate(array1, array2, key): combined_dict = {} for obj in array1: diff --git a/server/pages/urls.py b/server/pages/urls.py index 326fecf..de0edee 100644 --- a/server/pages/urls.py +++ b/server/pages/urls.py @@ -9,5 +9,6 @@ ), path("pages/", views.PageListView.as_view(), name="page-list"), path("pages/insights/", views.PageListFilterView.as_view(), name="page-list"), - path("tags/", views.TagApiView.as_view(), name="tag-create") + path("tags/", views.TagApiView.as_view(), name="tag-create"), + path("tags//", views.TagApiView.as_view(), name="tag-delete") ] \ No newline at end of file diff --git a/server/pages/views.py b/server/pages/views.py index c152ca0..6920420 100644 --- a/server/pages/views.py +++ b/server/pages/views.py @@ -116,6 +116,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, pk, format=None): try: tag = Tag.objects.get(id=pk) + tag.pages.clear() except Tag.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) tag.delete()