Skip to content

Commit

Permalink
feat: ajout routes documents personnels
Browse files Browse the repository at this point in the history
  • Loading branch information
ocruze committed Jan 21, 2025
1 parent 55e3a9f commit e9f12ab
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 6 deletions.
158 changes: 158 additions & 0 deletions assets/entrepot/pages/users/documents/MyDocuments.tsx
Original file line number Diff line number Diff line change
@@ -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<object>({});
const debouncedSetFilter = useDebounceCallback(setFilter, 500);

const documentsQuery = useQuery<DocumentListResponseDto[], CartesApiException>({
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<HTMLFormElement>) => {
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 (
<AppLayout documentTitle="Mes documents">
<h1>Mes documents</h1>

<h2>Liste de documents {documentsQuery.isFetching ? <LoadingIcon largeIcon={true} /> : `(${documentsQuery?.data?.length ?? 0})`}</h2>
<h3>Filtres</h3>
<Input
label="Nom"
nativeInputProps={{
onChange: (e) => debouncedSetFilter((prev) => ({ ...prev, name: e.target.value })),
}}
/>
<Input
label="Labels"
hintText="Une liste de labels séparés par une virgule"
nativeInputProps={{
onChange: (e) => debouncedSetFilter((prev) => ({ ...prev, labels: e.target.value })),
}}
/>
{documentsQuery.data !== undefined && documentsQuery.data.length > 0 && (
<Table
headers={["Nom", "Taille", "Actions"]}
data={documentsQuery.data.map((doc) => [
doc.name,
niceBytes(doc.size.toString()),
<ButtonsGroup
key={doc._id}
buttons={[
{
children: "Supprimer",
onClick: () => handleDeleteDocument(doc._id, doc.name),
},
]}
/>,
])}
fixed
/>
)}

<Accordion label="Ajouter un document" titleAs="h2">
<form onSubmit={handleAddDocument}>
<Input
label="Nom"
nativeInputProps={{
name: "name",
required: true,
}}
/>
<Input
label="description"
nativeInputProps={{
name: "description",
}}
/>
<Input
label="labels"
hintText="Une liste de labels séparés par une virgule"
nativeInputProps={{
name: "labels",
}}
/>
<Upload
label="Document"
hint=""
className={fr.cx("fr-input-group")}
multiple={false}
nativeInputProps={{
name: "file",
required: true,
}}
/>
<Button type="submit">Ajouter</Button>
</form>
</Accordion>

{addDocumentMutation.isPending && (
<Wait>
<p className={fr.cx("fr-h6", "fr-m-0", "fr-p-0")}>Ajout du document en cours</p>
</Wait>
)}

{deleteDocumentMutation.isPending && (
<Wait>
<p className={fr.cx("fr-h6", "fr-m-0", "fr-p-0")}>Suppression du document en cours</p>
</Wait>
)}
</AppLayout>
);
};

export default MyDocuments;
8 changes: 5 additions & 3 deletions assets/modules/entrepot/RQKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};
Expand Down
3 changes: 3 additions & 0 deletions assets/router/RouterRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -129,6 +130,8 @@ const RouterRenderer: FC = () => {
return <UserKeyForm />;
case "user_key_edit":
return <UserKeyForm keyId={route.params.keyId} />;
case "my_documents":
return <MyDocuments />;
case "accesses_request":
return <AccessesRequest fileIdentifier={route.params.fileIdentifier} />;
case "datastore_manage_storage":
Expand Down
1 change: 1 addition & 0 deletions assets/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`),

Expand Down
2 changes: 1 addition & 1 deletion src/Controller/Entrepot/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()'
Expand Down
154 changes: 154 additions & 0 deletions src/Controller/Entrepot/UserDocumentsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

namespace App\Controller\Entrepot;

use App\Controller\ApiControllerInterface;
use App\Exception\ApiException;
use App\Exception\CartesApiException;
use App\Services\EntrepotApi\UserDocumentsApiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(
'/api/users/documents',
name: 'cartesgouvfr_api_user_documents_',
options: ['expose' => 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());
}
}
}
4 changes: 2 additions & 2 deletions src/Services/EntrepotApi/AnnexeApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

1 comment on commit e9f12ab

@ocruze
Copy link
Member Author

@ocruze ocruze commented on e9f12ab Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.