Compare commits
No commits in common. "1f24f4e317b386511c9397e4c7114ba049bc9e0b" and "77dc4173b6307e9ff6255869d69b14bcf9c6a313" have entirely different histories.
1f24f4e317
...
77dc4173b6
31 changed files with 103 additions and 1883 deletions
|
|
@ -9,17 +9,14 @@ use App\Element\UseCases\UpdateElement\UpdateElement;
|
|||
use App\Element\UseCases\UpdateElement\UpdateElementRequest;
|
||||
use App\Exceptions\BadRequestException;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Shared\Files\FileUploader;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ElementController
|
||||
{
|
||||
public function __construct(
|
||||
private GetElement $getElement,
|
||||
private UpdateElement $updateElement,
|
||||
private FileUploader $fileUploader,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -55,15 +52,12 @@ class ElementController
|
|||
], 200);
|
||||
}
|
||||
|
||||
public function update(Request $request): JsonResponse
|
||||
public function update(?int $id, Request $request): JsonResponse
|
||||
{
|
||||
$iconImage = $this->uploadedFileInput($request, 'iconImage');
|
||||
$pdf = $this->uploadedFileInput($request, 'pdf');
|
||||
|
||||
try {
|
||||
$element = $this->updateElement->execute(
|
||||
new UpdateElementRequest(
|
||||
id: $this->intInput($request, 'elementId'),
|
||||
id: $id,
|
||||
title: $this->stringInput($request, 'title'),
|
||||
description: $this->stringInput($request, 'description'),
|
||||
iconImageUrl: $this->stringInput(
|
||||
|
|
@ -73,14 +67,6 @@ class ElementController
|
|||
richText: $this->stringInput($request, 'richText'),
|
||||
pdfPath: $this->stringInput($request, 'pdfPath'),
|
||||
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) {
|
||||
|
|
@ -98,31 +84,9 @@ class ElementController
|
|||
], 200);
|
||||
}
|
||||
|
||||
private function intInput(Request $request, string $key): ?int
|
||||
{
|
||||
$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)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -130,45 +94,6 @@ class ElementController
|
|||
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{
|
||||
* id: int,
|
||||
|
|
@ -186,29 +111,10 @@ class ElementController
|
|||
'id' => $element->getId(),
|
||||
'title' => $element->getTitle(),
|
||||
'description' => $element->getDescription(),
|
||||
'iconImageUrl' => $this->storageUrl(
|
||||
$element->getIconImageUrl(),
|
||||
'element-icons/',
|
||||
),
|
||||
'iconImageUrl' => $element->getIconImageUrl(),
|
||||
'richText' => $element->getRichText(),
|
||||
'pdfPath' => $this->storageUrl(
|
||||
$element->getPdfPath(),
|
||||
'element-pdfs/',
|
||||
),
|
||||
'pdfPath' => $element->getPdfPath(),
|
||||
'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,8 +16,6 @@ class UpdateElement
|
|||
private UpdateRichText $updateRichText,
|
||||
private UpdatePdfPath $updatePdfPath,
|
||||
private UpdateYoutubeUrl $updateYoutubeUrl,
|
||||
private UpdateIconImage $updateIconImage,
|
||||
private UpdatePdf $updatePdf,
|
||||
private ElementRepository $elementRepository,
|
||||
) {
|
||||
}
|
||||
|
|
@ -37,9 +35,7 @@ class UpdateElement
|
|||
&& $request->iconImageUrl === null
|
||||
&& $request->richText === null
|
||||
&& $request->pdfPath === null
|
||||
&& $request->youtubeUrl === null
|
||||
&& $request->iconImageContents === null
|
||||
&& $request->pdfContents === null;
|
||||
&& $request->youtubeUrl === null;
|
||||
if ($hasNoFields) {
|
||||
throw new BadRequestException('nothing to update');
|
||||
}
|
||||
|
|
@ -82,28 +78,6 @@ class UpdateElement
|
|||
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);
|
||||
if ($element === null) {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,6 @@ class UpdateElementRequest
|
|||
public ?string $richText,
|
||||
public ?string $pdfPath,
|
||||
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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?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,14 +6,11 @@ use App\Element\Element;
|
|||
use App\Element\ElementRepository;
|
||||
use App\Exceptions\BadRequestException;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Shared\Files\FileUploader;
|
||||
|
||||
class UpdateIconImageUrl
|
||||
{
|
||||
public function __construct(
|
||||
private ElementRepository $elementRepository,
|
||||
private FileUploader $fileUploader,
|
||||
) {
|
||||
public function __construct(private ElementRepository $elementRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,12 +32,9 @@ class UpdateIconImageUrl
|
|||
throw new NotFoundException('Element not found');
|
||||
}
|
||||
|
||||
$iconImageUrl = $this->nullableString($request->iconImageUrl);
|
||||
if ($iconImageUrl === null) {
|
||||
$this->deleteManagedFile($element->getIconImageUrl());
|
||||
}
|
||||
|
||||
$element->setIconImageUrl($iconImageUrl);
|
||||
$element->setIconImageUrl($this->nullableString(
|
||||
$request->iconImageUrl,
|
||||
));
|
||||
|
||||
return $this->elementRepository->update($element);
|
||||
}
|
||||
|
|
@ -53,13 +47,4 @@ class UpdateIconImageUrl
|
|||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function deleteManagedFile(?string $path): void
|
||||
{
|
||||
if ($path === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fileUploader->delete($path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
<?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,14 +6,11 @@ use App\Element\Element;
|
|||
use App\Element\ElementRepository;
|
||||
use App\Exceptions\BadRequestException;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Shared\Files\FileUploader;
|
||||
|
||||
class UpdatePdfPath
|
||||
{
|
||||
public function __construct(
|
||||
private ElementRepository $elementRepository,
|
||||
private FileUploader $fileUploader,
|
||||
) {
|
||||
public function __construct(private ElementRepository $elementRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,12 +32,7 @@ class UpdatePdfPath
|
|||
throw new NotFoundException('Element not found');
|
||||
}
|
||||
|
||||
$pdfPath = $this->nullableString($request->pdfPath);
|
||||
if ($pdfPath === null) {
|
||||
$this->deleteManagedFile($element->getPdfPath());
|
||||
}
|
||||
|
||||
$element->setPdfPath($pdfPath);
|
||||
$element->setPdfPath($this->nullableString($request->pdfPath));
|
||||
|
||||
return $this->elementRepository->update($element);
|
||||
}
|
||||
|
|
@ -53,13 +45,4 @@ class UpdatePdfPath
|
|||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function deleteManagedFile(?string $path): void
|
||||
{
|
||||
if ($path === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fileUploader->delete($path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<?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,8 +8,6 @@ use App\Auth\PasswordHasher;
|
|||
use App\Auth\RandomTokenGenerator;
|
||||
use App\Auth\SystemClock;
|
||||
use App\Auth\TokenGenerator;
|
||||
use App\Shared\Files\FileUploader;
|
||||
use App\Shared\Files\LaravelFileUploader;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
|
@ -28,9 +26,5 @@ class AppServiceProvider extends ServiceProvider
|
|||
Clock::class,
|
||||
SystemClock::class,
|
||||
);
|
||||
$this->app->bind(
|
||||
FileUploader::class,
|
||||
LaravelFileUploader::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?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,7 +6,6 @@ use App\Element\CreateElementDto;
|
|||
use App\Element\ElementRepository;
|
||||
use App\Set\SetRepository;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
|
||||
class ElementSeeder extends Seeder
|
||||
|
|
@ -16,18 +15,11 @@ class ElementSeeder extends Seeder
|
|||
$setRepository = app(SetRepository::class);
|
||||
$elementRepository = app(ElementRepository::class);
|
||||
$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(
|
||||
set: $baderechSet,
|
||||
title: $baderechSet->getName(),
|
||||
description: $baderechSet->getDescription(),
|
||||
iconImageUrl: $rootIconImageUrl,
|
||||
iconImageUrl: '/assets/baderech-haavodah-icon.png',
|
||||
richText: '',
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
|
|
@ -44,7 +36,7 @@ class ElementSeeder extends Seeder
|
|||
. 'unlock the ability to live with strength, confidence, '
|
||||
. 'and hope.',
|
||||
'richText' => $this->introductionRichText(),
|
||||
'pdfPath' => $introductionPdfPath,
|
||||
'pdfPath' => '/assets/pdfs/baderech.pdf',
|
||||
],
|
||||
[
|
||||
'title' => '2. Foundations',
|
||||
|
|
@ -124,22 +116,6 @@ 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
|
||||
{
|
||||
return '<h2 style="text-align: center;">'
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
|
@ -12,5 +12,5 @@ Route::get('/me', [AuthController::class, 'me'])
|
|||
->middleware(AuthMiddleware::class);
|
||||
Route::get('/sets', [SetController::class, 'index']);
|
||||
Route::get('/elements/{id}', [ElementController::class, 'show']);
|
||||
Route::post('/element/update', [ElementController::class, 'update'])
|
||||
Route::patch('/elements/{id}', [ElementController::class, 'update'])
|
||||
->middleware(AuthMiddleware::class);
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<?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,27 +6,14 @@ use App\Element\ElementRepository;
|
|||
use App\Set\SetRepository;
|
||||
use Database\Seeders\DatabaseSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DemoSeedDataTest extends TestCase
|
||||
{
|
||||
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
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$this->seed(DatabaseSeeder::class);
|
||||
|
||||
$setRepository = app(SetRepository::class);
|
||||
|
|
@ -36,13 +23,6 @@ class DemoSeedDataTest extends TestCase
|
|||
$childElements = $elementRepository->findByParentElement($rootElement);
|
||||
|
||||
$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->getYoutubeUrl());
|
||||
$this->assertCount(6, $childElements);
|
||||
|
|
@ -96,10 +76,9 @@ class DemoSeedDataTest extends TestCase
|
|||
$childElements[0]->getRichText(),
|
||||
);
|
||||
$this->assertSame(
|
||||
'element-pdfs/baderech.pdf',
|
||||
'/assets/pdfs/baderech.pdf',
|
||||
$childElements[0]->getPdfPath(),
|
||||
);
|
||||
Storage::disk('public')->assertExists('element-pdfs/baderech.pdf');
|
||||
|
||||
$introductionChildElements = $elementRepository->findByParentElement(
|
||||
$childElements[0],
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ use App\User\UserRepository;
|
|||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ElementsEndpointTest extends TestCase
|
||||
|
|
@ -122,8 +120,7 @@ class ElementsEndpointTest extends TestCase
|
|||
parentElement: null,
|
||||
));
|
||||
|
||||
$response = $this->postJson('/api/element/update', [
|
||||
'elementId' => $element->getId(),
|
||||
$response = $this->patchJson("/api/elements/{$element->getId()}", [
|
||||
'title' => 'Updated title',
|
||||
'description' => 'Updated description',
|
||||
'iconImageUrl' => '/assets/updated-icon.png',
|
||||
|
|
@ -163,8 +160,7 @@ class ElementsEndpointTest extends TestCase
|
|||
|
||||
$response = $this->withCredentials()
|
||||
->withUnencryptedCookie('auth_token', 'valid-token')
|
||||
->postJson('/api/element/update', [
|
||||
'elementId' => $element->getId(),
|
||||
->patchJson("/api/elements/{$element->getId()}", [
|
||||
'title' => 'Updated title',
|
||||
'description' => 'Updated description',
|
||||
'iconImageUrl' => '/assets/updated-icon.png',
|
||||
|
|
@ -193,225 +189,6 @@ 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
|
||||
{
|
||||
$userRepository = app(UserRepository::class);
|
||||
|
|
@ -434,30 +211,4 @@ 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,18 +8,13 @@ use App\Element\Element;
|
|||
use App\Element\UseCases\GetElement\GetElement;
|
||||
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||
use App\Element\UseCases\UpdateElement\UpdateIconImage;
|
||||
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
||||
use App\Element\UseCases\UpdateElement\UpdatePdf;
|
||||
use App\Element\UseCases\UpdateElement\UpdatePdfPath;
|
||||
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
||||
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
||||
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
||||
use App\Set\Set as DomainSet;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\Fakes\FakeElementRepository;
|
||||
use Tests\Fakes\FakeFileUploader;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ElementControllerTest extends TestCase
|
||||
|
|
@ -28,35 +23,20 @@ class ElementControllerTest extends TestCase
|
|||
|
||||
private FakeElementRepository $elementRepo;
|
||||
|
||||
private FakeFileUploader $fileUploader;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->elementRepo = new FakeElementRepository();
|
||||
$this->fileUploader = new FakeFileUploader();
|
||||
$getElement = new GetElement($this->elementRepo);
|
||||
$updateElement = new UpdateElement(
|
||||
new UpdateTitle($this->elementRepo),
|
||||
new UpdateDescription($this->elementRepo),
|
||||
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
|
||||
new UpdateIconImageUrl($this->elementRepo),
|
||||
new UpdateRichText($this->elementRepo),
|
||||
new UpdatePdfPath($this->elementRepo, $this->fileUploader),
|
||||
new UpdatePdfPath($this->elementRepo),
|
||||
new UpdateYoutubeUrl($this->elementRepo),
|
||||
new UpdateIconImage(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
),
|
||||
new UpdatePdf(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
),
|
||||
$this->elementRepo,
|
||||
);
|
||||
$this->controller = new ElementController(
|
||||
$getElement,
|
||||
$updateElement,
|
||||
$this->fileUploader,
|
||||
);
|
||||
$this->controller = new ElementController($getElement, $updateElement);
|
||||
}
|
||||
|
||||
public function testShowReturnsElementPayload(): void
|
||||
|
|
@ -155,88 +135,6 @@ 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
|
||||
{
|
||||
return new DomainSet(
|
||||
|
|
|
|||
|
|
@ -19,19 +19,15 @@ use App\Element\UseCases\UpdateElement\UpdateYoutubeUrlRequest;
|
|||
use App\Exceptions\BadRequestException;
|
||||
use App\Set\Set as DomainSet;
|
||||
use Tests\Fakes\FakeElementRepository;
|
||||
use Tests\Fakes\FakeFileUploader;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdateElementFieldsTest extends TestCase
|
||||
{
|
||||
private FakeElementRepository $elementRepo;
|
||||
|
||||
private FakeFileUploader $fileUploader;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->elementRepo = new FakeElementRepository();
|
||||
$this->fileUploader = new FakeFileUploader();
|
||||
}
|
||||
|
||||
public function testUpdateTitleUpdatesOnlyTitle(): void
|
||||
|
|
@ -92,10 +88,7 @@ class UpdateElementFieldsTest extends TestCase
|
|||
public function testUpdateIconImageUrlClearsEmptyString(): void
|
||||
{
|
||||
$element = $this->createFilledElement();
|
||||
$updateIconImageUrl = new UpdateIconImageUrl(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
);
|
||||
$updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo);
|
||||
|
||||
$updatedElement = $updateIconImageUrl->execute(
|
||||
new UpdateIconImageUrlRequest(
|
||||
|
|
@ -106,10 +99,6 @@ class UpdateElementFieldsTest extends TestCase
|
|||
|
||||
$this->assertNull($updatedElement->getIconImageUrl());
|
||||
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
||||
$this->assertSame(
|
||||
['/assets/original-icon.png'],
|
||||
$this->fileUploader->deletedPaths,
|
||||
);
|
||||
}
|
||||
|
||||
public function testUpdateRichTextUpdatesOnlyRichText(): void
|
||||
|
|
@ -137,10 +126,7 @@ class UpdateElementFieldsTest extends TestCase
|
|||
public function testUpdatePdfPathClearsEmptyString(): void
|
||||
{
|
||||
$element = $this->createFilledElement();
|
||||
$updatePdfPath = new UpdatePdfPath(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
);
|
||||
$updatePdfPath = new UpdatePdfPath($this->elementRepo);
|
||||
|
||||
$updatedElement = $updatePdfPath->execute(
|
||||
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '')
|
||||
|
|
@ -148,10 +134,6 @@ class UpdateElementFieldsTest extends TestCase
|
|||
|
||||
$this->assertNull($updatedElement->getPdfPath());
|
||||
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
|
||||
$this->assertSame(
|
||||
['/assets/pdfs/original.pdf'],
|
||||
$this->fileUploader->deletedPaths,
|
||||
);
|
||||
}
|
||||
|
||||
public function testUpdateYoutubeUrlClearsEmptyString(): void
|
||||
|
|
@ -172,16 +154,6 @@ class UpdateElementFieldsTest extends TestCase
|
|||
|
||||
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(
|
||||
id: 1,
|
||||
name: 'Baderech',
|
||||
|
|
@ -193,9 +165,9 @@ class UpdateElementFieldsTest extends TestCase
|
|||
set: $set,
|
||||
title: 'Original title',
|
||||
description: 'Original description',
|
||||
iconImageUrl: $iconImageUrl,
|
||||
iconImageUrl: '/assets/original-icon.png',
|
||||
richText: '<p>Original rich text</p>',
|
||||
pdfPath: $pdfPath,
|
||||
pdfPath: '/assets/pdfs/original.pdf',
|
||||
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
|
||||
parentElement: null,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ namespace Tests\Unit\Element\UseCases;
|
|||
use App\Element\CreateElementDto;
|
||||
use App\Element\Element;
|
||||
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
||||
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
|
||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||
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\UpdatePdf;
|
||||
use App\Element\UseCases\UpdateElement\UpdateRichText;
|
||||
use App\Element\UseCases\UpdateElement\UpdateTitle;
|
||||
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
|
||||
|
|
@ -18,36 +16,24 @@ 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 UpdateElementTest extends TestCase
|
||||
{
|
||||
private FakeElementRepository $elementRepo;
|
||||
|
||||
private FakeFileUploader $fileUploader;
|
||||
|
||||
private UpdateElement $updateElement;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->elementRepo = new FakeElementRepository();
|
||||
$this->fileUploader = new FakeFileUploader();
|
||||
$this->updateElement = new UpdateElement(
|
||||
new UpdateTitle($this->elementRepo),
|
||||
new UpdateDescription($this->elementRepo),
|
||||
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
|
||||
new UpdateIconImageUrl($this->elementRepo),
|
||||
new UpdateRichText($this->elementRepo),
|
||||
new UpdatePdfPath($this->elementRepo, $this->fileUploader),
|
||||
new UpdatePdfPath($this->elementRepo),
|
||||
new UpdateYoutubeUrl($this->elementRepo),
|
||||
new UpdateIconImage(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
),
|
||||
new UpdatePdf(
|
||||
$this->elementRepo,
|
||||
$this->fileUploader,
|
||||
),
|
||||
$this->elementRepo,
|
||||
);
|
||||
}
|
||||
|
|
@ -75,14 +61,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: '<p>Updated rich text</p>',
|
||||
pdfPath: '/assets/pdfs/updated.pdf',
|
||||
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -137,14 +115,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: '<p>Updated rich text</p>',
|
||||
pdfPath: '',
|
||||
youtubeUrl: '',
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -176,14 +146,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: null,
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -210,58 +172,6 @@ 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
|
||||
{
|
||||
$this->expectException(BadRequestException::class);
|
||||
|
|
@ -275,14 +185,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: '<p>Updated rich text</p>',
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -299,14 +201,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: null,
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -323,14 +217,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: '<p>Updated rich text</p>',
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -347,14 +233,6 @@ class UpdateElementTest extends TestCase
|
|||
richText: '<p>Updated rich text</p>',
|
||||
pdfPath: null,
|
||||
youtubeUrl: null,
|
||||
iconImageContents: null,
|
||||
iconImageOriginalName: null,
|
||||
iconImageMimeType: null,
|
||||
iconImageSizeBytes: null,
|
||||
pdfContents: null,
|
||||
pdfOriginalName: null,
|
||||
pdfMimeType: null,
|
||||
pdfSizeBytes: null,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
<?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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
<?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
BIN
backend/tests/fixtures/baderech.pdf
vendored
Binary file not shown.
BIN
backend/tests/fixtures/icon.png
vendored
BIN
backend/tests/fixtures/icon.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
|
|
@ -12,17 +12,26 @@ function loginAsAdmin(): void {
|
|||
function fillElementForm(
|
||||
title: string,
|
||||
description: string,
|
||||
iconImageUrl: string,
|
||||
richText: string,
|
||||
pdfPath: string,
|
||||
youtubeUrl: string,
|
||||
): void {
|
||||
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-icon-image-url"]')
|
||||
.clear()
|
||||
.type(iconImageUrl)
|
||||
cy.get('[data-cy="admin-element-rich-text"]').clear()
|
||||
if (richText !== '') {
|
||||
cy.get('[data-cy="admin-element-rich-text"]').type(richText, {
|
||||
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()
|
||||
if (youtubeUrl !== '') {
|
||||
cy.get('[data-cy="admin-element-youtube-url"]').type(youtubeUrl)
|
||||
|
|
@ -70,6 +79,7 @@ describe('admin element editing', () => {
|
|||
it('saves element edits and restores the seed content through the UI', () => {
|
||||
const originalTitle = 'Baderech HaAvodah'
|
||||
const originalDescription = 'a structured path for inner and outer growth'
|
||||
const originalIconImageUrl = '/assets/baderech-haavodah-icon.png'
|
||||
const updatedTitle = 'Baderech HaAvodah Updated'
|
||||
const updatedDescription = 'Updated admin description'
|
||||
const updatedRichText = '<p>Updated admin rich text</p>'
|
||||
|
|
@ -80,8 +90,10 @@ describe('admin element editing', () => {
|
|||
fillElementForm(
|
||||
updatedTitle,
|
||||
updatedDescription,
|
||||
originalIconImageUrl,
|
||||
updatedRichText,
|
||||
'',
|
||||
'',
|
||||
)
|
||||
cy.get('[data-cy="admin-element-save"]').click()
|
||||
|
||||
|
|
@ -98,6 +110,8 @@ describe('admin element editing', () => {
|
|||
fillElementForm(
|
||||
originalTitle,
|
||||
originalDescription,
|
||||
originalIconImageUrl,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
)
|
||||
|
|
@ -106,89 +120,4 @@ describe('admin element editing', () => {
|
|||
.should('be.visible')
|
||||
.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"]')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'src')
|
||||
.and('include', '/storage/element-icons/baderech-haavodah-icon.png')
|
||||
.and('include', '/assets/baderech-haavodah-icon.png')
|
||||
cy.get('[data-cy="element-rich-text"]').should('not.exist')
|
||||
cy.get('[data-cy="element-youtube-embed"]')
|
||||
.should('not.exist')
|
||||
|
|
@ -117,12 +117,9 @@ describe('media page sets', () => {
|
|||
.should('have.css', 'text-align', 'center')
|
||||
cy.get('[data-cy="element-youtube-embed"]').should('not.exist')
|
||||
cy.contains('[data-cy="element-pdf-link"]', 'View PDF')
|
||||
.as('pdfLink')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', '/assets/pdfs/baderech.pdf')
|
||||
.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)
|
||||
.as('introductionAudioLink')
|
||||
.should('have.attr', 'href', '/element/8')
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ interface ElementResponse {
|
|||
export interface UpdateElementInput {
|
||||
title: string
|
||||
description: string
|
||||
iconImageUrl: string
|
||||
richText: string
|
||||
pdfPath: string
|
||||
youtubeUrl: string
|
||||
}
|
||||
|
||||
|
|
@ -30,21 +32,11 @@ interface UpdateElementResponse {
|
|||
element: Element
|
||||
}
|
||||
|
||||
interface ElementPatchInput {
|
||||
title?: string
|
||||
description?: string
|
||||
iconImageUrl?: string
|
||||
richText?: string
|
||||
pdfPath?: string
|
||||
youtubeUrl?: string
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
error?: 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', () => {
|
||||
const element = ref<Element | null>(null)
|
||||
|
|
@ -52,8 +44,6 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isSaving = ref(false)
|
||||
const isUploadingIconImage = ref(false)
|
||||
const isUploadingPdf = ref(false)
|
||||
const saveError = ref<string | null>(null)
|
||||
|
||||
async function fetchElement(elementId: string): Promise<void> {
|
||||
|
|
@ -90,142 +80,59 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
}
|
||||
|
||||
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
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||
method: 'POST',
|
||||
const encodedElementId = encodeURIComponent(elementId)
|
||||
const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}`
|
||||
const response = await fetch(elementUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ elementId, ...input }),
|
||||
body: JSON.stringify(input),
|
||||
})
|
||||
|
||||
return await handleElementResponse(response, failureMessage)
|
||||
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) {
|
||||
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 {
|
||||
saveError.value = `Network error - ${failureMessage.toLowerCase()}`
|
||||
saveError.value = 'Network error - could not save element'
|
||||
return false
|
||||
} finally {
|
||||
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 {
|
||||
element,
|
||||
childElements,
|
||||
isLoading,
|
||||
error,
|
||||
isSaving,
|
||||
isUploadingIconImage,
|
||||
isUploadingPdf,
|
||||
saveError,
|
||||
fetchElement,
|
||||
updateElement,
|
||||
uploadElementIconImage,
|
||||
uploadElementPdf,
|
||||
clearElementIconImage,
|
||||
clearElementPdf,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,24 +8,23 @@ import { useElementsStore } from '@/stores/elements'
|
|||
interface ElementForm {
|
||||
title: string
|
||||
description: string
|
||||
iconImageUrl: string
|
||||
richText: string
|
||||
pdfPath: string
|
||||
youtubeUrl: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const elementsStore = useElementsStore()
|
||||
const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } =
|
||||
storeToRefs(elementsStore)
|
||||
const { element, isLoading, error, isSaving, saveError } = storeToRefs(elementsStore)
|
||||
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>({
|
||||
title: '',
|
||||
description: '',
|
||||
iconImageUrl: '',
|
||||
richText: '',
|
||||
pdfPath: '',
|
||||
youtubeUrl: '',
|
||||
})
|
||||
|
||||
|
|
@ -55,8 +54,6 @@ watch(
|
|||
}
|
||||
|
||||
savedMessage.value = null
|
||||
iconImageStatus.value = null
|
||||
pdfStatus.value = null
|
||||
void elementsStore.fetchElement(currentElementId)
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -69,7 +66,9 @@ watch(element, (currentElement) => {
|
|||
|
||||
form.title = currentElement.title
|
||||
form.description = currentElement.description
|
||||
form.iconImageUrl = currentElement.iconImageUrl ?? ''
|
||||
form.richText = currentElement.richText
|
||||
form.pdfPath = currentElement.pdfPath ?? ''
|
||||
form.youtubeUrl = currentElement.youtubeUrl ?? ''
|
||||
})
|
||||
|
||||
|
|
@ -82,7 +81,9 @@ async function handleSubmit(): Promise<void> {
|
|||
const saved = await elementsStore.updateElement(elementId.value, {
|
||||
title: form.title,
|
||||
description: form.description,
|
||||
iconImageUrl: form.iconImageUrl,
|
||||
richText: form.richText,
|
||||
pdfPath: form.pdfPath,
|
||||
youtubeUrl: form.youtubeUrl,
|
||||
})
|
||||
|
||||
|
|
@ -90,92 +91,6 @@ async function handleSubmit(): Promise<void> {
|
|||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -215,53 +130,15 @@ function resetInput(changeEvent: Event): void {
|
|||
/>
|
||||
</label>
|
||||
|
||||
<section class="admin-element-page__media-field">
|
||||
<span class="admin-element-page__label">Icon Image</span>
|
||||
<img
|
||||
v-if="element.iconImageUrl !== null"
|
||||
:src="element.iconImageUrl"
|
||||
:alt="`${element.title} icon`"
|
||||
class="admin-element-page__icon-preview"
|
||||
data-cy="admin-element-current-icon"
|
||||
<label class="admin-element-page__field">
|
||||
<span class="admin-element-page__label">Icon Image URL</span>
|
||||
<input
|
||||
v-model="form.iconImageUrl"
|
||||
class="admin-element-page__input"
|
||||
data-cy="admin-element-icon-image-url"
|
||||
type="text"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<label class="admin-element-page__field">
|
||||
<span class="admin-element-page__label">Rich Text HTML</span>
|
||||
|
|
@ -273,56 +150,15 @@ function resetInput(changeEvent: Event): void {
|
|||
/>
|
||||
</label>
|
||||
|
||||
<section class="admin-element-page__media-field">
|
||||
<span class="admin-element-page__label">PDF</span>
|
||||
<a
|
||||
v-if="element.pdfPath !== null"
|
||||
:href="element.pdfPath"
|
||||
class="admin-element-page__pdf-link"
|
||||
data-cy="admin-element-current-pdf"
|
||||
target="_blank"
|
||||
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">
|
||||
<span class="admin-element-page__label">PDF Path</span>
|
||||
<input
|
||||
v-model="form.pdfPath"
|
||||
class="admin-element-page__input"
|
||||
data-cy="admin-element-pdf-path"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="admin-element-page__field">
|
||||
<span class="admin-element-page__label">YouTube URL</span>
|
||||
|
|
@ -398,16 +234,6 @@ function resetInput(changeEvent: Event): void {
|
|||
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 {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -441,61 +267,6 @@ function resetInput(changeEvent: Event): void {
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -543,11 +314,5 @@ function resetInput(changeEvent: Event): void {
|
|||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-element-page__media-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue