diff --git a/.github/workflows/docs-publish.yaml b/.github/workflows/docs-publish.yaml index fbc0bf9d..ebb76832 100644 --- a/.github/workflows/docs-publish.yaml +++ b/.github/workflows/docs-publish.yaml @@ -5,7 +5,6 @@ on: push: branches: - main - - develop release: types: - published diff --git a/.github/workflows/frontend-docker.yaml b/.github/workflows/frontend-docker.yaml index 2f5ef671..2b44dc6f 100644 --- a/.github/workflows/frontend-docker.yaml +++ b/.github/workflows/frontend-docker.yaml @@ -37,7 +37,7 @@ jobs: --build-arg NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} \ --build-arg NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} \ --build-arg API_URL=https://eap-backend.azurewebsites.net/ \ - --build-arg NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }} \ + --build-arg NEXTAUTH_URL=${{ secrets.NEXT_PUBLIC_API_URL }} \ -t turingassuranceplatform/eap_frontend:main \ -t turingassuranceplatform/eap_frontend:${{ env.commit_date }}.${{ env.sha_short }} \ -f docker/staging/Dockerfile . diff --git a/eap_backend/eap_api/migrations/0024_auto_20240926_1231.py b/eap_backend/eap_api/migrations/0024_auto_20240926_1231.py new file mode 100644 index 00000000..cb4029c3 --- /dev/null +++ b/eap_backend/eap_api/migrations/0024_auto_20240926_1231.py @@ -0,0 +1,80 @@ +# Generated by Django 3.2.8 on 2024-09-26 12:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("eap_api", "0023_assurancecaseimage"), + ] + + operations = [ + migrations.AddField( + model_name="comment", + name="context", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.context", + ), + ), + migrations.AddField( + model_name="comment", + name="evidence", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.evidence", + ), + ), + migrations.AddField( + model_name="comment", + name="goal", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.toplevelnormativegoal", + ), + ), + migrations.AddField( + model_name="comment", + name="property_claim", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.propertyclaim", + ), + ), + migrations.AddField( + model_name="comment", + name="strategy", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.strategy", + ), + ), + migrations.AlterField( + model_name="comment", + name="assurance_case", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="eap_api.assurancecase", + ), + ), + ] diff --git a/eap_backend/eap_api/migrations/0025_alter_assurancecaseimage_assurance_case.py b/eap_backend/eap_api/migrations/0025_alter_assurancecaseimage_assurance_case.py new file mode 100644 index 00000000..2c48a5a0 --- /dev/null +++ b/eap_backend/eap_api/migrations/0025_alter_assurancecaseimage_assurance_case.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.8 on 2024-09-30 09:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("eap_api", "0024_auto_20240926_1231"), + ] + + operations = [ + migrations.AlterField( + model_name="assurancecaseimage", + name="assurance_case", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="case_image", + to="eap_api.assurancecase", + unique=True, + ), + ), + ] diff --git a/eap_backend/eap_api/models.py b/eap_backend/eap_api/models.py index 4c36222e..b7840234 100644 --- a/eap_backend/eap_api/models.py +++ b/eap_backend/eap_api/models.py @@ -100,23 +100,6 @@ def was_published_recently(self): return self.created_date >= timezone.now() - datetime.timedelta(days=1) -class Comment(models.Model): - author = models.ForeignKey( - EAPUser, related_name="comments", on_delete=models.CASCADE - ) - assurance_case = models.ForeignKey( - AssuranceCase, related_name="comments", on_delete=models.CASCADE - ) - content = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"Comment by {self.author} on {self.assurance_case}" - - class Meta: - ordering = ["created_at"] - - class TopLevelNormativeGoal(CaseItem): keywords = models.CharField(max_length=3000) assurance_case = models.ForeignKey( @@ -254,5 +237,64 @@ class AssuranceCaseImage(models.Model): AssuranceCase, related_name="case_image", on_delete=models.CASCADE, + unique=True, ) image = models.ImageField(upload_to="images/", default=None) + + +class Comment(models.Model): + author = models.ForeignKey( + EAPUser, related_name="comments", on_delete=models.CASCADE + ) + + assurance_case = models.ForeignKey( + AssuranceCase, + related_name="comments", + on_delete=models.CASCADE, + null=True, + default=None, + ) + goal = models.ForeignKey( + TopLevelNormativeGoal, + related_name="comments", + on_delete=models.CASCADE, + default=None, + null=True, + ) + strategy = models.ForeignKey( + Strategy, + related_name="comments", + on_delete=models.CASCADE, + default=None, + null=True, + ) + property_claim = models.ForeignKey( + PropertyClaim, + related_name="comments", + on_delete=models.CASCADE, + default=None, + null=True, + ) + evidence = models.ForeignKey( + Evidence, + related_name="comments", + on_delete=models.CASCADE, + default=None, + null=True, + ) + context = models.ForeignKey( + Context, + related_name="comments", + on_delete=models.CASCADE, + default=None, + null=True, + ) + + content = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Comment by {self.author} on {self.assurance_case}" + + class Meta: + ordering = ["created_at"] diff --git a/eap_backend/eap_api/serializers.py b/eap_backend/eap_api/serializers.py index 2cd39738..acf43435 100644 --- a/eap_backend/eap_api/serializers.py +++ b/eap_backend/eap_api/serializers.py @@ -131,6 +131,11 @@ class Meta: "id", "author", "assurance_case", + "goal", + "strategy", + "property_claim", + "evidence", + "context", "content", "created_at", ) @@ -391,6 +396,14 @@ class Meta: model = AssuranceCaseImage fields = ("id", "assurance_case_id", "image") + def create(self, validated_data: dict): + case_image, _ = AssuranceCaseImage.objects.update_or_create( + assurance_case=validated_data.get("assurance_case"), + defaults={"image": validated_data.get("image")}, + ) + + return case_image + class StrategySerializer(serializers.ModelSerializer): goal_id = serializers.PrimaryKeyRelatedField( diff --git a/eap_backend/eap_api/urls.py b/eap_backend/eap_api/urls.py index 55da8e13..84416376 100644 --- a/eap_backend/eap_api/urls.py +++ b/eap_backend/eap_api/urls.py @@ -52,7 +52,7 @@ name="attach_property_claim", ), path( - "cases//comments/", + "//comments/", views.comment_list, name="comment_list", ), diff --git a/eap_backend/eap_api/view_utils.py b/eap_backend/eap_api/view_utils.py index 8c563528..359d1a43 100644 --- a/eap_backend/eap_api/view_utils.py +++ b/eap_backend/eap_api/view_utils.py @@ -1,6 +1,7 @@ import functools -from typing import Any, Callable, Literal, Optional, Union, cast +from typing import Any, Callable, Literal, Optional, Type, Union, cast +from django.db import models from django.db.models.query import QuerySet from django.forms.models import model_to_dict from django.http import JsonResponse @@ -245,6 +246,34 @@ def _move_to_sandbox( case_item.save() +class CommentUtils: + @staticmethod + def get_model_instance( + element_name: str, element_id: int + ) -> CaseItem | AssuranceCase: + + model_class: Type[models.Model] | None = None + + if element_name == "cases": + model_class = AssuranceCase + elif element_name == "propertyclaims": + model_class = PropertyClaim + elif element_name == "goals": + model_class = TopLevelNormativeGoal + elif element_name == "strategies": + model_class = Strategy + elif element_name == "contexts": + model_class = Context + elif element_name == "evidence": + model_class = Evidence + + if model_class is None: + error_message: str = f"Invalid URL {element_name}/{element_id}" + raise ValueError(error_message) + + return model_class.objects.get(pk=element_id) + + class UpdateIdentifierUtils: @staticmethod def update_identifiers( @@ -773,7 +802,7 @@ def save_json_tree(data, obj_type, parent_id=None, parent_type=None): def get_case_permissions( - case: AssuranceCase, user: EAPUser + case: AssuranceCase | int | str, user: EAPUser ) -> Literal["manage"] | Literal["edit"] | Literal["review"] | Literal["view"] | None: """ See if the user is allowed to view or edit the case. diff --git a/eap_backend/eap_api/views.py b/eap_backend/eap_api/views.py index c9cbd2a4..a3cc6873 100644 --- a/eap_backend/eap_api/views.py +++ b/eap_backend/eap_api/views.py @@ -44,8 +44,10 @@ StrategySerializer, TopLevelNormativeGoalSerializer, UsernameAwareUserSerializer, + get_case_id, ) from .view_utils import ( + CommentUtils, SandboxUtils, ShareAssuranceCaseUtils, SocialAuthenticationUtils, @@ -252,7 +254,6 @@ def case_list(request): @api_view(["GET", "POST"]) @permission_classes([IsAuthenticated]) def case_image(request: HttpRequest, pk) -> Response: - print(f"{pk=}") if request.method == "GET": try: assurance_case: AssuranceCaseImage = AssuranceCaseImage.objects.get( @@ -904,24 +905,32 @@ def github_repository_list(request): @api_view(["GET", "POST"]) @permission_classes([IsAuthenticated]) -def comment_list(request, assurance_case_id): +def comment_list(request: HttpRequest, element_name: str, element_id: int): """ - List all comments for an assurance case, or create a new comment. + List all comments for an case element, or create a new comment. """ - permissions = get_case_permissions(assurance_case_id, request.user) - if permissions is None or permissions == "view": - return HttpResponse(status=403) + model_instance = CommentUtils.get_model_instance(element_name, element_id) + assurance_case_id: int | None = None + if isinstance(model_instance, AssuranceCase): + assurance_case_id = model_instance.pk + else: + assurance_case_id = get_case_id(model_instance) + + permissions: str | None = get_case_permissions( + cast(int, assurance_case_id), cast(EAPUser, request.user) + ) + + if permissions is None: + return HttpResponse(status=status.HTTP_403_FORBIDDEN) if request.method == "GET": - comments = Comment.objects.filter(assurance_case_id=assurance_case_id) - serializer = CommentSerializer(comments, many=True) + serializer = CommentSerializer(model_instance.comments, many=True) return Response(serializer.data) elif request.method == "POST": + if permissions not in ["manage", "edit", "review"]: + return HttpResponse(status=status.HTTP_403_FORBIDDEN) data = request.data.copy() - data["assurance_case_id"] = ( - assurance_case_id # Ensure assurance_case_id is set in the data - ) serializer = CommentSerializer(data=data) if serializer.is_valid(): # Ensure the author is set to the current user @@ -938,8 +947,9 @@ def comment_detail(request, pk): """ Retrieve, update or delete a specific comment. """ + try: - comment = Comment.objects.get(id=pk) + comment = Comment.objects.get(id=pk, author=request.user) except Comment.DoesNotExist: return HttpResponse(status=404) diff --git a/eap_backend/tests/test_views.py b/eap_backend/tests/test_views.py index 1e7bde19..2f824c37 100644 --- a/eap_backend/tests/test_views.py +++ b/eap_backend/tests/test_views.py @@ -1767,7 +1767,8 @@ def _check_status_on_edit(self, status_code: int): def _check_status_on_comment(self, status_code: int): response_post: HttpResponse = self.client.post( reverse( - "comment_list", kwargs={"assurance_case_id": self.assurance_case.pk} + "comment_list", + kwargs={"element_name": "cases", "element_id": self.assurance_case.pk}, ), data=json.dumps( {"content": "A comment", "assurance_case": self.assurance_case.pk} diff --git a/next_frontend/app/(main)/dashboard/page.tsx b/next_frontend/app/(main)/dashboard/page.tsx index cb4cda29..343bcc71 100644 --- a/next_frontend/app/(main)/dashboard/page.tsx +++ b/next_frontend/app/(main)/dashboard/page.tsx @@ -1,125 +1,8 @@ -// 'use client' - -// import CaseList from '@/components/cases/CaseList' -// import NoCasesFound from '@/components/cases/NoCasesFound' -// import { unauthorized, useLoginToken } from '@/hooks/useAuth' -// import { useEmailModal } from '@/hooks/useEmailModal' -// import { Loader2 } from 'lucide-react' -// import { useSession } from 'next-auth/react' -// import { useRouter } from 'next/navigation' -// import React, { useEffect, useState } from 'react' - -// const Dashboard = () => { -// const [assuranceCases, setAssuranceCases] = useState([]) -// const [loading, setLoading] = useState(true) -// const [tokenChecked, setTokenChecked] = useState(false) // New state to ensure token check is complete -// const [currentUser, setCurrentUser] = useState(null) - -// const [token] = useLoginToken(); -// const router = useRouter() -// const { data } = useSession() -// const emailModal = useEmailModal(); - -// const fetchAssuranceCases = async (token: any) => { -// try { -// const myHeaders = new Headers(); -// myHeaders.append("Content-Type", "application/json"); -// myHeaders.append("Authorization", `Token ${token}`); - -// const requestOptions: RequestInit = { -// method: 'GET', -// headers: myHeaders, -// redirect: 'follow' -// }; - -// const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL_STAGING}/api/cases?owner=true&view=false&edit=false&review=false`, requestOptions) - -// if(response.status === 401) { -// console.log('Invalid Token') -// localStorage.removeItem('token') -// router.push('login') -// return; -// } - -// const result = await response.json(); -// return result; -// } catch (error) { -// console.error("Failed to fetch assurance cases:", error); -// router.push('login'); -// } -// } - -// const fetchCurrentUser = async () => { -// const requestOptions: RequestInit = { -// headers: { -// Authorization: `Token ${token}`, -// }, -// }; - -// const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL_STAGING}/api/user/`, requestOptions); - -// if(response.status === 404 || response.status === 403 ) { -// // TODO: 404 NOT FOUND PAGE -// console.log('Render Not Found Page') -// return -// } - -// if(response.status === 401) return unauthorized() - -// const result = await response.json() -// return result -// } - -// useEffect(() => { -// const storedToken = token || localStorage.getItem('token'); - -// if(storedToken) { -// fetchAssuranceCases(storedToken).then(result => { -// setAssuranceCases(result) -// setLoading(false) -// }) -// } else { -// if (tokenChecked) { -// router.push('login'); -// } -// } - -// // Set token check to complete -// setTokenChecked(true); -// }, [token, router, tokenChecked]) - -// useEffect(() => { -// fetchCurrentUser().then(result => { -// setCurrentUser(result) -// if(!result.email) emailModal.onOpen() -// }) -// },[]) - -// return ( -// <> -// {loading ? ( -//
-//
-// -//

Fetching cases...

-//
-//
-// ) : ( -// <> -// {assuranceCases.length === 0 ? : } -// -// )} -// -// ) -// } - -// export default Dashboard - - 'use client' import CaseList from '@/components/cases/CaseList' import NoCasesFound from '@/components/cases/NoCasesFound' +import useStore from '@/data/store' import { unauthorized, useLoginToken } from '@/hooks/useAuth' import { useEmailModal } from '@/hooks/useEmailModal' import { Loader2 } from 'lucide-react' @@ -178,7 +61,7 @@ const Dashboard = () => { }, } - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL_STAGING}/api/user/`, requestOptions) + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/user/`, requestOptions) if (response.status === 404 || response.status === 403) { console.log('Render Not Found Page') @@ -221,12 +104,12 @@ const Dashboard = () => { }, [token, status, router, tokenChecked]) useEffect(() => { - if (status === 'authenticated' && tokenLoaded) { + // if (status === 'authenticated' && tokenLoaded) { fetchCurrentUser().then((result) => { setCurrentUser(result) if (!result?.email) emailModal.onOpen() }) - } + // } }, [status, tokenLoaded]) return ( diff --git a/next_frontend/app/api/screenshot/route.ts b/next_frontend/app/api/screenshot/route.ts index a51186f3..4facbeb3 100644 --- a/next_frontend/app/api/screenshot/route.ts +++ b/next_frontend/app/api/screenshot/route.ts @@ -1,70 +1,49 @@ -import fs from 'fs'; -import path from 'path'; -import { BlobServiceClient } from "@azure/storage-blob"; import { NextRequest, NextResponse } from 'next/server'; +type CaptureProps = { + base64image: string, + id: number, + token: string +} + export async function POST(request: NextRequest) { - const { base64, id } = await request.json() - const filename = `chart-screenshot-case-${id}.png` + const { base64image, id, token }: CaptureProps = await request.json(); + const filename = `chart-screenshot-case-${id}.png`; - const base64Data = base64.replace(/^data:image\/\w+;base64,/, '') - const buffer = Buffer.from(base64Data, 'base64') + // Convert base64 string to Blob if needed + const blob = base64ToBlob(base64image, 'image/png'); try { - const containerName = 'sample-container' - const account = process.env.NEXT_PUBLIC_STORAGESOURCENAME - - const blobSasUrl = 'https://teamedia.blob.core.windows.net/?sv=2022-11-02&ss=bfqt&srt=co&sp=rwdlacupiytfx&se=2025-05-06T03:42:08Z&st=2024-05-05T19:42:08Z&spr=https&sig=eAyqjGI6Tz5jzZi%2FWrVr%2BGfMnTR%2Fnbe8HLbDYuoVnMY%3D' - - const blobServiceClient = new BlobServiceClient(blobSasUrl) - - // Get a reference to a container - const containerClient = blobServiceClient.getContainerClient(containerName); - - const blockBlobClient = containerClient.getBlockBlobClient(filename); - await blockBlobClient.uploadData(buffer); - - // Return the URL of the uploaded image - const imageUrl = `https://${account}.blob.core.windows.net/${containerName}/${filename}`; - return NextResponse.json({ imageUrl }) - + const formdata = new FormData(); + formdata.append("media", blob, filename); + + const requestOptions: RequestInit = { + method: "POST", + body: formdata, + redirect: "follow", + headers: { + Authorization: `Token ${token}`, + }, + }; + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/cases/${id}/image`, requestOptions); + const { message, data } = await response.json(); + return NextResponse.json({ message, data }); } catch (error) { - console.log(error) - return NextResponse.json({ error, message: 'Couldnt upload image' }) + console.log(error); + return NextResponse.json({ error, message: "Couldn't upload image" }); } +} +// Utility function to convert base64 to Blob +function base64ToBlob(base64: string, mimeType: string) { + const byteString = atob(base64.split(",")[1]); // Decode base64 + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); - // try { - // // Remove header from base64 string - // const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, ''); - - // // Create buffer from base64 string - // const buffer = Buffer.from(base64Data, 'base64'); - - // // Save to azure - // const imageUrl = await saveToStorage(buffer, filename) - // return imageUrl - // } catch (error) { - // console.error('Error saving image:', error); - // throw error; - // } - - ////Read files from the file system using Node.js fs module - // const templatesDir = path.resolve(process.cwd(), 'caseTemplates'); - // const files = fs.readdirSync(templatesDir); - - // // Filter JSON files - // const jsonFiles = files.filter(file => file.endsWith('.json')); - - // // Read JSON content and parse it - // const newTemplates = jsonFiles.map(file => { - // const filePath = path.join(templatesDir, file); - // const content = fs.readFileSync(filePath, 'utf-8'); - // return JSON.parse(content); - // }); - - // // Find default case - // let defaultCase = newTemplates.find(c => c.name === 'empty') || newTemplates[0]; + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); + } - // return NextResponse.json({ newTemplates, defaultCase }) + return new Blob([uint8Array], { type: mimeType }); } diff --git a/next_frontend/components/Websocket.tsx b/next_frontend/components/Websocket.tsx index 98a317f4..cc2d1b7f 100644 --- a/next_frontend/components/Websocket.tsx +++ b/next_frontend/components/Websocket.tsx @@ -64,7 +64,7 @@ // clearInterval(interval); // }; // }, []); // Empty dependency array ensures the effect runs only once on mount - + // return ( //
@@ -112,29 +112,29 @@ const WebSocketComponent = () => { console.error("AssuranceCase or AssuranceCase ID is undefined, WebSocket cannot be established."); return; } - + let interval: any; const wsUrl = `wss://staging-eap-backend.azurewebsites.net/ws/case/${assuranceCase.id}/?token=${token}`; - + const setupWebSocket = () => { const websocket = new WebSocket(wsUrl); websocketRef.current = websocket; // Store the WebSocket instance in the ref - + websocket.addEventListener("open", (event: any) => { console.log("WebSocket connection established: ", event); const pingMessage = JSON.stringify({ content: "ping" }); - + // Send an initial ping message and start ping interval websocket.send(pingMessage); interval = setInterval(() => { websocket.send(pingMessage); }, pingInterval); }); - + websocket.addEventListener("message", (event: any) => { console.log("Message received from server: ", event); setMessages((prevMessages) => [...prevMessages, `Received "${event.data}" from server.`]); - + const data = JSON.parse(event.data); // Handle current connections update @@ -153,20 +153,20 @@ const WebSocketComponent = () => { console.log("Updated assurance case goals:", updatedGoals); } }); - + websocket.addEventListener("close", (event: any) => { console.log("WebSocket connection closed: ", event); clearInterval(interval); }); - + websocket.addEventListener("error", (event: any) => { console.error("WebSocket error occurred: ", event); }); }; - + // Initialize the WebSocket connection setupWebSocket(); - + // Cleanup function to close WebSocket and clear interval on unmount return () => { if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) { @@ -175,7 +175,7 @@ const WebSocketComponent = () => { clearInterval(interval); }; }, [assuranceCase?.id, token]); // Run effect when assuranceCase.id or token changes - + const prevAssuranceCaseString = usePrevious(JSON.stringify(assuranceCase)); @@ -209,4 +209,4 @@ const WebSocketComponent = () => { ); }; -export default WebSocketComponent; \ No newline at end of file +export default WebSocketComponent; diff --git a/next_frontend/components/auth/RegisterForm.tsx b/next_frontend/components/auth/RegisterForm.tsx index 427ff548..2eb34b9c 100644 --- a/next_frontend/components/auth/RegisterForm.tsx +++ b/next_frontend/components/auth/RegisterForm.tsx @@ -57,7 +57,7 @@ const RegisterForm = () => { password1: values.password1, password2: values.password2, }; - + const requestOptions: RequestInit = { method: "POST", headers: { @@ -65,7 +65,7 @@ const RegisterForm = () => { }, body: JSON.stringify(user), } - + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL_STAGING}/api/auth/register/`, requestOptions) console.log(response) @@ -73,9 +73,9 @@ const RegisterForm = () => { if(!response.ok || response.status === 400) { setErrors(['Invalid details, please try again.']) } - + const result = await response.json() - + if (result.key) { setToken(result.key); router.push('/dashboard') diff --git a/next_frontend/components/cases/ActionButtons.tsx b/next_frontend/components/cases/ActionButtons.tsx index 1e571215..485b21b7 100644 --- a/next_frontend/components/cases/ActionButtons.tsx +++ b/next_frontend/components/cases/ActionButtons.tsx @@ -117,7 +117,8 @@ const ActionButtons = ({ showCreateGoal, actions, notify, notifyError }: ActionB const newImage = JSON.stringify({ id: assuranceCase.id, - base64: base64image + base64image, + token }); const requestOptions: RequestInit = { @@ -128,15 +129,13 @@ const ActionButtons = ({ showCreateGoal, actions, notify, notifyError }: ActionB }; const response = await fetch("/api/screenshot", requestOptions) - const { imageUrl, error, message } = await response.json() + const { error, message, data } = await response.json() if(error) { notifyError(message) } - if(imageUrl) { - notify('Screenshot Saved!') - } + notify('Screenshot Saved!') } } diff --git a/next_frontend/components/cases/CaseCard.tsx b/next_frontend/components/cases/CaseCard.tsx index a2a73b2a..8e59c97a 100644 --- a/next_frontend/components/cases/CaseCard.tsx +++ b/next_frontend/components/cases/CaseCard.tsx @@ -30,7 +30,8 @@ const CaseCard = ({ assuranceCase } : CaseCardProps) => { const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) - const [imgSrc, setImgSrc] = useState(`https://teamedia.blob.core.windows.net/sample-container/chart-screenshot-case-${assuranceCase.id}.png`); + const [imgSrc, setImgSrc] = useState(''); + // const [imgSrc, setImgSrc] = useState(`https://teamedia.blob.core.windows.net/sample-container/chart-screenshot-case-${assuranceCase.id}.png`); // const [imageExists, setImageExists] = useState(true) // const [imageUrl, setImageUrl] = useState('') @@ -59,20 +60,47 @@ const CaseCard = ({ assuranceCase } : CaseCardProps) => { } } + const fetchScreenshot = async () => { + try { + const requestOptions: RequestInit = { + method: "GET", + headers: { + Authorization: `Token ${token}`, + }, + redirect: "follow" + }; + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/cases/${id}/image`, requestOptions) + + if(response.status == 404) { + setImgSrc('/images/assurance-case-medium.png') + return + } + + const result = await response.json() + setImgSrc(result.image) + } catch (error) { + console.log('Failed to fetch image') + } + } + + useEffect(() => { + fetchScreenshot() + }) + return (
- {`Assurance { - setImgSrc('/images/assurance-case-medium.png'); - }} - /> + {imgSrc && ( + {`Assurance + )}
{name} {description} diff --git a/next_frontend/components/cases/CaseList.tsx b/next_frontend/components/cases/CaseList.tsx index fcb121a0..fb7a2002 100644 --- a/next_frontend/components/cases/CaseList.tsx +++ b/next_frontend/components/cases/CaseList.tsx @@ -14,6 +14,7 @@ import { useCreateCaseModal } from '@/hooks/useCreateCaseModal' import { useImportModal } from '@/hooks/useImportModal' import { useShareModal } from '@/hooks/useShareModal' import { Input } from '../ui/input' +import useStore from '@/data/store' interface CaseListProps { assuranceCases: any[] diff --git a/next_frontend/components/cases/CommentForm.tsx b/next_frontend/components/cases/CommentForm.tsx new file mode 100644 index 00000000..68dd322d --- /dev/null +++ b/next_frontend/components/cases/CommentForm.tsx @@ -0,0 +1,128 @@ +import React, { Dispatch, SetStateAction, useState } from 'react' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { Textarea } from "../ui/textarea" +import { Button } from '../ui/button' +import useStore from '@/data/store'; +import { useLoginToken } from '@/hooks/useAuth' +import { addElementComment } from '@/lib/case-helper' + +const formSchema = z.object({ + comment: z.string().min(2, { + message: "Comment must be atleast 2 characters" + }) +}) + +interface CommentsFormProps { + node: any +}; + +const CommentsForm: React.FC = ({ node }: CommentsFormProps) => { + const { assuranceCase, setAssuranceCase, nodeComments, setNodeComments } = useStore(); + const [token] = useLoginToken(); + const [loading, setLoading] = useState(false) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + comment: '' + } + }); + + async function onSubmit(values: z.infer) { + console.log(values) + setLoading(true) + + let newComment = { + content: values.comment + } as any + + let entity = null; + switch (node.type) { + case "context": + entity = "contexts"; + newComment.context = node.data.id + break; + case "strategy": + entity = "strategies"; + newComment.strategy = node.data.id + break; + case "property": + entity = "propertyclaims"; + newComment.property_claim = node.data.id + break; + case "evidence": + entity = "evidence"; + newComment.evidence = node.data.id + break; + default: + entity = "goals"; + newComment.goal = node.data.id + break; + } + + try { + let url = `${process.env.NEXT_PUBLIC_API_URL}/api/${entity}/${node.data.id}/comments/`; + + const requestOptions: RequestInit = { + method: "POST", + headers: { + Authorization: `Token ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(newComment) + }; + + const response = await fetch(url, requestOptions); + const result = await response.json() + + // **Update the comments as an array** + const newCommentsList = [...nodeComments, result] + + setNodeComments(newCommentsList) + + // Clear form input + form.setValue('comment', '') + } catch (error) { + console.log('Error', error) + } finally { + setLoading(false) + } + } + + return ( +
+ + ( + + New Comment + +