From e9f12abb26efb490599b74c060cd77379b47b3a6 Mon Sep 17 00:00:00 2001 From: Orka Arnest CRUZE Date: Tue, 21 Jan 2025 18:15:06 +0100 Subject: [PATCH] feat: ajout routes documents personnels --- .../pages/users/documents/MyDocuments.tsx | 158 ++++++++++++++++++ assets/modules/entrepot/RQKeys.ts | 8 +- assets/router/RouterRenderer.tsx | 3 + assets/router/router.ts | 1 + src/Controller/Entrepot/UserController.php | 2 +- .../Entrepot/UserDocumentsController.php | 154 +++++++++++++++++ src/Services/EntrepotApi/AnnexeApiService.php | 4 +- .../EntrepotApi/UserDocumentsApiService.php | 113 +++++++++++++ 8 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 assets/entrepot/pages/users/documents/MyDocuments.tsx create mode 100644 src/Controller/Entrepot/UserDocumentsController.php create mode 100644 src/Services/EntrepotApi/UserDocumentsApiService.php diff --git a/assets/entrepot/pages/users/documents/MyDocuments.tsx b/assets/entrepot/pages/users/documents/MyDocuments.tsx new file mode 100644 index 00000000..fb8c5445 --- /dev/null +++ b/assets/entrepot/pages/users/documents/MyDocuments.tsx @@ -0,0 +1,158 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import Button from "@codegouvfr/react-dsfr/Button"; +import ButtonsGroup from "@codegouvfr/react-dsfr/ButtonsGroup"; +import Input from "@codegouvfr/react-dsfr/Input"; +import Table from "@codegouvfr/react-dsfr/Table"; +import { Upload } from "@codegouvfr/react-dsfr/Upload"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { FC, FormEvent, useState } from "react"; +import { useDebounceCallback } from "usehooks-ts"; + +import { DocumentListResponseDto } from "../../../../@types/entrepot"; +import AppLayout from "../../../../components/Layout/AppLayout"; +import LoadingIcon from "../../../../components/Utils/LoadingIcon"; +import Wait from "../../../../components/Utils/Wait"; +import RQKeys from "../../../../modules/entrepot/RQKeys"; +import { CartesApiException, jsonFetch } from "../../../../modules/jsonFetch"; +import SymfonyRouting from "../../../../modules/Routing"; +import { niceBytes } from "../../../../utils"; + +const MyDocuments: FC = () => { + const [filter, setFilter] = useState({}); + const debouncedSetFilter = useDebounceCallback(setFilter, 500); + + const documentsQuery = useQuery({ + queryKey: RQKeys.my_documents(filter), + queryFn: async ({ signal }) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_user_documents_get_list", filter); + return await jsonFetch(url, { signal }); + }, + }); + + const addDocumentMutation = useMutation({ + mutationFn: async (formData: FormData) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_user_documents_add"); + return await jsonFetch(url, { method: "POST" }, formData, true, true); + }, + onSettled: () => { + documentsQuery.refetch(); + }, + }); + + const handleAddDocument = (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + console.log(formData.values().toArray()); + + addDocumentMutation.mutate(formData); + e.currentTarget.reset(); + }; + + const deleteDocumentMutation = useMutation({ + mutationFn: async (documentId: string) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_user_documents_remove", { documentId }); + return await jsonFetch(url, { method: "DELETE" }); + }, + onSettled: () => { + documentsQuery.refetch(); + }, + }); + + const handleDeleteDocument = (id: string, name: string) => { + if (confirm(`Voulez-vous vraiment supprimer le document ${name} ?`)) { + deleteDocumentMutation.mutate(id); + } + }; + + return ( + +

Mes documents

+ +

Liste de documents {documentsQuery.isFetching ? : `(${documentsQuery?.data?.length ?? 0})`}

+

Filtres

+ debouncedSetFilter((prev) => ({ ...prev, name: e.target.value })), + }} + /> + debouncedSetFilter((prev) => ({ ...prev, labels: e.target.value })), + }} + /> + {documentsQuery.data !== undefined && documentsQuery.data.length > 0 && ( + [ + doc.name, + niceBytes(doc.size.toString()), + handleDeleteDocument(doc._id, doc.name), + }, + ]} + />, + ])} + fixed + /> + )} + + +
+ + + + + + +
+ + {addDocumentMutation.isPending && ( + +

Ajout du document en cours

+
+ )} + + {deleteDocumentMutation.isPending && ( + +

Suppression du document en cours

+
+ )} + + ); +}; + +export default MyDocuments; diff --git a/assets/modules/entrepot/RQKeys.ts b/assets/modules/entrepot/RQKeys.ts index f7696c7b..c6019497 100644 --- a/assets/modules/entrepot/RQKeys.ts +++ b/assets/modules/entrepot/RQKeys.ts @@ -51,10 +51,12 @@ const RQKeys = { catalogs_communities: (): string[] => ["catalogs", "communities"], - my_keys: (): string[] => ["my_keys"], - my_key: (keyId: string): string[] => ["my_key", keyId], - my_permissions: (): string[] => ["my_permissions"], user_me: (): string[] => ["user", "me"], + my_keys: (): string[] => ["user", "me", "keys"], + my_key: (keyId: string): string[] => ["user", "me", "keys", keyId], + my_permissions: (): string[] => ["user", "me", "permissions"], + my_documents: (query?: unknown): string[] => ["user", "me", "documents", JSON.stringify(query)], + my_document: (documentId: string): string[] => ["user", "me", "documents", documentId], accesses_request: (fileIdentifier: string): string[] => ["accesses_request", fileIdentifier], }; diff --git a/assets/router/RouterRenderer.tsx b/assets/router/RouterRenderer.tsx index 9a39db20..8d6094a9 100644 --- a/assets/router/RouterRenderer.tsx +++ b/assets/router/RouterRenderer.tsx @@ -29,6 +29,7 @@ const LoginDisabled = lazy(() => import("../pages/LoginDisabled/LoginDisabled")) const Me = lazy(() => import("../entrepot/pages/users/me/Me")); const MyAccessKeys = lazy(() => import("../entrepot/pages/users/access-keys/MyAccessKeys")); const UserKeyForm = lazy(() => import("../entrepot/pages/users/keys/UserKeyForm")); +const MyDocuments = lazy(() => import("../entrepot/pages/users/documents/MyDocuments")); const DatastoreManageStorage = lazy(() => import("../entrepot/pages/datastore/ManageStorage/DatastoreManageStorage")); const DatastoreManagePermissions = lazy(() => import("../entrepot/pages/datastore/ManagePermissions/DatastoreManagePermissions")); @@ -129,6 +130,8 @@ const RouterRenderer: FC = () => { return ; case "user_key_edit": return ; + case "my_documents": + return ; case "accesses_request": return ; case "datastore_manage_storage": diff --git a/assets/router/router.ts b/assets/router/router.ts index 43687bce..30e63fa8 100644 --- a/assets/router/router.ts +++ b/assets/router/router.ts @@ -48,6 +48,7 @@ const routeDefs = { }, (p) => `${appRoot}/mes-cles/${p.keyId}/modification` ), + my_documents: defineRoute(`${appRoot}/mes-documents`), dashboard_pro: defineRoute(`${appRoot}/tableau-de-bord`), diff --git a/src/Controller/Entrepot/UserController.php b/src/Controller/Entrepot/UserController.php index a82cff7f..ada521ff 100644 --- a/src/Controller/Entrepot/UserController.php +++ b/src/Controller/Entrepot/UserController.php @@ -16,7 +16,7 @@ use Symfony\Component\Routing\Annotation\Route; #[Route( - '/api/user', + '/api/users', name: 'cartesgouvfr_api_user_', options: ['expose' => true], condition: 'request.isXmlHttpRequest()' diff --git a/src/Controller/Entrepot/UserDocumentsController.php b/src/Controller/Entrepot/UserDocumentsController.php new file mode 100644 index 00000000..482b87dc --- /dev/null +++ b/src/Controller/Entrepot/UserDocumentsController.php @@ -0,0 +1,154 @@ + true], + condition: 'request.isXmlHttpRequest()' +)] +class UserDocumentsController extends AbstractController implements ApiControllerInterface +{ + public function __construct( + private UserDocumentsApiService $userDocumentsApiService, + ) { + } + + #[Route('', name: 'get_list', methods: ['GET'])] + public function getAll(Request $request): Response + { + try { + return $this->json($this->userDocumentsApiService->getAll($request->query->all())); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}', name: 'get', methods: ['GET'])] + public function get(string $documentId): Response + { + try { + return $this->json($this->userDocumentsApiService->get($documentId)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('', name: 'add', methods: ['POST'])] + public function add(Request $request): Response + { + try { + $file = $request->files->get('file'); + $name = $request->request->get('name'); + $description = $request->request->get('description'); + $labels = $request->request->get('labels') ? explode(',', $request->request->get('labels')) : null; + + return $this->json($this->userDocumentsApiService->add( + $file->getRealPath(), + $name, + $description, + $labels + )); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}', name: 'modify', methods: ['PATCH'])] + public function modify(string $documentId, Request $request): Response + { + try { + $data = json_decode($request->getContent(), true); + + return $this->json($this->userDocumentsApiService->modify( + $documentId, + $data['name'] ?? null, + $data['description'] ?? null, + $data['extra'] ?? null, + $data['labels'] ?? null, + $data['public_url'] ?? null + )); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}', name: 'replace_file', methods: ['PUT'])] + public function replaceFile(string $documentId, Request $request): Response + { + try { + $file = $request->files->get('file'); + + return $this->json($this->userDocumentsApiService->replaceFile( + $documentId, + $file->getRealPath() + )); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}', name: 'remove', methods: ['DELETE'])] + public function remove(string $documentId): Response + { + try { + return $this->json($this->userDocumentsApiService->remove($documentId)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}/file', name: 'download', methods: ['GET'])] + public function download(string $documentId): Response + { + try { + return new Response($this->userDocumentsApiService->download($documentId)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}/sharings', name: 'sharings_get', methods: ['GET'])] + public function getSharings(string $documentId): Response + { + try { + return $this->json($this->userDocumentsApiService->getSharings($documentId)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}/sharings', name: 'sharings_add', methods: ['POST'])] + public function addSharing(string $documentId, Request $request): Response + { + try { + $userIds = json_decode($request->getContent(), true); + + return $this->json($this->userDocumentsApiService->addSharing($documentId, $userIds)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } + + #[Route('/{documentId}/sharings', name: 'sharings_remove', methods: ['DELETE'])] + public function removeSharing(string $documentId, Request $request): Response + { + try { + $userIds = explode(',', $request->query->get('users', '')); + + return $this->json($this->userDocumentsApiService->removeSharing($documentId, $userIds)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } +} diff --git a/src/Services/EntrepotApi/AnnexeApiService.php b/src/Services/EntrepotApi/AnnexeApiService.php index dfe1f6e0..267e13b3 100644 --- a/src/Services/EntrepotApi/AnnexeApiService.php +++ b/src/Services/EntrepotApi/AnnexeApiService.php @@ -87,9 +87,9 @@ public function publish(string $datastoreId, string $annexeId): array ]); } - public function remove(string $datastoreId, string $annexeId): void + public function remove(string $datastoreId, string $annexeId): array { - $this->request('DELETE', "datastores/$datastoreId/annexes/$annexeId"); + return $this->request('DELETE', "datastores/$datastoreId/annexes/$annexeId"); } public function download(string $datastoreId, string $annexeId): string diff --git a/src/Services/EntrepotApi/UserDocumentsApiService.php b/src/Services/EntrepotApi/UserDocumentsApiService.php new file mode 100644 index 00000000..9eb15cc9 --- /dev/null +++ b/src/Services/EntrepotApi/UserDocumentsApiService.php @@ -0,0 +1,113 @@ +|null $query + */ + public function getAll(?array $query = []): array + { + return $this->requestAll('users/me/documents', $query); + } + + public function get(string $documentId): array + { + return $this->request('GET', "users/me/documents/$documentId"); + } + + /** + * @param array $labels + */ + public function add(string $filePath, string $name, ?string $description = null, ?array $labels = null): array + { + $formFields = [ + 'name' => $name, + ]; + if (null !== $description) { + $formFields['description'] = $description; + } + + if (null !== $labels) { + $formFields['labels'] = join(',', $labels); + } + + $response = $this->sendFile('POST', 'users/me/documents', $filePath, $formFields); + $this->filesystem->remove($filePath); + + return $response; + } + + /** + * @param array $extra + * @param array $labels + */ + public function modify(string $documentId, string $name, ?string $description = null, ?array $extra = null, ?array $labels = null, ?bool $publicUrl = null): array + { + $body = []; + + if (null !== $name) { + $body['name'] = $name; + } + + if (null !== $description) { + $body['description'] = $description; + } + + if (null !== $extra) { + $body['extra'] = $extra; + } + + if (null !== $labels) { + $body['labels'] = join(',', $labels); + } + + if (null !== $publicUrl) { + $body['public_url'] = true === $publicUrl ? 'true' : 'false'; + } + + return $this->request('PATCH', "users/me/documents/$documentId", $body); + } + + public function replaceFile(string $documentId, string $filePath): array + { + $response = $this->sendFile('PUT', "users/me/documents/$documentId", $filePath); + $this->filesystem->remove($filePath); + + return $response; + } + + public function remove(string $documentId): array + { + return $this->request('DELETE', "users/me/documents/$documentId"); + } + + public function download(string $documentId): string + { + return $this->request('GET', "users/me/documents/$documentId/file", [], [], [], false, false); + } + + public function getSharings(string $documentId): array + { + return $this->request('GET', "users/me/documents/$documentId/sharings"); + } + + /** + * @param array $userIds + */ + public function addSharing(string $documentId, array $userIds): array + { + return $this->request('POST', "users/me/documents/$documentId/sharings", $userIds); + } + + /** + * @param array $userIds + */ + public function removeSharing(string $documentId, array $userIds): array + { + return $this->request('DELETE', "users/me/documents/$documentId/sharings", [], [ + 'users' => implode(',', $userIds), + ]); + } +}