Compare commits
20 commits
77dc4173b6
...
1f24f4e317
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f24f4e317 | |||
| 4cf45a761d | |||
| 6c14e193e0 | |||
| 01434a9b2d | |||
| 247433bc60 | |||
| 1934a6b94f | |||
| 4b432d9b99 | |||
| d0810ba4b3 | |||
| bd38e350cf | |||
| 1d9e2cff0a | |||
| 9cb968afd8 | |||
| 299ccc4f58 | |||
| 71bb131687 | |||
| b6da71ecd2 | |||
| a2c3ffad8b | |||
| 4292494345 | |||
| cd2e63d17d | |||
| 1fbe198d8c | |||
| 97ecbebb9e | |||
| 0f3bb6de6b |
31 changed files with 1884 additions and 104 deletions
|
|
@ -9,14 +9,17 @@ use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElementRequest;
|
use App\Element\UseCases\UpdateElement\UpdateElementRequest;
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Exceptions\NotFoundException;
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
class ElementController
|
class ElementController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private GetElement $getElement,
|
private GetElement $getElement,
|
||||||
private UpdateElement $updateElement,
|
private UpdateElement $updateElement,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,12 +55,15 @@ class ElementController
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(?int $id, Request $request): JsonResponse
|
public function update(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$iconImage = $this->uploadedFileInput($request, 'iconImage');
|
||||||
|
$pdf = $this->uploadedFileInput($request, 'pdf');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$element = $this->updateElement->execute(
|
$element = $this->updateElement->execute(
|
||||||
new UpdateElementRequest(
|
new UpdateElementRequest(
|
||||||
id: $id,
|
id: $this->intInput($request, 'elementId'),
|
||||||
title: $this->stringInput($request, 'title'),
|
title: $this->stringInput($request, 'title'),
|
||||||
description: $this->stringInput($request, 'description'),
|
description: $this->stringInput($request, 'description'),
|
||||||
iconImageUrl: $this->stringInput(
|
iconImageUrl: $this->stringInput(
|
||||||
|
|
@ -67,6 +73,14 @@ class ElementController
|
||||||
richText: $this->stringInput($request, 'richText'),
|
richText: $this->stringInput($request, 'richText'),
|
||||||
pdfPath: $this->stringInput($request, 'pdfPath'),
|
pdfPath: $this->stringInput($request, 'pdfPath'),
|
||||||
youtubeUrl: $this->stringInput($request, 'youtubeUrl'),
|
youtubeUrl: $this->stringInput($request, 'youtubeUrl'),
|
||||||
|
iconImageContents: $iconImage['contents'],
|
||||||
|
iconImageOriginalName: $iconImage['originalName'],
|
||||||
|
iconImageMimeType: $iconImage['mimeType'],
|
||||||
|
iconImageSizeBytes: $iconImage['sizeBytes'],
|
||||||
|
pdfContents: $pdf['contents'],
|
||||||
|
pdfOriginalName: $pdf['originalName'],
|
||||||
|
pdfMimeType: $pdf['mimeType'],
|
||||||
|
pdfSizeBytes: $pdf['sizeBytes'],
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (BadRequestException $exception) {
|
} catch (BadRequestException $exception) {
|
||||||
|
|
@ -84,9 +98,31 @@ class ElementController
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stringInput(Request $request, string $key): ?string
|
private function intInput(Request $request, string $key): ?int
|
||||||
{
|
{
|
||||||
$value = $request->input($key);
|
$value = $request->input($key);
|
||||||
|
if (is_int($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && ctype_digit($value)) {
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stringInput(Request $request, string $key): ?string
|
||||||
|
{
|
||||||
|
if (! $request->exists($key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $request->input($key);
|
||||||
|
if ($value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (! is_string($value)) {
|
if (! is_string($value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +130,45 @@ class ElementController
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* contents: string|null,
|
||||||
|
* originalName: string|null,
|
||||||
|
* mimeType: string|null,
|
||||||
|
* sizeBytes: int|null
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function uploadedFileInput(Request $request, string $key): array
|
||||||
|
{
|
||||||
|
$emptyFileInput = [
|
||||||
|
'contents' => null,
|
||||||
|
'originalName' => null,
|
||||||
|
'mimeType' => null,
|
||||||
|
'sizeBytes' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $request->hasFile($key)) {
|
||||||
|
return $emptyFileInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file($key);
|
||||||
|
if (! $file instanceof UploadedFile) {
|
||||||
|
return $emptyFileInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
$realPath = $file->getRealPath();
|
||||||
|
if ($realPath === false) {
|
||||||
|
return $emptyFileInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'contents' => (string) file_get_contents($realPath),
|
||||||
|
'originalName' => $file->getClientOriginalName(),
|
||||||
|
'mimeType' => (string) $file->getMimeType(),
|
||||||
|
'sizeBytes' => (int) $file->getSize(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* id: int,
|
* id: int,
|
||||||
|
|
@ -111,10 +186,29 @@ class ElementController
|
||||||
'id' => $element->getId(),
|
'id' => $element->getId(),
|
||||||
'title' => $element->getTitle(),
|
'title' => $element->getTitle(),
|
||||||
'description' => $element->getDescription(),
|
'description' => $element->getDescription(),
|
||||||
'iconImageUrl' => $element->getIconImageUrl(),
|
'iconImageUrl' => $this->storageUrl(
|
||||||
|
$element->getIconImageUrl(),
|
||||||
|
'element-icons/',
|
||||||
|
),
|
||||||
'richText' => $element->getRichText(),
|
'richText' => $element->getRichText(),
|
||||||
'pdfPath' => $element->getPdfPath(),
|
'pdfPath' => $this->storageUrl(
|
||||||
|
$element->getPdfPath(),
|
||||||
|
'element-pdfs/',
|
||||||
|
),
|
||||||
'youtubeUrl' => $element->getYoutubeUrl(),
|
'youtubeUrl' => $element->getYoutubeUrl(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function storageUrl(?string $path, string $folderPrefix): ?string
|
||||||
|
{
|
||||||
|
if ($path === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($path, $folderPrefix)) {
|
||||||
|
return $this->fileUploader->url($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ class UpdateElement
|
||||||
private UpdateRichText $updateRichText,
|
private UpdateRichText $updateRichText,
|
||||||
private UpdatePdfPath $updatePdfPath,
|
private UpdatePdfPath $updatePdfPath,
|
||||||
private UpdateYoutubeUrl $updateYoutubeUrl,
|
private UpdateYoutubeUrl $updateYoutubeUrl,
|
||||||
|
private UpdateIconImage $updateIconImage,
|
||||||
|
private UpdatePdf $updatePdf,
|
||||||
private ElementRepository $elementRepository,
|
private ElementRepository $elementRepository,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +37,9 @@ class UpdateElement
|
||||||
&& $request->iconImageUrl === null
|
&& $request->iconImageUrl === null
|
||||||
&& $request->richText === null
|
&& $request->richText === null
|
||||||
&& $request->pdfPath === null
|
&& $request->pdfPath === null
|
||||||
&& $request->youtubeUrl === null;
|
&& $request->youtubeUrl === null
|
||||||
|
&& $request->iconImageContents === null
|
||||||
|
&& $request->pdfContents === null;
|
||||||
if ($hasNoFields) {
|
if ($hasNoFields) {
|
||||||
throw new BadRequestException('nothing to update');
|
throw new BadRequestException('nothing to update');
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +82,28 @@ class UpdateElement
|
||||||
youtubeUrl: $request->youtubeUrl,
|
youtubeUrl: $request->youtubeUrl,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if ($request->iconImageContents !== null) {
|
||||||
|
$this->updateIconImage->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
id: $request->id,
|
||||||
|
iconImageContents: $request->iconImageContents,
|
||||||
|
iconImageOriginalName: $request->iconImageOriginalName,
|
||||||
|
iconImageMimeType: $request->iconImageMimeType,
|
||||||
|
iconImageSizeBytes: $request->iconImageSizeBytes,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($request->pdfContents !== null) {
|
||||||
|
$this->updatePdf->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
id: $request->id,
|
||||||
|
pdfContents: $request->pdfContents,
|
||||||
|
pdfOriginalName: $request->pdfOriginalName,
|
||||||
|
pdfMimeType: $request->pdfMimeType,
|
||||||
|
pdfSizeBytes: $request->pdfSizeBytes,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$element = $this->elementRepository->find($request->id);
|
$element = $this->elementRepository->find($request->id);
|
||||||
if ($element === null) {
|
if ($element === null) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ class UpdateElementRequest
|
||||||
public ?string $richText,
|
public ?string $richText,
|
||||||
public ?string $pdfPath,
|
public ?string $pdfPath,
|
||||||
public ?string $youtubeUrl,
|
public ?string $youtubeUrl,
|
||||||
|
public ?string $iconImageContents,
|
||||||
|
public ?string $iconImageOriginalName,
|
||||||
|
public ?string $iconImageMimeType,
|
||||||
|
public ?int $iconImageSizeBytes,
|
||||||
|
public ?string $pdfContents,
|
||||||
|
public ?string $pdfOriginalName,
|
||||||
|
public ?string $pdfMimeType,
|
||||||
|
public ?int $pdfSizeBytes,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\UpdateElement;
|
||||||
|
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileToUpload;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
|
class UpdateIconImage
|
||||||
|
{
|
||||||
|
private const ALLOWED_MIME_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ElementRepository $elementRepository,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequestException
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function execute(UpdateIconImageRequest $request): Element
|
||||||
|
{
|
||||||
|
if ($request->id === null) {
|
||||||
|
throw new BadRequestException('id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$request->iconImageContents === null
|
||||||
|
|| $request->iconImageOriginalName === null
|
||||||
|
|| $request->iconImageMimeType === null
|
||||||
|
|| $request->iconImageSizeBytes === null
|
||||||
|
) {
|
||||||
|
throw new BadRequestException('icon image is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
! in_array(
|
||||||
|
$request->iconImageMimeType,
|
||||||
|
self::ALLOWED_MIME_TYPES,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'icon image must be a jpeg, png or webp image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'icon image must be 5MB or smaller',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$element = $this->elementRepository->find($request->id);
|
||||||
|
if ($element === null) {
|
||||||
|
throw new NotFoundException('Element not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$iconImage = new FileToUpload(
|
||||||
|
contents: $request->iconImageContents,
|
||||||
|
originalName: $request->iconImageOriginalName,
|
||||||
|
mimeType: $request->iconImageMimeType,
|
||||||
|
sizeBytes: $request->iconImageSizeBytes,
|
||||||
|
);
|
||||||
|
$path = $this->fileUploader->upload($iconImage, 'element-icons');
|
||||||
|
$element->setIconImageUrl($path);
|
||||||
|
|
||||||
|
return $this->elementRepository->update($element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\UpdateElement;
|
||||||
|
|
||||||
|
class UpdateIconImageRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $id,
|
||||||
|
public ?string $iconImageContents,
|
||||||
|
public ?string $iconImageOriginalName,
|
||||||
|
public ?string $iconImageMimeType,
|
||||||
|
public ?int $iconImageSizeBytes,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ use App\Element\Element;
|
||||||
use App\Element\ElementRepository;
|
use App\Element\ElementRepository;
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Exceptions\NotFoundException;
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
class UpdateIconImageUrl
|
class UpdateIconImageUrl
|
||||||
{
|
{
|
||||||
public function __construct(private ElementRepository $elementRepository)
|
public function __construct(
|
||||||
{
|
private ElementRepository $elementRepository,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,9 +35,12 @@ class UpdateIconImageUrl
|
||||||
throw new NotFoundException('Element not found');
|
throw new NotFoundException('Element not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$element->setIconImageUrl($this->nullableString(
|
$iconImageUrl = $this->nullableString($request->iconImageUrl);
|
||||||
$request->iconImageUrl,
|
if ($iconImageUrl === null) {
|
||||||
));
|
$this->deleteManagedFile($element->getIconImageUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
$element->setIconImageUrl($iconImageUrl);
|
||||||
|
|
||||||
return $this->elementRepository->update($element);
|
return $this->elementRepository->update($element);
|
||||||
}
|
}
|
||||||
|
|
@ -47,4 +53,13 @@ class UpdateIconImageUrl
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function deleteManagedFile(?string $path): void
|
||||||
|
{
|
||||||
|
if ($path === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fileUploader->delete($path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
backend/app/Element/UseCases/UpdateElement/UpdatePdf.php
Normal file
75
backend/app/Element/UseCases/UpdateElement/UpdatePdf.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\UpdateElement;
|
||||||
|
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileToUpload;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
|
class UpdatePdf
|
||||||
|
{
|
||||||
|
private const ALLOWED_MIME_TYPES = [
|
||||||
|
'application/pdf',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ElementRepository $elementRepository,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequestException
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function execute(UpdatePdfRequest $request): Element
|
||||||
|
{
|
||||||
|
if ($request->id === null) {
|
||||||
|
throw new BadRequestException('id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$request->pdfContents === null
|
||||||
|
|| $request->pdfOriginalName === null
|
||||||
|
|| $request->pdfMimeType === null
|
||||||
|
|| $request->pdfSizeBytes === null
|
||||||
|
) {
|
||||||
|
throw new BadRequestException('pdf is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
! in_array(
|
||||||
|
$request->pdfMimeType,
|
||||||
|
self::ALLOWED_MIME_TYPES,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new BadRequestException('pdf must be a pdf file');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->pdfSizeBytes > self::MAX_SIZE_BYTES) {
|
||||||
|
throw new BadRequestException('pdf must be 10MB or smaller');
|
||||||
|
}
|
||||||
|
|
||||||
|
$element = $this->elementRepository->find($request->id);
|
||||||
|
if ($element === null) {
|
||||||
|
throw new NotFoundException('Element not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf = new FileToUpload(
|
||||||
|
contents: $request->pdfContents,
|
||||||
|
originalName: $request->pdfOriginalName,
|
||||||
|
mimeType: $request->pdfMimeType,
|
||||||
|
sizeBytes: $request->pdfSizeBytes,
|
||||||
|
);
|
||||||
|
$path = $this->fileUploader->upload($pdf, 'element-pdfs');
|
||||||
|
$element->setPdfPath($path);
|
||||||
|
|
||||||
|
return $this->elementRepository->update($element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ use App\Element\Element;
|
||||||
use App\Element\ElementRepository;
|
use App\Element\ElementRepository;
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Exceptions\NotFoundException;
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
class UpdatePdfPath
|
class UpdatePdfPath
|
||||||
{
|
{
|
||||||
public function __construct(private ElementRepository $elementRepository)
|
public function __construct(
|
||||||
{
|
private ElementRepository $elementRepository,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,7 +35,12 @@ class UpdatePdfPath
|
||||||
throw new NotFoundException('Element not found');
|
throw new NotFoundException('Element not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$element->setPdfPath($this->nullableString($request->pdfPath));
|
$pdfPath = $this->nullableString($request->pdfPath);
|
||||||
|
if ($pdfPath === null) {
|
||||||
|
$this->deleteManagedFile($element->getPdfPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
$element->setPdfPath($pdfPath);
|
||||||
|
|
||||||
return $this->elementRepository->update($element);
|
return $this->elementRepository->update($element);
|
||||||
}
|
}
|
||||||
|
|
@ -45,4 +53,13 @@ class UpdatePdfPath
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function deleteManagedFile(?string $path): void
|
||||||
|
{
|
||||||
|
if ($path === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fileUploader->delete($path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\UpdateElement;
|
||||||
|
|
||||||
|
class UpdatePdfRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $id,
|
||||||
|
public ?string $pdfContents,
|
||||||
|
public ?string $pdfOriginalName,
|
||||||
|
public ?string $pdfMimeType,
|
||||||
|
public ?int $pdfSizeBytes,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ use App\Auth\PasswordHasher;
|
||||||
use App\Auth\RandomTokenGenerator;
|
use App\Auth\RandomTokenGenerator;
|
||||||
use App\Auth\SystemClock;
|
use App\Auth\SystemClock;
|
||||||
use App\Auth\TokenGenerator;
|
use App\Auth\TokenGenerator;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
use App\Shared\Files\LaravelFileUploader;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
|
@ -26,5 +28,9 @@ class AppServiceProvider extends ServiceProvider
|
||||||
Clock::class,
|
Clock::class,
|
||||||
SystemClock::class,
|
SystemClock::class,
|
||||||
);
|
);
|
||||||
|
$this->app->bind(
|
||||||
|
FileUploader::class,
|
||||||
|
LaravelFileUploader::class,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
backend/app/Shared/Files/FileToUpload.php
Normal file
34
backend/app/Shared/Files/FileToUpload.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Files;
|
||||||
|
|
||||||
|
final readonly class FileToUpload
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $contents,
|
||||||
|
private string $originalName,
|
||||||
|
private string $mimeType,
|
||||||
|
private int $sizeBytes,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContents(): string
|
||||||
|
{
|
||||||
|
return $this->contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginalName(): string
|
||||||
|
{
|
||||||
|
return $this->originalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType(): string
|
||||||
|
{
|
||||||
|
return $this->mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSizeBytes(): int
|
||||||
|
{
|
||||||
|
return $this->sizeBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/app/Shared/Files/FileUploader.php
Normal file
12
backend/app/Shared/Files/FileUploader.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Files;
|
||||||
|
|
||||||
|
interface FileUploader
|
||||||
|
{
|
||||||
|
public function upload(FileToUpload $file, string $folder): string;
|
||||||
|
|
||||||
|
public function url(string $path): string;
|
||||||
|
|
||||||
|
public function delete(string $path): void;
|
||||||
|
}
|
||||||
50
backend/app/Shared/Files/LaravelFileUploader.php
Normal file
50
backend/app/Shared/Files/LaravelFileUploader.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Files;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class LaravelFileUploader implements FileUploader
|
||||||
|
{
|
||||||
|
public function upload(FileToUpload $file, string $folder): string
|
||||||
|
{
|
||||||
|
$path = $folder . '/' . Str::random(40) . $this->extensionFor($file);
|
||||||
|
Storage::disk('public')->put($path, $file->getContents());
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url(string $path): string
|
||||||
|
{
|
||||||
|
return Storage::disk('public')->url($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $path): void
|
||||||
|
{
|
||||||
|
Storage::disk('public')->delete($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extensionFor(FileToUpload $file): string
|
||||||
|
{
|
||||||
|
$mimeExtensions = [
|
||||||
|
'image/jpeg' => '.jpg',
|
||||||
|
'image/png' => '.png',
|
||||||
|
'image/webp' => '.webp',
|
||||||
|
'application/pdf' => '.pdf',
|
||||||
|
];
|
||||||
|
if (isset($mimeExtensions[$file->getMimeType()])) {
|
||||||
|
return $mimeExtensions[$file->getMimeType()];
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalExtension = pathinfo(
|
||||||
|
$file->getOriginalName(),
|
||||||
|
PATHINFO_EXTENSION,
|
||||||
|
);
|
||||||
|
if ($originalExtension !== '') {
|
||||||
|
return '.' . $originalExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Element\CreateElementDto;
|
||||||
use App\Element\ElementRepository;
|
use App\Element\ElementRepository;
|
||||||
use App\Set\SetRepository;
|
use App\Set\SetRepository;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class ElementSeeder extends Seeder
|
class ElementSeeder extends Seeder
|
||||||
|
|
@ -15,11 +16,18 @@ class ElementSeeder extends Seeder
|
||||||
$setRepository = app(SetRepository::class);
|
$setRepository = app(SetRepository::class);
|
||||||
$elementRepository = app(ElementRepository::class);
|
$elementRepository = app(ElementRepository::class);
|
||||||
$baderechSet = $setRepository->find(1);
|
$baderechSet = $setRepository->find(1);
|
||||||
|
$rootIconImageUrl = $this->seedStorageFile(
|
||||||
|
'element-icons/baderech-haavodah-icon.png',
|
||||||
|
);
|
||||||
|
$introductionPdfPath = $this->seedStorageFile(
|
||||||
|
'element-pdfs/baderech.pdf',
|
||||||
|
);
|
||||||
|
|
||||||
$rootElement = $elementRepository->create(new CreateElementDto(
|
$rootElement = $elementRepository->create(new CreateElementDto(
|
||||||
set: $baderechSet,
|
set: $baderechSet,
|
||||||
title: $baderechSet->getName(),
|
title: $baderechSet->getName(),
|
||||||
description: $baderechSet->getDescription(),
|
description: $baderechSet->getDescription(),
|
||||||
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
iconImageUrl: $rootIconImageUrl,
|
||||||
richText: '',
|
richText: '',
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
|
@ -36,7 +44,7 @@ class ElementSeeder extends Seeder
|
||||||
. 'unlock the ability to live with strength, confidence, '
|
. 'unlock the ability to live with strength, confidence, '
|
||||||
. 'and hope.',
|
. 'and hope.',
|
||||||
'richText' => $this->introductionRichText(),
|
'richText' => $this->introductionRichText(),
|
||||||
'pdfPath' => '/assets/pdfs/baderech.pdf',
|
'pdfPath' => $introductionPdfPath,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => '2. Foundations',
|
'title' => '2. Foundations',
|
||||||
|
|
@ -116,6 +124,22 @@ class ElementSeeder extends Seeder
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function seedStorageFile(
|
||||||
|
string $storagePath,
|
||||||
|
): string {
|
||||||
|
$sourcePath = base_path(
|
||||||
|
"database/seeders/assets/$storagePath",
|
||||||
|
);
|
||||||
|
$contents = file_get_contents($sourcePath);
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new RuntimeException("Seed file missing: $sourcePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::disk('public')->put($storagePath, $contents);
|
||||||
|
|
||||||
|
return $storagePath;
|
||||||
|
}
|
||||||
|
|
||||||
private function introductionRichText(): string
|
private function introductionRichText(): string
|
||||||
{
|
{
|
||||||
return '<h2 style="text-align: center;">'
|
return '<h2 style="text-align: center;">'
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
backend/database/seeders/assets/element-pdfs/baderech.pdf
Normal file
BIN
backend/database/seeders/assets/element-pdfs/baderech.pdf
Normal file
Binary file not shown.
|
|
@ -12,5 +12,5 @@ Route::get('/me', [AuthController::class, 'me'])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
Route::get('/sets', [SetController::class, 'index']);
|
Route::get('/sets', [SetController::class, 'index']);
|
||||||
Route::get('/elements/{id}', [ElementController::class, 'show']);
|
Route::get('/elements/{id}', [ElementController::class, 'show']);
|
||||||
Route::patch('/elements/{id}', [ElementController::class, 'update'])
|
Route::post('/element/update', [ElementController::class, 'update'])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
|
|
|
||||||
36
backend/tests/Fakes/FakeFileUploader.php
Normal file
36
backend/tests/Fakes/FakeFileUploader.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Fakes;
|
||||||
|
|
||||||
|
use App\Shared\Files\FileToUpload;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
|
class FakeFileUploader implements FileUploader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, array{file: FileToUpload, folder: string}>
|
||||||
|
*/
|
||||||
|
public array $uploads = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
public array $deletedPaths = [];
|
||||||
|
|
||||||
|
public function upload(FileToUpload $file, string $folder): string
|
||||||
|
{
|
||||||
|
$this->uploads[] = ['file' => $file, 'folder' => $folder];
|
||||||
|
|
||||||
|
return $folder . '/fake-' . $file->getOriginalName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url(string $path): string
|
||||||
|
{
|
||||||
|
return 'https://test.local/storage/' . $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $path): void
|
||||||
|
{
|
||||||
|
$this->deletedPaths[] = $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,27 @@ use App\Element\ElementRepository;
|
||||||
use App\Set\SetRepository;
|
use App\Set\SetRepository;
|
||||||
use Database\Seeders\DatabaseSeeder;
|
use Database\Seeders\DatabaseSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class DemoSeedDataTest extends TestCase
|
class DemoSeedDataTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function testElementSeederDoesNotReadFrontendAssets(): void
|
||||||
|
{
|
||||||
|
$seederSource = file_get_contents(
|
||||||
|
base_path('database/seeders/ElementSeeder.php'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertIsString($seederSource);
|
||||||
|
$this->assertStringNotContainsString('../frontend/', $seederSource);
|
||||||
|
}
|
||||||
|
|
||||||
public function testBaderechRootElementHasDemoChildren(): void
|
public function testBaderechRootElementHasDemoChildren(): void
|
||||||
{
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
$this->seed(DatabaseSeeder::class);
|
$this->seed(DatabaseSeeder::class);
|
||||||
|
|
||||||
$setRepository = app(SetRepository::class);
|
$setRepository = app(SetRepository::class);
|
||||||
|
|
@ -23,6 +36,13 @@ class DemoSeedDataTest extends TestCase
|
||||||
$childElements = $elementRepository->findByParentElement($rootElement);
|
$childElements = $elementRepository->findByParentElement($rootElement);
|
||||||
|
|
||||||
$this->assertSame('', $rootElement->getRichText());
|
$this->assertSame('', $rootElement->getRichText());
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons/baderech-haavodah-icon.png',
|
||||||
|
$rootElement->getIconImageUrl(),
|
||||||
|
);
|
||||||
|
Storage::disk('public')->assertExists(
|
||||||
|
'element-icons/baderech-haavodah-icon.png',
|
||||||
|
);
|
||||||
$this->assertNull($rootElement->getPdfPath());
|
$this->assertNull($rootElement->getPdfPath());
|
||||||
$this->assertNull($rootElement->getYoutubeUrl());
|
$this->assertNull($rootElement->getYoutubeUrl());
|
||||||
$this->assertCount(6, $childElements);
|
$this->assertCount(6, $childElements);
|
||||||
|
|
@ -76,9 +96,10 @@ class DemoSeedDataTest extends TestCase
|
||||||
$childElements[0]->getRichText(),
|
$childElements[0]->getRichText(),
|
||||||
);
|
);
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
'/assets/pdfs/baderech.pdf',
|
'element-pdfs/baderech.pdf',
|
||||||
$childElements[0]->getPdfPath(),
|
$childElements[0]->getPdfPath(),
|
||||||
);
|
);
|
||||||
|
Storage::disk('public')->assertExists('element-pdfs/baderech.pdf');
|
||||||
|
|
||||||
$introductionChildElements = $elementRepository->findByParentElement(
|
$introductionChildElements = $elementRepository->findByParentElement(
|
||||||
$childElements[0],
|
$childElements[0],
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ use App\User\UserRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class ElementsEndpointTest extends TestCase
|
class ElementsEndpointTest extends TestCase
|
||||||
|
|
@ -120,7 +122,8 @@ class ElementsEndpointTest extends TestCase
|
||||||
parentElement: null,
|
parentElement: null,
|
||||||
));
|
));
|
||||||
|
|
||||||
$response = $this->patchJson("/api/elements/{$element->getId()}", [
|
$response = $this->postJson('/api/element/update', [
|
||||||
|
'elementId' => $element->getId(),
|
||||||
'title' => 'Updated title',
|
'title' => 'Updated title',
|
||||||
'description' => 'Updated description',
|
'description' => 'Updated description',
|
||||||
'iconImageUrl' => '/assets/updated-icon.png',
|
'iconImageUrl' => '/assets/updated-icon.png',
|
||||||
|
|
@ -160,7 +163,8 @@ class ElementsEndpointTest extends TestCase
|
||||||
|
|
||||||
$response = $this->withCredentials()
|
$response = $this->withCredentials()
|
||||||
->withUnencryptedCookie('auth_token', 'valid-token')
|
->withUnencryptedCookie('auth_token', 'valid-token')
|
||||||
->patchJson("/api/elements/{$element->getId()}", [
|
->postJson('/api/element/update', [
|
||||||
|
'elementId' => $element->getId(),
|
||||||
'title' => 'Updated title',
|
'title' => 'Updated title',
|
||||||
'description' => 'Updated description',
|
'description' => 'Updated description',
|
||||||
'iconImageUrl' => '/assets/updated-icon.png',
|
'iconImageUrl' => '/assets/updated-icon.png',
|
||||||
|
|
@ -189,6 +193,225 @@ class ElementsEndpointTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAuthenticatedUpdateClearsUploadedMedia(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
Storage::disk('public')->put(
|
||||||
|
'element-icons/original-icon.png',
|
||||||
|
'icon bytes',
|
||||||
|
);
|
||||||
|
Storage::disk('public')->put(
|
||||||
|
'element-pdfs/original.pdf',
|
||||||
|
'pdf bytes',
|
||||||
|
);
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||||
|
));
|
||||||
|
$element = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: 'element-icons/original-icon.png',
|
||||||
|
richText: '<p>A structured path for growth</p>',
|
||||||
|
pdfPath: 'element-pdfs/original.pdf',
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$this->createSession('valid-token');
|
||||||
|
|
||||||
|
$response = $this->withCredentials()
|
||||||
|
->withUnencryptedCookie('auth_token', 'valid-token')
|
||||||
|
->postJson('/api/element/update', [
|
||||||
|
'elementId' => $element->getId(),
|
||||||
|
'iconImageUrl' => '',
|
||||||
|
'pdfPath' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$body = $response->json();
|
||||||
|
$this->assertNull($body['element']['iconImageUrl']);
|
||||||
|
$this->assertNull($body['element']['pdfPath']);
|
||||||
|
$updatedElement = $elementRepository->find($element->getId());
|
||||||
|
$this->assertNotNull($updatedElement);
|
||||||
|
$this->assertNull($updatedElement->getIconImageUrl());
|
||||||
|
$this->assertNull($updatedElement->getPdfPath());
|
||||||
|
Storage::disk('public')->assertMissing(
|
||||||
|
'element-icons/original-icon.png',
|
||||||
|
);
|
||||||
|
Storage::disk('public')->assertMissing(
|
||||||
|
'element-pdfs/original.pdf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIconImageUploadRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||||
|
));
|
||||||
|
$element = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '<p>A structured path for growth</p>',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$response = $this->post(
|
||||||
|
'/api/element/update',
|
||||||
|
[
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
'iconImage' => $this->iconImageUpload(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertUnauthorized();
|
||||||
|
$response->assertExactJson([
|
||||||
|
'error' => 'unauthenticated',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthenticatedIconImageUploadReturnsElementPayload(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||||
|
));
|
||||||
|
$element = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '<p>A structured path for growth</p>',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$this->createSession('valid-token');
|
||||||
|
|
||||||
|
$response = $this->withCredentials()
|
||||||
|
->withUnencryptedCookie('auth_token', 'valid-token')
|
||||||
|
->post(
|
||||||
|
'/api/element/update',
|
||||||
|
[
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
'iconImage' => $this->iconImageUpload(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$body = $response->json();
|
||||||
|
$this->assertSame($element->getId(), $body['element']['id']);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'/storage/element-icons/',
|
||||||
|
$body['element']['iconImageUrl'],
|
||||||
|
);
|
||||||
|
$updatedElement = $elementRepository->find($element->getId());
|
||||||
|
$this->assertNotNull($updatedElement);
|
||||||
|
$updatedIconImageUrl = $updatedElement->getIconImageUrl();
|
||||||
|
$this->assertIsString($updatedIconImageUrl);
|
||||||
|
$this->assertStringStartsWith(
|
||||||
|
'element-icons/',
|
||||||
|
$updatedIconImageUrl,
|
||||||
|
);
|
||||||
|
Storage::disk('public')->assertExists($updatedIconImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPdfUploadRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||||
|
));
|
||||||
|
$element = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '<p>A structured path for growth</p>',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$response = $this->post(
|
||||||
|
'/api/element/update',
|
||||||
|
[
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
'pdf' => $this->pdfUpload(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertUnauthorized();
|
||||||
|
$response->assertExactJson([
|
||||||
|
'error' => 'unauthenticated',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthenticatedPdfUploadReturnsElementPayload(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||||
|
));
|
||||||
|
$element = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Baderech HaAvodah',
|
||||||
|
description: 'A structured path for growth',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '<p>A structured path for growth</p>',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$this->createSession('valid-token');
|
||||||
|
|
||||||
|
$response = $this->withCredentials()
|
||||||
|
->withUnencryptedCookie('auth_token', 'valid-token')
|
||||||
|
->post(
|
||||||
|
'/api/element/update',
|
||||||
|
[
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
'pdf' => $this->pdfUpload(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$body = $response->json();
|
||||||
|
$this->assertSame($element->getId(), $body['element']['id']);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'/storage/element-pdfs/',
|
||||||
|
$body['element']['pdfPath'],
|
||||||
|
);
|
||||||
|
$updatedElement = $elementRepository->find($element->getId());
|
||||||
|
$this->assertNotNull($updatedElement);
|
||||||
|
$updatedPdfPath = $updatedElement->getPdfPath();
|
||||||
|
$this->assertIsString($updatedPdfPath);
|
||||||
|
$this->assertStringStartsWith('element-pdfs/', $updatedPdfPath);
|
||||||
|
Storage::disk('public')->assertExists($updatedPdfPath);
|
||||||
|
}
|
||||||
|
|
||||||
private function createSession(string $token): void
|
private function createSession(string $token): void
|
||||||
{
|
{
|
||||||
$userRepository = app(UserRepository::class);
|
$userRepository = app(UserRepository::class);
|
||||||
|
|
@ -211,4 +434,30 @@ class ElementsEndpointTest extends TestCase
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function iconImageUpload(): UploadedFile
|
||||||
|
{
|
||||||
|
$fixturePath = __DIR__ . '/../fixtures/icon.png';
|
||||||
|
|
||||||
|
return new UploadedFile(
|
||||||
|
$fixturePath,
|
||||||
|
'icon.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pdfUpload(): UploadedFile
|
||||||
|
{
|
||||||
|
$fixturePath = __DIR__ . '/../fixtures/baderech.pdf';
|
||||||
|
|
||||||
|
return new UploadedFile(
|
||||||
|
$fixturePath,
|
||||||
|
'baderech.pdf',
|
||||||
|
'application/pdf',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,18 @@ use App\Element\Element;
|
||||||
use App\Element\UseCases\GetElement\GetElement;
|
use App\Element\UseCases\GetElement\GetElement;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdateIconImage;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdatePdf;
|
||||||
use App\Element\UseCases\UpdateElement\UpdatePdfPath;
|
use App\Element\UseCases\UpdateElement\UpdatePdfPath;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
||||||
use App\Set\Set as DomainSet;
|
use App\Set\Set as DomainSet;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
use Tests\Fakes\FakeElementRepository;
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeFileUploader;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class ElementControllerTest extends TestCase
|
class ElementControllerTest extends TestCase
|
||||||
|
|
@ -23,20 +28,35 @@ class ElementControllerTest extends TestCase
|
||||||
|
|
||||||
private FakeElementRepository $elementRepo;
|
private FakeElementRepository $elementRepo;
|
||||||
|
|
||||||
|
private FakeFileUploader $fileUploader;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->elementRepo = new FakeElementRepository();
|
$this->elementRepo = new FakeElementRepository();
|
||||||
|
$this->fileUploader = new FakeFileUploader();
|
||||||
$getElement = new GetElement($this->elementRepo);
|
$getElement = new GetElement($this->elementRepo);
|
||||||
$updateElement = new UpdateElement(
|
$updateElement = new UpdateElement(
|
||||||
new UpdateTitle($this->elementRepo),
|
new UpdateTitle($this->elementRepo),
|
||||||
new UpdateDescription($this->elementRepo),
|
new UpdateDescription($this->elementRepo),
|
||||||
new UpdateIconImageUrl($this->elementRepo),
|
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
|
||||||
new UpdateRichText($this->elementRepo),
|
new UpdateRichText($this->elementRepo),
|
||||||
new UpdatePdfPath($this->elementRepo),
|
new UpdatePdfPath($this->elementRepo, $this->fileUploader),
|
||||||
new UpdateYoutubeUrl($this->elementRepo),
|
new UpdateYoutubeUrl($this->elementRepo),
|
||||||
|
new UpdateIconImage(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
),
|
||||||
|
new UpdatePdf(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
),
|
||||||
$this->elementRepo,
|
$this->elementRepo,
|
||||||
);
|
);
|
||||||
$this->controller = new ElementController($getElement, $updateElement);
|
$this->controller = new ElementController(
|
||||||
|
$getElement,
|
||||||
|
$updateElement,
|
||||||
|
$this->fileUploader,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShowReturnsElementPayload(): void
|
public function testShowReturnsElementPayload(): void
|
||||||
|
|
@ -135,6 +155,88 @@ class ElementControllerTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUpdateWithIconImageReturnsElementPayload(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$element = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'<p>A structured path for growth</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$fixturePath = __DIR__ . '/../../fixtures/icon.png';
|
||||||
|
$request = new Request([], [
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
], [], [], [
|
||||||
|
'iconImage' => new UploadedFile(
|
||||||
|
$fixturePath,
|
||||||
|
'icon.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
], ['REQUEST_METHOD' => 'POST']);
|
||||||
|
|
||||||
|
$response = $this->controller->update($request);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getContent(), true);
|
||||||
|
$this->assertSame($element->getId(), $body['element']['id']);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://test.local/storage/element-icons/fake-icon.png',
|
||||||
|
$body['element']['iconImageUrl'],
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons',
|
||||||
|
$this->fileUploader->uploads[0]['folder'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateWithPdfReturnsElementPayload(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$element = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'<p>A structured path for growth</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$fixturePath = __DIR__ . '/../../fixtures/baderech.pdf';
|
||||||
|
$request = new Request([], [
|
||||||
|
'elementId' => (string) $element->getId(),
|
||||||
|
], [], [], [
|
||||||
|
'pdf' => new UploadedFile(
|
||||||
|
$fixturePath,
|
||||||
|
'baderech.pdf',
|
||||||
|
'application/pdf',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
], ['REQUEST_METHOD' => 'POST']);
|
||||||
|
|
||||||
|
$response = $this->controller->update($request);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getContent(), true);
|
||||||
|
$this->assertSame($element->getId(), $body['element']['id']);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://test.local/storage/element-pdfs/fake-baderech.pdf',
|
||||||
|
$body['element']['pdfPath'],
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs',
|
||||||
|
$this->fileUploader->uploads[0]['folder'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function createSet(int $id, string $name): DomainSet
|
private function createSet(int $id, string $name): DomainSet
|
||||||
{
|
{
|
||||||
return new DomainSet(
|
return new DomainSet(
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,19 @@ use App\Element\UseCases\UpdateElement\UpdateYoutubeUrlRequest;
|
||||||
use App\Exceptions\BadRequestException;
|
use App\Exceptions\BadRequestException;
|
||||||
use App\Set\Set as DomainSet;
|
use App\Set\Set as DomainSet;
|
||||||
use Tests\Fakes\FakeElementRepository;
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeFileUploader;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class UpdateElementFieldsTest extends TestCase
|
class UpdateElementFieldsTest extends TestCase
|
||||||
{
|
{
|
||||||
private FakeElementRepository $elementRepo;
|
private FakeElementRepository $elementRepo;
|
||||||
|
|
||||||
|
private FakeFileUploader $fileUploader;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->elementRepo = new FakeElementRepository();
|
$this->elementRepo = new FakeElementRepository();
|
||||||
|
$this->fileUploader = new FakeFileUploader();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdateTitleUpdatesOnlyTitle(): void
|
public function testUpdateTitleUpdatesOnlyTitle(): void
|
||||||
|
|
@ -88,7 +92,10 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
public function testUpdateIconImageUrlClearsEmptyString(): void
|
public function testUpdateIconImageUrlClearsEmptyString(): void
|
||||||
{
|
{
|
||||||
$element = $this->createFilledElement();
|
$element = $this->createFilledElement();
|
||||||
$updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo);
|
$updateIconImageUrl = new UpdateIconImageUrl(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
);
|
||||||
|
|
||||||
$updatedElement = $updateIconImageUrl->execute(
|
$updatedElement = $updateIconImageUrl->execute(
|
||||||
new UpdateIconImageUrlRequest(
|
new UpdateIconImageUrlRequest(
|
||||||
|
|
@ -99,6 +106,10 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
|
|
||||||
$this->assertNull($updatedElement->getIconImageUrl());
|
$this->assertNull($updatedElement->getIconImageUrl());
|
||||||
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
||||||
|
$this->assertSame(
|
||||||
|
['/assets/original-icon.png'],
|
||||||
|
$this->fileUploader->deletedPaths,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdateRichTextUpdatesOnlyRichText(): void
|
public function testUpdateRichTextUpdatesOnlyRichText(): void
|
||||||
|
|
@ -126,7 +137,10 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
public function testUpdatePdfPathClearsEmptyString(): void
|
public function testUpdatePdfPathClearsEmptyString(): void
|
||||||
{
|
{
|
||||||
$element = $this->createFilledElement();
|
$element = $this->createFilledElement();
|
||||||
$updatePdfPath = new UpdatePdfPath($this->elementRepo);
|
$updatePdfPath = new UpdatePdfPath(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
);
|
||||||
|
|
||||||
$updatedElement = $updatePdfPath->execute(
|
$updatedElement = $updatePdfPath->execute(
|
||||||
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '')
|
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '')
|
||||||
|
|
@ -134,6 +148,10 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
|
|
||||||
$this->assertNull($updatedElement->getPdfPath());
|
$this->assertNull($updatedElement->getPdfPath());
|
||||||
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
||||||
|
$this->assertSame(
|
||||||
|
['/assets/pdfs/original.pdf'],
|
||||||
|
$this->fileUploader->deletedPaths,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdateYoutubeUrlClearsEmptyString(): void
|
public function testUpdateYoutubeUrlClearsEmptyString(): void
|
||||||
|
|
@ -154,6 +172,16 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
|
|
||||||
private function createFilledElement(): Element
|
private function createFilledElement(): Element
|
||||||
{
|
{
|
||||||
|
return $this->createElementWithMedia(
|
||||||
|
'/assets/original-icon.png',
|
||||||
|
'/assets/pdfs/original.pdf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElementWithMedia(
|
||||||
|
?string $iconImageUrl,
|
||||||
|
?string $pdfPath,
|
||||||
|
): Element {
|
||||||
$set = new DomainSet(
|
$set = new DomainSet(
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Baderech',
|
name: 'Baderech',
|
||||||
|
|
@ -165,9 +193,9 @@ class UpdateElementFieldsTest extends TestCase
|
||||||
set: $set,
|
set: $set,
|
||||||
title: 'Original title',
|
title: 'Original title',
|
||||||
description: 'Original description',
|
description: 'Original description',
|
||||||
iconImageUrl: '/assets/original-icon.png',
|
iconImageUrl: $iconImageUrl,
|
||||||
richText: '<p>Original rich text</p>',
|
richText: '<p>Original rich text</p>',
|
||||||
pdfPath: '/assets/pdfs/original.pdf',
|
pdfPath: $pdfPath,
|
||||||
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
|
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
|
||||||
parentElement: null,
|
parentElement: null,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ namespace Tests\Unit\Element\UseCases;
|
||||||
use App\Element\CreateElementDto;
|
use App\Element\CreateElementDto;
|
||||||
use App\Element\Element;
|
use App\Element\Element;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElementRequest;
|
use App\Element\UseCases\UpdateElement\UpdateElementRequest;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdateIconImage;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
||||||
use App\Element\UseCases\UpdateElement\UpdatePdfPath;
|
use App\Element\UseCases\UpdateElement\UpdatePdfPath;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdatePdf;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
||||||
|
|
@ -16,24 +18,36 @@ use App\Exceptions\BadRequestException;
|
||||||
use App\Exceptions\NotFoundException;
|
use App\Exceptions\NotFoundException;
|
||||||
use App\Set\Set as DomainSet;
|
use App\Set\Set as DomainSet;
|
||||||
use Tests\Fakes\FakeElementRepository;
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeFileUploader;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class UpdateElementTest extends TestCase
|
class UpdateElementTest extends TestCase
|
||||||
{
|
{
|
||||||
private FakeElementRepository $elementRepo;
|
private FakeElementRepository $elementRepo;
|
||||||
|
|
||||||
|
private FakeFileUploader $fileUploader;
|
||||||
|
|
||||||
private UpdateElement $updateElement;
|
private UpdateElement $updateElement;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->elementRepo = new FakeElementRepository();
|
$this->elementRepo = new FakeElementRepository();
|
||||||
|
$this->fileUploader = new FakeFileUploader();
|
||||||
$this->updateElement = new UpdateElement(
|
$this->updateElement = new UpdateElement(
|
||||||
new UpdateTitle($this->elementRepo),
|
new UpdateTitle($this->elementRepo),
|
||||||
new UpdateDescription($this->elementRepo),
|
new UpdateDescription($this->elementRepo),
|
||||||
new UpdateIconImageUrl($this->elementRepo),
|
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
|
||||||
new UpdateRichText($this->elementRepo),
|
new UpdateRichText($this->elementRepo),
|
||||||
new UpdatePdfPath($this->elementRepo),
|
new UpdatePdfPath($this->elementRepo, $this->fileUploader),
|
||||||
new UpdateYoutubeUrl($this->elementRepo),
|
new UpdateYoutubeUrl($this->elementRepo),
|
||||||
|
new UpdateIconImage(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
),
|
||||||
|
new UpdatePdf(
|
||||||
|
$this->elementRepo,
|
||||||
|
$this->fileUploader,
|
||||||
|
),
|
||||||
$this->elementRepo,
|
$this->elementRepo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +75,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: '<p>Updated rich text</p>',
|
richText: '<p>Updated rich text</p>',
|
||||||
pdfPath: '/assets/pdfs/updated.pdf',
|
pdfPath: '/assets/pdfs/updated.pdf',
|
||||||
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -115,6 +137,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: '<p>Updated rich text</p>',
|
richText: '<p>Updated rich text</p>',
|
||||||
pdfPath: '',
|
pdfPath: '',
|
||||||
youtubeUrl: '',
|
youtubeUrl: '',
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -146,6 +176,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: null,
|
richText: null,
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -172,6 +210,58 @@ class UpdateElementTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUpdatesUploadedFiles(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$element = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Original title',
|
||||||
|
'Original description',
|
||||||
|
null,
|
||||||
|
'<p>Original rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
$updatedElement = $this->updateElement->execute(
|
||||||
|
new UpdateElementRequest(
|
||||||
|
id: $element->getId(),
|
||||||
|
title: null,
|
||||||
|
description: null,
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: null,
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
iconImageContents: 'binary-image-bytes',
|
||||||
|
iconImageOriginalName: 'icon.png',
|
||||||
|
iconImageMimeType: 'image/png',
|
||||||
|
iconImageSizeBytes: 1024,
|
||||||
|
pdfContents: 'binary-pdf-bytes',
|
||||||
|
pdfOriginalName: 'baderech.pdf',
|
||||||
|
pdfMimeType: 'application/pdf',
|
||||||
|
pdfSizeBytes: 1024,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons/fake-icon.png',
|
||||||
|
$updatedElement->getIconImageUrl(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs/fake-baderech.pdf',
|
||||||
|
$updatedElement->getPdfPath(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons',
|
||||||
|
$this->fileUploader->uploads[0]['folder'],
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs',
|
||||||
|
$this->fileUploader->uploads[1]['folder'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testThrowsWhenIdMissing(): void
|
public function testThrowsWhenIdMissing(): void
|
||||||
{
|
{
|
||||||
$this->expectException(BadRequestException::class);
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
@ -185,6 +275,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: '<p>Updated rich text</p>',
|
richText: '<p>Updated rich text</p>',
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,6 +299,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: null,
|
richText: null,
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +323,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: '<p>Updated rich text</p>',
|
richText: '<p>Updated rich text</p>',
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +347,14 @@ class UpdateElementTest extends TestCase
|
||||||
richText: '<p>Updated rich text</p>',
|
richText: '<p>Updated rich text</p>',
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
175
backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php
Normal file
175
backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Element\UseCases;
|
||||||
|
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdateIconImage;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdateIconImageRequest;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Set\Set as DomainSet;
|
||||||
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeFileUploader;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class UpdateIconImageTest extends TestCase
|
||||||
|
{
|
||||||
|
private FakeElementRepository $elementRepository;
|
||||||
|
|
||||||
|
private FakeFileUploader $fileUploader;
|
||||||
|
|
||||||
|
private UpdateIconImage $useCase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->elementRepository = new FakeElementRepository();
|
||||||
|
$this->fileUploader = new FakeFileUploader();
|
||||||
|
$this->useCase = new UpdateIconImage(
|
||||||
|
$this->elementRepository,
|
||||||
|
$this->fileUploader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* iconImageContents: string,
|
||||||
|
* iconImageOriginalName: string,
|
||||||
|
* iconImageMimeType: string,
|
||||||
|
* iconImageSizeBytes: int
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function imageArgs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'iconImageContents' => 'binary-image-bytes',
|
||||||
|
'iconImageOriginalName' => 'icon.png',
|
||||||
|
'iconImageMimeType' => 'image/png',
|
||||||
|
'iconImageSizeBytes' => 1024,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUploadsIconImageAndStoresPath(): void
|
||||||
|
{
|
||||||
|
$element = $this->createElement();
|
||||||
|
|
||||||
|
$updatedElement = $this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
...$this->imageArgs(),
|
||||||
|
id: $element->getId(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons/fake-icon.png',
|
||||||
|
$updatedElement->getIconImageUrl(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons/fake-icon.png',
|
||||||
|
$this->elementRepository
|
||||||
|
->find($element->getId())
|
||||||
|
->getIconImageUrl(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-icons',
|
||||||
|
$this->fileUploader->uploads[0]['folder'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIdThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('id is required');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
...$this->imageArgs(),
|
||||||
|
id: null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIconImageThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('icon image is required');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
id: 1,
|
||||||
|
iconImageContents: null,
|
||||||
|
iconImageOriginalName: null,
|
||||||
|
iconImageMimeType: null,
|
||||||
|
iconImageSizeBytes: null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisallowedMimeThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage(
|
||||||
|
'icon image must be a jpeg, png or webp image',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
id: 1,
|
||||||
|
iconImageContents: 'not-an-image',
|
||||||
|
iconImageOriginalName: 'icon.pdf',
|
||||||
|
iconImageMimeType: 'application/pdf',
|
||||||
|
iconImageSizeBytes: 1024,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOversizedIconImageThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('icon image must be 5MB or smaller');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
id: 1,
|
||||||
|
iconImageContents: 'big',
|
||||||
|
iconImageOriginalName: 'icon.png',
|
||||||
|
iconImageMimeType: 'image/png',
|
||||||
|
iconImageSizeBytes: 6 * 1024 * 1024,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingElementThrowsNotFound(): void
|
||||||
|
{
|
||||||
|
$this->expectException(NotFoundException::class);
|
||||||
|
$this->expectExceptionMessage('Element not found');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdateIconImageRequest(
|
||||||
|
...$this->imageArgs(),
|
||||||
|
id: 999,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElement(): Element
|
||||||
|
{
|
||||||
|
$set = new DomainSet(
|
||||||
|
id: 1,
|
||||||
|
name: 'Baderech',
|
||||||
|
description: 'Baderech description',
|
||||||
|
iconImageUrl: '/assets/baderech-icon.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Original title',
|
||||||
|
description: 'Original description',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
173
backend/tests/Unit/Element/UseCases/UpdatePdfTest.php
Normal file
173
backend/tests/Unit/Element/UseCases/UpdatePdfTest.php
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Element\UseCases;
|
||||||
|
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdatePdf;
|
||||||
|
use App\Element\UseCases\UpdateElement\UpdatePdfRequest;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Set\Set as DomainSet;
|
||||||
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeFileUploader;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class UpdatePdfTest extends TestCase
|
||||||
|
{
|
||||||
|
private FakeElementRepository $elementRepository;
|
||||||
|
|
||||||
|
private FakeFileUploader $fileUploader;
|
||||||
|
|
||||||
|
private UpdatePdf $useCase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->elementRepository = new FakeElementRepository();
|
||||||
|
$this->fileUploader = new FakeFileUploader();
|
||||||
|
$this->useCase = new UpdatePdf(
|
||||||
|
$this->elementRepository,
|
||||||
|
$this->fileUploader,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* pdfContents: string,
|
||||||
|
* pdfOriginalName: string,
|
||||||
|
* pdfMimeType: string,
|
||||||
|
* pdfSizeBytes: int
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function pdfArgs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'pdfContents' => 'binary-pdf-bytes',
|
||||||
|
'pdfOriginalName' => 'baderech.pdf',
|
||||||
|
'pdfMimeType' => 'application/pdf',
|
||||||
|
'pdfSizeBytes' => 1024,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUploadsPdfAndStoresPath(): void
|
||||||
|
{
|
||||||
|
$element = $this->createElement();
|
||||||
|
|
||||||
|
$updatedElement = $this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
...$this->pdfArgs(),
|
||||||
|
id: $element->getId(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs/fake-baderech.pdf',
|
||||||
|
$updatedElement->getPdfPath(),
|
||||||
|
);
|
||||||
|
$storedElement = $this->elementRepository->find($element->getId());
|
||||||
|
$this->assertNotNull($storedElement);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs/fake-baderech.pdf',
|
||||||
|
$storedElement->getPdfPath(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'element-pdfs',
|
||||||
|
$this->fileUploader->uploads[0]['folder'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIdThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('id is required');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
...$this->pdfArgs(),
|
||||||
|
id: null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullPdfThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('pdf is required');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
id: 1,
|
||||||
|
pdfContents: null,
|
||||||
|
pdfOriginalName: null,
|
||||||
|
pdfMimeType: null,
|
||||||
|
pdfSizeBytes: null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisallowedMimeThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('pdf must be a pdf file');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
id: 1,
|
||||||
|
pdfContents: 'not-a-pdf',
|
||||||
|
pdfOriginalName: 'icon.png',
|
||||||
|
pdfMimeType: 'image/png',
|
||||||
|
pdfSizeBytes: 1024,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOversizedPdfThrowsBadRequest(): void
|
||||||
|
{
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('pdf must be 10MB or smaller');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
id: 1,
|
||||||
|
pdfContents: 'big',
|
||||||
|
pdfOriginalName: 'baderech.pdf',
|
||||||
|
pdfMimeType: 'application/pdf',
|
||||||
|
pdfSizeBytes: 11 * 1024 * 1024,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingElementThrowsNotFound(): void
|
||||||
|
{
|
||||||
|
$this->expectException(NotFoundException::class);
|
||||||
|
$this->expectExceptionMessage('Element not found');
|
||||||
|
|
||||||
|
$this->useCase->execute(
|
||||||
|
new UpdatePdfRequest(
|
||||||
|
...$this->pdfArgs(),
|
||||||
|
id: 999,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElement(): Element
|
||||||
|
{
|
||||||
|
$set = new DomainSet(
|
||||||
|
id: 1,
|
||||||
|
name: 'Baderech',
|
||||||
|
description: 'Baderech description',
|
||||||
|
iconImageUrl: '/assets/baderech-icon.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
title: 'Original title',
|
||||||
|
description: 'Original description',
|
||||||
|
iconImageUrl: null,
|
||||||
|
richText: '',
|
||||||
|
pdfPath: null,
|
||||||
|
youtubeUrl: null,
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
backend/tests/fixtures/baderech.pdf
vendored
Normal file
BIN
backend/tests/fixtures/baderech.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/tests/fixtures/icon.png
vendored
Normal file
BIN
backend/tests/fixtures/icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -12,26 +12,17 @@ function loginAsAdmin(): void {
|
||||||
function fillElementForm(
|
function fillElementForm(
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
iconImageUrl: string,
|
|
||||||
richText: string,
|
richText: string,
|
||||||
pdfPath: string,
|
|
||||||
youtubeUrl: string,
|
youtubeUrl: string,
|
||||||
): void {
|
): void {
|
||||||
cy.get('[data-cy="admin-element-title"]').clear().type(title)
|
cy.get('[data-cy="admin-element-title"]').clear().type(title)
|
||||||
cy.get('[data-cy="admin-element-description"]').clear().type(description)
|
cy.get('[data-cy="admin-element-description"]').clear().type(description)
|
||||||
cy.get('[data-cy="admin-element-icon-image-url"]')
|
|
||||||
.clear()
|
|
||||||
.type(iconImageUrl)
|
|
||||||
cy.get('[data-cy="admin-element-rich-text"]').clear()
|
cy.get('[data-cy="admin-element-rich-text"]').clear()
|
||||||
if (richText !== '') {
|
if (richText !== '') {
|
||||||
cy.get('[data-cy="admin-element-rich-text"]').type(richText, {
|
cy.get('[data-cy="admin-element-rich-text"]').type(richText, {
|
||||||
parseSpecialCharSequences: false,
|
parseSpecialCharSequences: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
cy.get('[data-cy="admin-element-pdf-path"]').clear()
|
|
||||||
if (pdfPath !== '') {
|
|
||||||
cy.get('[data-cy="admin-element-pdf-path"]').type(pdfPath)
|
|
||||||
}
|
|
||||||
cy.get('[data-cy="admin-element-youtube-url"]').clear()
|
cy.get('[data-cy="admin-element-youtube-url"]').clear()
|
||||||
if (youtubeUrl !== '') {
|
if (youtubeUrl !== '') {
|
||||||
cy.get('[data-cy="admin-element-youtube-url"]').type(youtubeUrl)
|
cy.get('[data-cy="admin-element-youtube-url"]').type(youtubeUrl)
|
||||||
|
|
@ -79,7 +70,6 @@ describe('admin element editing', () => {
|
||||||
it('saves element edits and restores the seed content through the UI', () => {
|
it('saves element edits and restores the seed content through the UI', () => {
|
||||||
const originalTitle = 'Baderech HaAvodah'
|
const originalTitle = 'Baderech HaAvodah'
|
||||||
const originalDescription = 'a structured path for inner and outer growth'
|
const originalDescription = 'a structured path for inner and outer growth'
|
||||||
const originalIconImageUrl = '/assets/baderech-haavodah-icon.png'
|
|
||||||
const updatedTitle = 'Baderech HaAvodah Updated'
|
const updatedTitle = 'Baderech HaAvodah Updated'
|
||||||
const updatedDescription = 'Updated admin description'
|
const updatedDescription = 'Updated admin description'
|
||||||
const updatedRichText = '<p>Updated admin rich text</p>'
|
const updatedRichText = '<p>Updated admin rich text</p>'
|
||||||
|
|
@ -90,10 +80,8 @@ describe('admin element editing', () => {
|
||||||
fillElementForm(
|
fillElementForm(
|
||||||
updatedTitle,
|
updatedTitle,
|
||||||
updatedDescription,
|
updatedDescription,
|
||||||
originalIconImageUrl,
|
|
||||||
updatedRichText,
|
updatedRichText,
|
||||||
'',
|
'',
|
||||||
'',
|
|
||||||
)
|
)
|
||||||
cy.get('[data-cy="admin-element-save"]').click()
|
cy.get('[data-cy="admin-element-save"]').click()
|
||||||
|
|
||||||
|
|
@ -110,8 +98,6 @@ describe('admin element editing', () => {
|
||||||
fillElementForm(
|
fillElementForm(
|
||||||
originalTitle,
|
originalTitle,
|
||||||
originalDescription,
|
originalDescription,
|
||||||
originalIconImageUrl,
|
|
||||||
'',
|
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
)
|
)
|
||||||
|
|
@ -120,4 +106,89 @@ describe('admin element editing', () => {
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('contain.text', 'Saved')
|
.and('contain.text', 'Saved')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uploads icon and pdf files immediately through the UI', () => {
|
||||||
|
cy.resetDb()
|
||||||
|
loginAsAdmin()
|
||||||
|
cy.visit('/admin/element/1')
|
||||||
|
cy.intercept('POST', /\/api\/element\/update$/).as('updateElement')
|
||||||
|
|
||||||
|
cy.get('[data-cy="admin-element-icon-image-input"]').selectFile(
|
||||||
|
'public/assets/baderech-haavodah-icon.png',
|
||||||
|
{ force: true },
|
||||||
|
)
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
cy.get('[data-cy="admin-element-icon-image-status"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('contain.text', 'Icon image updated')
|
||||||
|
cy.get('[data-cy="admin-element-current-icon"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.attr', 'src')
|
||||||
|
.and('include', '/storage/element-icons/')
|
||||||
|
|
||||||
|
cy.get('[data-cy="admin-element-pdf-input"]').selectFile(
|
||||||
|
'public/assets/pdfs/baderech.pdf',
|
||||||
|
{ force: true },
|
||||||
|
)
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
cy.get('[data-cy="admin-element-pdf-status"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('contain.text', 'PDF updated')
|
||||||
|
cy.get('[data-cy="admin-element-current-pdf"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.attr', 'href')
|
||||||
|
.and('include', '/storage/element-pdfs/')
|
||||||
|
|
||||||
|
cy.contains('header.site-header a', 'View Element').click()
|
||||||
|
cy.get('[data-cy="element-icon"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.attr', 'src')
|
||||||
|
.and('include', '/storage/element-icons/')
|
||||||
|
cy.get('[data-cy="element-pdf-link"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.attr', 'href')
|
||||||
|
.and('include', '/storage/element-pdfs/')
|
||||||
|
|
||||||
|
cy.resetDb()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes uploaded icon and pdf files immediately through the UI', () => {
|
||||||
|
cy.resetDb()
|
||||||
|
loginAsAdmin()
|
||||||
|
cy.visit('/admin/element/1')
|
||||||
|
cy.intercept('POST', /\/api\/element\/update$/).as('updateElement')
|
||||||
|
|
||||||
|
cy.get('[data-cy="admin-element-icon-image-input"]').selectFile(
|
||||||
|
'public/assets/baderech-haavodah-icon.png',
|
||||||
|
{ force: true },
|
||||||
|
)
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
cy.get('[data-cy="admin-element-pdf-input"]').selectFile(
|
||||||
|
'public/assets/pdfs/baderech.pdf',
|
||||||
|
{ force: true },
|
||||||
|
)
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
|
||||||
|
cy.contains('button', 'Remove icon image').click()
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
cy.get('[data-cy="admin-element-icon-image-status"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('contain.text', 'Icon image removed')
|
||||||
|
cy.get('[data-cy="admin-element-current-icon"]').should('not.exist')
|
||||||
|
cy.contains('No icon image').should('be.visible')
|
||||||
|
|
||||||
|
cy.contains('button', 'Remove PDF').click()
|
||||||
|
cy.wait('@updateElement')
|
||||||
|
cy.get('[data-cy="admin-element-pdf-status"]')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('contain.text', 'PDF removed')
|
||||||
|
cy.get('[data-cy="admin-element-current-pdf"]').should('not.exist')
|
||||||
|
cy.contains('No PDF').should('be.visible')
|
||||||
|
|
||||||
|
cy.contains('header.site-header a', 'View Element').click()
|
||||||
|
cy.get('[data-cy="element-icon"]').should('not.exist')
|
||||||
|
cy.get('[data-cy="element-pdf-link"]').should('not.exist')
|
||||||
|
|
||||||
|
cy.resetDb()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ describe('media page sets', () => {
|
||||||
cy.get('[data-cy="element-icon"]')
|
cy.get('[data-cy="element-icon"]')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('have.attr', 'src')
|
.and('have.attr', 'src')
|
||||||
.and('include', '/assets/baderech-haavodah-icon.png')
|
.and('include', '/storage/element-icons/baderech-haavodah-icon.png')
|
||||||
cy.get('[data-cy="element-rich-text"]').should('not.exist')
|
cy.get('[data-cy="element-rich-text"]').should('not.exist')
|
||||||
cy.get('[data-cy="element-youtube-embed"]')
|
cy.get('[data-cy="element-youtube-embed"]')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
|
@ -117,9 +117,12 @@ describe('media page sets', () => {
|
||||||
.should('have.css', 'text-align', 'center')
|
.should('have.css', 'text-align', 'center')
|
||||||
cy.get('[data-cy="element-youtube-embed"]').should('not.exist')
|
cy.get('[data-cy="element-youtube-embed"]').should('not.exist')
|
||||||
cy.contains('[data-cy="element-pdf-link"]', 'View PDF')
|
cy.contains('[data-cy="element-pdf-link"]', 'View PDF')
|
||||||
|
.as('pdfLink')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('have.attr', 'href', '/assets/pdfs/baderech.pdf')
|
|
||||||
.and('have.attr', 'target', '_blank')
|
.and('have.attr', 'target', '_blank')
|
||||||
|
cy.get('@pdfLink')
|
||||||
|
.and('have.attr', 'href')
|
||||||
|
.and('include', '/storage/element-pdfs/baderech.pdf')
|
||||||
cy.contains('[data-cy="child-element-link"]', introductionAudioTitle)
|
cy.contains('[data-cy="child-element-link"]', introductionAudioTitle)
|
||||||
.as('introductionAudioLink')
|
.as('introductionAudioLink')
|
||||||
.should('have.attr', 'href', '/element/8')
|
.should('have.attr', 'href', '/element/8')
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@ interface ElementResponse {
|
||||||
export interface UpdateElementInput {
|
export interface UpdateElementInput {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
iconImageUrl: string
|
|
||||||
richText: string
|
richText: string
|
||||||
pdfPath: string
|
|
||||||
youtubeUrl: string
|
youtubeUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,11 +30,21 @@ interface UpdateElementResponse {
|
||||||
element: Element
|
element: Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ElementPatchInput {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
iconImageUrl?: string
|
||||||
|
richText?: string
|
||||||
|
pdfPath?: string
|
||||||
|
youtubeUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ErrorResponse {
|
interface ErrorResponse {
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
|
||||||
|
const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update`
|
||||||
|
|
||||||
export const useElementsStore = defineStore('elements', () => {
|
export const useElementsStore = defineStore('elements', () => {
|
||||||
const element = ref<Element | null>(null)
|
const element = ref<Element | null>(null)
|
||||||
|
|
@ -44,6 +52,8 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
const isUploadingIconImage = ref(false)
|
||||||
|
const isUploadingPdf = ref(false)
|
||||||
const saveError = ref<string | null>(null)
|
const saveError = ref<string | null>(null)
|
||||||
|
|
||||||
async function fetchElement(elementId: string): Promise<void> {
|
async function fetchElement(elementId: string): Promise<void> {
|
||||||
|
|
@ -80,59 +90,142 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateElement(elementId: string, input: UpdateElementInput): Promise<boolean> {
|
async function updateElement(elementId: string, input: UpdateElementInput): Promise<boolean> {
|
||||||
|
return await saveElementPatch(elementId, input, 'Could not save element')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearElementIconImage(elementId: string): Promise<boolean> {
|
||||||
|
return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearElementPdf(elementId: string): Promise<boolean> {
|
||||||
|
return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveElementPatch(
|
||||||
|
elementId: string,
|
||||||
|
input: ElementPatchInput,
|
||||||
|
failureMessage: string,
|
||||||
|
): Promise<boolean> {
|
||||||
saveError.value = null
|
saveError.value = null
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encodedElementId = encodeURIComponent(elementId)
|
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||||
const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}`
|
method: 'POST',
|
||||||
const response = await fetch(elementUrl, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify(input),
|
body: JSON.stringify({ elementId, ...input }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 401) {
|
return await handleElementResponse(response, failureMessage)
|
||||||
saveError.value = 'Please log in again'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
saveError.value = 'Element not found'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 400) {
|
|
||||||
const data: ErrorResponse = await response.json()
|
|
||||||
saveError.value = data.error ?? 'Could not save element'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
saveError.value = 'Could not save element'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: UpdateElementResponse = await response.json()
|
|
||||||
element.value = data.element
|
|
||||||
return true
|
|
||||||
} catch {
|
} catch {
|
||||||
saveError.value = 'Network error - could not save element'
|
saveError.value = `Network error - ${failureMessage.toLowerCase()}`
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadElementIconImage(elementId: string, file: File): Promise<boolean> {
|
||||||
|
saveError.value = null
|
||||||
|
isUploadingIconImage.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('elementId', elementId)
|
||||||
|
formData.append('iconImage', file)
|
||||||
|
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await handleElementResponse(response, 'Could not upload icon image')
|
||||||
|
} catch {
|
||||||
|
saveError.value = 'Network error - could not upload icon image'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isUploadingIconImage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadElementPdf(elementId: string, file: File): Promise<boolean> {
|
||||||
|
saveError.value = null
|
||||||
|
isUploadingPdf.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('elementId', elementId)
|
||||||
|
formData.append('pdf', file)
|
||||||
|
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await handleElementResponse(response, 'Could not upload PDF')
|
||||||
|
} catch {
|
||||||
|
saveError.value = 'Network error - could not upload PDF'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isUploadingPdf.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleElementResponse(
|
||||||
|
response: Response,
|
||||||
|
failureMessage: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (response.status === 401) {
|
||||||
|
saveError.value = 'Please log in again'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
saveError.value = 'Element not found'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 400) {
|
||||||
|
saveError.value = await errorMessage(response, failureMessage)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
saveError.value = failureMessage
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: UpdateElementResponse = await response.json()
|
||||||
|
element.value = data.element
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function errorMessage(response: Response, fallbackMessage: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const data: ErrorResponse = await response.json()
|
||||||
|
return data.error ?? fallbackMessage
|
||||||
|
} catch {
|
||||||
|
return fallbackMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
childElements,
|
childElements,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isUploadingIconImage,
|
||||||
|
isUploadingPdf,
|
||||||
saveError,
|
saveError,
|
||||||
fetchElement,
|
fetchElement,
|
||||||
updateElement,
|
updateElement,
|
||||||
|
uploadElementIconImage,
|
||||||
|
uploadElementPdf,
|
||||||
|
clearElementIconImage,
|
||||||
|
clearElementPdf,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,24 @@ import { useElementsStore } from '@/stores/elements'
|
||||||
interface ElementForm {
|
interface ElementForm {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
iconImageUrl: string
|
|
||||||
richText: string
|
richText: string
|
||||||
pdfPath: string
|
|
||||||
youtubeUrl: string
|
youtubeUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const elementsStore = useElementsStore()
|
const elementsStore = useElementsStore()
|
||||||
const { element, isLoading, error, isSaving, saveError } = storeToRefs(elementsStore)
|
const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } =
|
||||||
|
storeToRefs(elementsStore)
|
||||||
const savedMessage = ref<string | null>(null)
|
const savedMessage = ref<string | null>(null)
|
||||||
|
const iconImageStatus = ref<string | null>(null)
|
||||||
|
const pdfStatus = ref<string | null>(null)
|
||||||
|
const iconImageInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const pdfInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const form = reactive<ElementForm>({
|
const form = reactive<ElementForm>({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
iconImageUrl: '',
|
|
||||||
richText: '',
|
richText: '',
|
||||||
pdfPath: '',
|
|
||||||
youtubeUrl: '',
|
youtubeUrl: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -54,6 +55,8 @@ watch(
|
||||||
}
|
}
|
||||||
|
|
||||||
savedMessage.value = null
|
savedMessage.value = null
|
||||||
|
iconImageStatus.value = null
|
||||||
|
pdfStatus.value = null
|
||||||
void elementsStore.fetchElement(currentElementId)
|
void elementsStore.fetchElement(currentElementId)
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|
@ -66,9 +69,7 @@ watch(element, (currentElement) => {
|
||||||
|
|
||||||
form.title = currentElement.title
|
form.title = currentElement.title
|
||||||
form.description = currentElement.description
|
form.description = currentElement.description
|
||||||
form.iconImageUrl = currentElement.iconImageUrl ?? ''
|
|
||||||
form.richText = currentElement.richText
|
form.richText = currentElement.richText
|
||||||
form.pdfPath = currentElement.pdfPath ?? ''
|
|
||||||
form.youtubeUrl = currentElement.youtubeUrl ?? ''
|
form.youtubeUrl = currentElement.youtubeUrl ?? ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -81,9 +82,7 @@ async function handleSubmit(): Promise<void> {
|
||||||
const saved = await elementsStore.updateElement(elementId.value, {
|
const saved = await elementsStore.updateElement(elementId.value, {
|
||||||
title: form.title,
|
title: form.title,
|
||||||
description: form.description,
|
description: form.description,
|
||||||
iconImageUrl: form.iconImageUrl,
|
|
||||||
richText: form.richText,
|
richText: form.richText,
|
||||||
pdfPath: form.pdfPath,
|
|
||||||
youtubeUrl: form.youtubeUrl,
|
youtubeUrl: form.youtubeUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -91,6 +90,92 @@ async function handleSubmit(): Promise<void> {
|
||||||
savedMessage.value = 'Saved'
|
savedMessage.value = 'Saved'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseIconImage(): void {
|
||||||
|
iconImageInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
function choosePdf(): void {
|
||||||
|
pdfInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIconImageChange(changeEvent: Event): Promise<void> {
|
||||||
|
const selectedFile = selectedInputFile(changeEvent)
|
||||||
|
if (selectedFile === null || elementId.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedMessage.value = null
|
||||||
|
iconImageStatus.value = null
|
||||||
|
const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile)
|
||||||
|
resetInput(changeEvent)
|
||||||
|
|
||||||
|
if (uploaded) {
|
||||||
|
iconImageStatus.value = 'Icon image updated'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePdfChange(changeEvent: Event): Promise<void> {
|
||||||
|
const selectedFile = selectedInputFile(changeEvent)
|
||||||
|
if (selectedFile === null || elementId.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedMessage.value = null
|
||||||
|
pdfStatus.value = null
|
||||||
|
const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile)
|
||||||
|
resetInput(changeEvent)
|
||||||
|
|
||||||
|
if (uploaded) {
|
||||||
|
pdfStatus.value = 'PDF updated'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveIconImage(): Promise<void> {
|
||||||
|
if (elementId.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedMessage.value = null
|
||||||
|
iconImageStatus.value = null
|
||||||
|
const removed = await elementsStore.clearElementIconImage(elementId.value)
|
||||||
|
if (removed) {
|
||||||
|
iconImageStatus.value = 'Icon image removed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemovePdf(): Promise<void> {
|
||||||
|
if (elementId.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedMessage.value = null
|
||||||
|
pdfStatus.value = null
|
||||||
|
const removed = await elementsStore.clearElementPdf(elementId.value)
|
||||||
|
if (removed) {
|
||||||
|
pdfStatus.value = 'PDF removed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedInputFile(changeEvent: Event): File | null {
|
||||||
|
const target = changeEvent.target
|
||||||
|
if (!(target instanceof HTMLInputElement)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.files === null || target.files.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.files.item(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInput(changeEvent: Event): void {
|
||||||
|
const target = changeEvent.target
|
||||||
|
if (target instanceof HTMLInputElement) {
|
||||||
|
target.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -130,15 +215,53 @@ async function handleSubmit(): Promise<void> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="admin-element-page__field">
|
<section class="admin-element-page__media-field">
|
||||||
<span class="admin-element-page__label">Icon Image URL</span>
|
<span class="admin-element-page__label">Icon Image</span>
|
||||||
<input
|
<img
|
||||||
v-model="form.iconImageUrl"
|
v-if="element.iconImageUrl !== null"
|
||||||
class="admin-element-page__input"
|
:src="element.iconImageUrl"
|
||||||
data-cy="admin-element-icon-image-url"
|
:alt="`${element.title} icon`"
|
||||||
type="text"
|
class="admin-element-page__icon-preview"
|
||||||
|
data-cy="admin-element-current-icon"
|
||||||
/>
|
/>
|
||||||
</label>
|
<p v-else class="admin-element-page__empty-media">No icon image</p>
|
||||||
|
<div class="admin-element-page__media-actions">
|
||||||
|
<input
|
||||||
|
ref="iconImageInput"
|
||||||
|
class="admin-element-page__file-input"
|
||||||
|
data-cy="admin-element-icon-image-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
:disabled="isUploadingIconImage"
|
||||||
|
@change="handleIconImageChange"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="admin-element-page__secondary-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isUploadingIconImage"
|
||||||
|
@click="chooseIconImage"
|
||||||
|
>
|
||||||
|
{{ isUploadingIconImage ? 'Uploading...' : 'Choose icon image' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="element.iconImageUrl !== null"
|
||||||
|
class="admin-element-page__secondary-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isSaving || isUploadingIconImage"
|
||||||
|
@click="handleRemoveIconImage"
|
||||||
|
>
|
||||||
|
Remove icon image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="iconImageStatus !== null"
|
||||||
|
class="admin-element-page__status admin-element-page__status--inline"
|
||||||
|
data-cy="admin-element-icon-image-status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{{ iconImageStatus }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<label class="admin-element-page__field">
|
<label class="admin-element-page__field">
|
||||||
<span class="admin-element-page__label">Rich Text HTML</span>
|
<span class="admin-element-page__label">Rich Text HTML</span>
|
||||||
|
|
@ -150,15 +273,56 @@ async function handleSubmit(): Promise<void> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="admin-element-page__field">
|
<section class="admin-element-page__media-field">
|
||||||
<span class="admin-element-page__label">PDF Path</span>
|
<span class="admin-element-page__label">PDF</span>
|
||||||
<input
|
<a
|
||||||
v-model="form.pdfPath"
|
v-if="element.pdfPath !== null"
|
||||||
class="admin-element-page__input"
|
:href="element.pdfPath"
|
||||||
data-cy="admin-element-pdf-path"
|
class="admin-element-page__pdf-link"
|
||||||
type="text"
|
data-cy="admin-element-current-pdf"
|
||||||
/>
|
target="_blank"
|
||||||
</label>
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
View current PDF
|
||||||
|
</a>
|
||||||
|
<p v-else class="admin-element-page__empty-media">No PDF</p>
|
||||||
|
<div class="admin-element-page__media-actions">
|
||||||
|
<input
|
||||||
|
ref="pdfInput"
|
||||||
|
class="admin-element-page__file-input"
|
||||||
|
data-cy="admin-element-pdf-input"
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
:disabled="isUploadingPdf"
|
||||||
|
@change="handlePdfChange"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="admin-element-page__secondary-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isUploadingPdf"
|
||||||
|
@click="choosePdf"
|
||||||
|
>
|
||||||
|
{{ isUploadingPdf ? 'Uploading...' : 'Choose PDF' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="element.pdfPath !== null"
|
||||||
|
class="admin-element-page__secondary-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="isSaving || isUploadingPdf"
|
||||||
|
@click="handleRemovePdf"
|
||||||
|
>
|
||||||
|
Remove PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="pdfStatus !== null"
|
||||||
|
class="admin-element-page__status admin-element-page__status--inline"
|
||||||
|
data-cy="admin-element-pdf-status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{{ pdfStatus }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<label class="admin-element-page__field">
|
<label class="admin-element-page__field">
|
||||||
<span class="admin-element-page__label">YouTube URL</span>
|
<span class="admin-element-page__label">YouTube URL</span>
|
||||||
|
|
@ -234,6 +398,16 @@ async function handleSubmit(): Promise<void> {
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-element-page__media-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.7rem;
|
||||||
|
padding: 0.9rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-element-page__label {
|
.admin-element-page__label {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -267,6 +441,61 @@ async function handleSubmit(): Promise<void> {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-element-page__icon-preview {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__empty-media {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__media-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__secondary-button,
|
||||||
|
.admin-element-page__pdf-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.55rem 0.95rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-white);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__secondary-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__secondary-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-element-page__secondary-button:hover:not(:disabled),
|
||||||
|
.admin-element-page__secondary-button:focus-visible,
|
||||||
|
.admin-element-page__pdf-link:hover,
|
||||||
|
.admin-element-page__pdf-link:focus-visible {
|
||||||
|
border-color: var(--color-slate);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-element-page__actions {
|
.admin-element-page__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -314,5 +543,11 @@ async function handleSubmit(): Promise<void> {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-element-page__media-actions {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue