diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 9ad8ed8..69ef8ce 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -3,6 +3,8 @@ namespace App\Controllers; use App\Element\Element; +use App\Element\UseCases\CreateChildElement\CreateChildElement; +use App\Element\UseCases\CreateChildElement\CreateChildElementRequest; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\GetElement\GetElementRequest; use App\Element\UseCases\UpdateElement\UpdateElement; @@ -18,6 +20,7 @@ class ElementController { public function __construct( private GetElement $getElement, + private CreateChildElement $createChildElement, private UpdateElement $updateElement, private FileUploader $fileUploader, ) { @@ -55,6 +58,30 @@ class ElementController ], 200); } + public function createChild(Request $request, ?int $parentId): JsonResponse + { + try { + $element = $this->createChildElement->execute( + new CreateChildElementRequest( + parentElementId: $parentId, + title: $this->stringInput($request, 'title'), + ) + ); + } catch (BadRequestException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 400); + } catch (NotFoundException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 404); + } + + return new JsonResponse([ + 'element' => $this->buildElementPayload($element), + ], 201); + } + public function update(Request $request): JsonResponse { $file = $this->uploadedFileInput($request, 'file'); diff --git a/backend/app/Element/UseCases/CreateChildElement/CreateChildElement.php b/backend/app/Element/UseCases/CreateChildElement/CreateChildElement.php new file mode 100644 index 0000000..0286abf --- /dev/null +++ b/backend/app/Element/UseCases/CreateChildElement/CreateChildElement.php @@ -0,0 +1,50 @@ +parentElementId === null) { + throw new BadRequestException('parentElementId is required'); + } + + if ($request->title === null || $request->title === '') { + throw new BadRequestException('title is required'); + } + + $parentElement = $this->elementRepository->find( + $request->parentElementId + ); + if ($parentElement === null) { + throw new NotFoundException('Parent element not found'); + } + + return $this->elementRepository->create(new CreateElementDto( + set: $parentElement->getSet(), + title: $request->title, + description: '', + iconImageUrl: null, + richText: '', + shortPdfPath: null, + longPdfPath: null, + youtubeUrl: null, + parentElement: $parentElement, + )); + } +} diff --git a/backend/app/Element/UseCases/CreateChildElement/CreateChildElementRequest.php b/backend/app/Element/UseCases/CreateChildElement/CreateChildElementRequest.php new file mode 100644 index 0000000..c4f5931 --- /dev/null +++ b/backend/app/Element/UseCases/CreateChildElement/CreateChildElementRequest.php @@ -0,0 +1,12 @@ +middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); +Route::post('/elements/{parentId}/children', [ + ElementController::class, + 'createChild', +]) + ->middleware(AuthMiddleware::class); Route::post('/element/update', [ElementController::class, 'update']) ->middleware(AuthMiddleware::class); diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 7d97527..9e3d9ff 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Controllers; use App\Controllers\ElementController; use App\Element\CreateElementDto; use App\Element\Element; +use App\Element\UseCases\CreateChildElement\CreateChildElement; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\UpdateElement\UpdateDescription; use App\Element\UseCases\UpdateElement\UpdateElement; @@ -61,6 +62,7 @@ class ElementControllerTest extends TestCase ); $this->controller = new ElementController( $getElement, + new CreateChildElement($this->elementRepo), $updateElement, $this->fileUploader, ); diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 20cca63..cf3488a 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -29,6 +29,10 @@ export interface UpdateElementInput { youtubeUrl: string } +export interface CreateChildElementInput { + title: string +} + interface UpdateElementResponse { element: Element } @@ -48,6 +52,7 @@ interface ErrorResponse { } const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string +const ELEMENTS_URL = `${API_BASE_URL}/api/elements` const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update` export const useElementsStore = defineStore('elements', () => { @@ -59,7 +64,9 @@ export const useElementsStore = defineStore('elements', () => { const isUploadingIconImage = ref(false) const isUploadingShortPdf = ref(false) const isUploadingLongPdf = ref(false) + const isManagingChildren = ref(false) const saveError = ref(null) + const childActionError = ref(null) async function fetchElement(elementId: string): Promise { element.value = null @@ -101,6 +108,37 @@ export const useElementsStore = defineStore('elements', () => { return await saveElementPatch(elementId, input, 'Could not save element') } + async function createChildElement( + parentElementId: string, + input: CreateChildElementInput, + ): Promise { + childActionError.value = null + isManagingChildren.value = true + + try { + const encodedParentElementId = encodeURIComponent(parentElementId) + const response = await fetch( + `${ELEMENTS_URL}/${encodedParentElementId}/children`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(input), + }, + ) + + return await handleChildElementResponse( + response, + 'Could not add child element', + ) + } catch { + childActionError.value = 'Network error - could not add child element' + return null + } finally { + isManagingChildren.value = false + } + } + async function clearElementIconImage(elementId: string): Promise { return await saveElementPatch( elementId, @@ -253,6 +291,29 @@ export const useElementsStore = defineStore('elements', () => { return true } + async function handleChildElementResponse( + response: Response, + failureMessage: string, + ): Promise { + if (response.status === 401) { + childActionError.value = 'Please log in again' + return null + } + + if (response.status === 400 || response.status === 404) { + childActionError.value = await errorMessage(response, failureMessage) + return null + } + + if (!response.ok) { + childActionError.value = failureMessage + return null + } + + const data: UpdateElementResponse = await response.json() + return data.element + } + async function errorMessage( response: Response, fallbackMessage: string, @@ -274,9 +335,12 @@ export const useElementsStore = defineStore('elements', () => { isUploadingIconImage, isUploadingShortPdf, isUploadingLongPdf, + isManagingChildren, saveError, + childActionError, fetchElement, updateElement, + createChildElement, uploadElementIconImage, uploadElementShortPdf, uploadElementLongPdf, diff --git a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue index ab6e470..9a0632d 100644 --- a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue @@ -1,7 +1,7 @@