diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 9ad8ed8..5164e59 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -3,6 +3,10 @@ namespace App\Controllers; use App\Element\Element; +use App\Element\UseCases\CreateChildElement\CreateChildElement; +use App\Element\UseCases\CreateChildElement\CreateChildElementRequest; +use App\Element\UseCases\DeleteChildElement\DeleteChildElement; +use App\Element\UseCases\DeleteChildElement\DeleteChildElementRequest; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\GetElement\GetElementRequest; use App\Element\UseCases\UpdateElement\UpdateElement; @@ -18,6 +22,8 @@ class ElementController { public function __construct( private GetElement $getElement, + private CreateChildElement $createChildElement, + private DeleteChildElement $deleteChildElement, private UpdateElement $updateElement, private FileUploader $fileUploader, ) { @@ -55,6 +61,52 @@ 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 deleteChild(?int $parentId, ?int $childId): JsonResponse + { + try { + $this->deleteChildElement->execute( + new DeleteChildElementRequest( + parentElementId: $parentId, + childElementId: $childId, + ) + ); + } catch (BadRequestException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 400); + } catch (NotFoundException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 404); + } + + return new JsonResponse(null, 204); + } + public function update(Request $request): JsonResponse { $file = $this->uploadedFileInput($request, 'file'); diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php index e4fa74e..f759322 100644 --- a/backend/app/Element/ElementRepository.php +++ b/backend/app/Element/ElementRepository.php @@ -10,6 +10,8 @@ interface ElementRepository public function update(Element $element): Element; + public function delete(Element $element): void; + public function find(int $id): ?Element; public function findRootBySet(DomainSet $set): ?Element; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 771bc4c..77a110b 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -63,6 +63,18 @@ class EloquentElementRepository implements ElementRepository return $this->toDomain($model); } + public function delete(Element $element): void + { + $model = ElementModel::find($element->getId()); + if ($model === null) { + throw new DomainException( + "Element with id: {$element->getId()} doesnt exist" + ); + } + + $model->delete(); + } + public function find(int $id): ?Element { $model = ElementModel::find($id); 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 @@ +parentElementId === null) { + throw new BadRequestException('parentElementId is required'); + } + + if ($request->childElementId === null) { + throw new BadRequestException('childElementId is required'); + } + + $parentElement = $this->elementRepository->find( + $request->parentElementId + ); + if ($parentElement === null) { + throw new NotFoundException('Parent element not found'); + } + + $childElement = $this->elementRepository->find( + $request->childElementId + ); + if ($childElement === null) { + throw new NotFoundException('Child element not found'); + } + + $actualParentElement = $childElement->getParentElement(); + if ( + $actualParentElement === null + || $actualParentElement->getId() !== $parentElement->getId() + ) { + throw new BadRequestException( + 'Child element does not belong to parent' + ); + } + + $this->deleteSubtree($childElement); + } + + private function deleteSubtree(Element $element): void + { + $childElements = $this->elementRepository->findByParentElement( + $element + ); + foreach ($childElements as $childElement) { + $this->deleteSubtree($childElement); + } + + $this->deleteManagedFile($element->getIconImageUrl()); + $this->deleteManagedFile($element->getShortPdfPath()); + $this->deleteManagedFile($element->getLongPdfPath()); + $this->elementRepository->delete($element); + } + + private function deleteManagedFile(?string $path): void + { + if ($path === null) { + return; + } + + $this->fileUploader->delete($path); + } +} diff --git a/backend/app/Element/UseCases/DeleteChildElement/DeleteChildElementRequest.php b/backend/app/Element/UseCases/DeleteChildElement/DeleteChildElementRequest.php new file mode 100644 index 0000000..e2573d1 --- /dev/null +++ b/backend/app/Element/UseCases/DeleteChildElement/DeleteChildElementRequest.php @@ -0,0 +1,12 @@ + ['api/*'], - 'allowed_methods' => ['GET', 'POST', 'PATCH', 'OPTIONS'], + 'allowed_methods' => ['GET', 'POST', 'PATCH', 'OPTIONS', 'DELETE'], 'allowed_origins' => $allowedOrigins, 'allowed_origins_patterns' => [], 'allowed_headers' => [ diff --git a/backend/routes/api.php b/backend/routes/api.php index d3e0c43..1eb4ef1 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -12,5 +12,15 @@ Route::get('/me', [AuthController::class, 'me']) ->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::delete('/elements/{parentId}/children/{childId}', [ + ElementController::class, + 'deleteChild', +]) + ->middleware(AuthMiddleware::class); Route::post('/element/update', [ElementController::class, 'update']) ->middleware(AuthMiddleware::class); diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 18f1a6b..4e08547 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -42,6 +42,11 @@ class FakeElementRepository implements ElementRepository return $this->cloneElement($updatedElement); } + public function delete(Element $element): void + { + unset($this->elementsById[$element->getId()]); + } + public function find(int $id): ?Element { if (! isset($this->elementsById[$id])) { diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index cd17cd2..157f944 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -142,6 +142,267 @@ class ElementsEndpointTest extends TestCase ]); } + public function testCreateChildRequiresAuthentication(): void + { + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $parentElement = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '
A structured path for growth
', + null, + null, + null, + null, + ); + + $response = $this->postJson( + "/api/elements/{$parentElement->getId()}/children", + [ + 'title' => 'New child', + ], + ); + + $response->assertUnauthorized(); + $response->assertExactJson([ + 'error' => 'unauthenticated', + ]); + } + + public function testAuthenticatedCreateChildReturnsElementPayload(): void + { + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $parentElement = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + 'A structured path for growth
', + null, + null, + null, + null, + ); + $this->createSession('valid-token'); + + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') + ->postJson( + "/api/elements/{$parentElement->getId()}/children", + [ + 'title' => 'New admin child', + ], + ); + + $response->assertCreated(); + $body = $response->json(); + $childElementId = $body['element']['id']; + $response->assertExactJson([ + 'element' => [ + 'id' => $childElementId, + 'title' => 'New admin child', + 'description' => '', + 'iconImageUrl' => null, + 'richText' => '', + 'shortPdfPath' => null, + 'longPdfPath' => null, + 'youtubeUrl' => null, + ], + ]); + + $createdElement = $elementRepository->find($childElementId); + $this->assertNotNull($createdElement); + $this->assertSame( + $parentElement->getId(), + $createdElement->getParentElement()->getId(), + ); + $this->assertSame( + $parentElement->getSet()->getId(), + $createdElement->getSet()->getId(), + ); + } + + public function testDeleteChildRequiresAuthentication(): void + { + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $parentElement = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + 'A structured path for growth
', + null, + null, + null, + null, + ); + $childElement = $this->createElement( + $elementRepository, + $set, + 'Child', + 'Child description', + null, + '', + null, + null, + null, + $parentElement, + ); + + $response = $this->deleteJson( + "/api/elements/{$parentElement->getId()}" + . "/children/{$childElement->getId()}", + ); + + $response->assertUnauthorized(); + $response->assertExactJson([ + 'error' => 'unauthenticated', + ]); + } + + public function testAuthenticatedDeleteChildRemovesSubtree(): void + { + Storage::fake('public'); + Storage::disk('public')->put('element-icons/child.png', 'child icon'); + Storage::disk('public')->put( + 'element-pdfs/short/child.pdf', + 'child short pdf', + ); + Storage::disk('public')->put( + 'element-pdfs/long/grandchild.pdf', + 'grandchild long pdf', + ); + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $parentElement = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + 'A structured path for growth
', + null, + null, + null, + null, + ); + $childElement = $this->createElement( + $elementRepository, + $set, + 'Child', + 'Child description', + 'element-icons/child.png', + '', + 'element-pdfs/short/child.pdf', + null, + null, + $parentElement, + ); + $grandchildElement = $this->createElement( + $elementRepository, + $set, + 'Grandchild', + 'Grandchild description', + null, + '', + null, + 'element-pdfs/long/grandchild.pdf', + null, + $childElement, + ); + $this->createSession('valid-token'); + + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') + ->deleteJson( + "/api/elements/{$parentElement->getId()}" + . "/children/{$childElement->getId()}", + ); + + $response->assertNoContent(); + $this->assertNull($elementRepository->find($childElement->getId())); + $this->assertNull( + $elementRepository->find($grandchildElement->getId()), + ); + Storage::disk('public')->assertMissing('element-icons/child.png'); + Storage::disk('public')->assertMissing( + 'element-pdfs/short/child.pdf', + ); + Storage::disk('public')->assertMissing( + 'element-pdfs/long/grandchild.pdf', + ); + } + + public function testAuthenticatedDeleteChildRejectsNonDirectChild(): void + { + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $parentElement = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + 'A structured path for growth
', + null, + null, + null, + null, + ); + $childElement = $this->createElement( + $elementRepository, + $set, + 'Child', + 'Child description', + null, + '', + null, + null, + null, + $parentElement, + ); + $grandchildElement = $this->createElement( + $elementRepository, + $set, + 'Grandchild', + 'Grandchild description', + null, + '', + null, + null, + null, + $childElement, + ); + $this->createSession('valid-token'); + + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') + ->deleteJson( + "/api/elements/{$parentElement->getId()}" + . "/children/{$grandchildElement->getId()}", + ); + + $response->assertBadRequest(); + $response->assertExactJson([ + 'error' => 'Child element does not belong to parent', + ]); + $this->assertNotNull( + $elementRepository->find($grandchildElement->getId()), + ); + } + public function testAuthenticatedUpdateReturnsElementPayload(): void { $sampleYoutubeUrl = 'https://www.youtube.com/watch?v=' diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 7d97527..c0ce0a6 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -5,6 +5,8 @@ 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\DeleteChildElement\DeleteChildElement; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\UpdateElement\UpdateDescription; use App\Element\UseCases\UpdateElement\UpdateElement; @@ -61,6 +63,8 @@ class ElementControllerTest extends TestCase ); $this->controller = new ElementController( $getElement, + new CreateChildElement($this->elementRepo), + new DeleteChildElement($this->elementRepo, $this->fileUploader), $updateElement, $this->fileUploader, ); diff --git a/backend/tests/Unit/Element/UseCases/CreateChildElementTest.php b/backend/tests/Unit/Element/UseCases/CreateChildElementTest.php new file mode 100644 index 0000000..85819c5 --- /dev/null +++ b/backend/tests/Unit/Element/UseCases/CreateChildElementTest.php @@ -0,0 +1,102 @@ +elementRepository = new FakeElementRepository(); + $this->createChildElement = new CreateChildElement( + $this->elementRepository, + ); + } + + public function testCreatesChildElementFromParentSet(): void + { + $parentElement = $this->createParentElement(); + + $childElement = $this->createChildElement->execute( + new CreateChildElementRequest( + parentElementId: $parentElement->getId(), + title: 'Admin child', + ) + ); + + $this->assertSame('Admin child', $childElement->getTitle()); + $this->assertSame('', $childElement->getDescription()); + $this->assertNull($childElement->getIconImageUrl()); + $this->assertSame('', $childElement->getRichText()); + $this->assertNull($childElement->getShortPdfPath()); + $this->assertNull($childElement->getLongPdfPath()); + $this->assertNull($childElement->getYoutubeUrl()); + $this->assertSame( + $parentElement->getSet()->getId(), + $childElement->getSet()->getId(), + ); + $this->assertSame( + $parentElement->getId(), + $childElement->getParentElement()->getId(), + ); + } + + public function testThrowsWhenParentElementIsMissing(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Parent element not found'); + + $this->createChildElement->execute(new CreateChildElementRequest( + parentElementId: 999, + title: 'Missing parent child', + )); + } + + public function testThrowsWhenTitleIsMissing(): void + { + $parentElement = $this->createParentElement(); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('title is required'); + + $this->createChildElement->execute(new CreateChildElementRequest( + parentElementId: $parentElement->getId(), + title: '', + )); + } + + private function createParentElement(): 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: 'Parent', + description: 'Parent description', + iconImageUrl: null, + richText: '', + shortPdfPath: null, + longPdfPath: null, + youtubeUrl: null, + parentElement: null, + )); + } +} diff --git a/backend/tests/Unit/Element/UseCases/DeleteChildElementTest.php b/backend/tests/Unit/Element/UseCases/DeleteChildElementTest.php new file mode 100644 index 0000000..bac9af2 --- /dev/null +++ b/backend/tests/Unit/Element/UseCases/DeleteChildElementTest.php @@ -0,0 +1,173 @@ +elementRepository = new FakeElementRepository(); + $this->fileUploader = new FakeFileUploader(); + $this->deleteChildElement = new DeleteChildElement( + $this->elementRepository, + $this->fileUploader, + ); + } + + public function testDeletesChildSubtreeAndManagedFiles(): void + { + $parentElement = $this->createElement( + parentElement: null, + title: 'Parent', + iconImageUrl: null, + shortPdfPath: null, + longPdfPath: null, + ); + $childElement = $this->createElement( + parentElement: $parentElement, + title: 'Child', + iconImageUrl: 'element-icons/child.png', + shortPdfPath: 'element-pdfs/short/child.pdf', + longPdfPath: 'element-pdfs/long/child.pdf', + ); + $grandchildElement = $this->createElement( + parentElement: $childElement, + title: 'Grandchild', + iconImageUrl: 'element-icons/grandchild.png', + shortPdfPath: 'element-pdfs/short/grandchild.pdf', + longPdfPath: 'element-pdfs/long/grandchild.pdf', + ); + + $this->deleteChildElement->execute(new DeleteChildElementRequest( + parentElementId: $parentElement->getId(), + childElementId: $childElement->getId(), + )); + + $this->assertNull( + $this->elementRepository->find($childElement->getId()), + ); + $this->assertNull( + $this->elementRepository->find($grandchildElement->getId()), + ); + $this->assertSame([ + 'element-icons/grandchild.png', + 'element-pdfs/short/grandchild.pdf', + 'element-pdfs/long/grandchild.pdf', + 'element-icons/child.png', + 'element-pdfs/short/child.pdf', + 'element-pdfs/long/child.pdf', + ], $this->fileUploader->deletedPaths); + } + + public function testThrowsWhenParentElementIsMissing(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Parent element not found'); + + $this->deleteChildElement->execute(new DeleteChildElementRequest( + parentElementId: 999, + childElementId: 1, + )); + } + + public function testThrowsWhenChildElementIsMissing(): void + { + $parentElement = $this->createElement( + parentElement: null, + title: 'Parent', + iconImageUrl: null, + shortPdfPath: null, + longPdfPath: null, + ); + + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage('Child element not found'); + + $this->deleteChildElement->execute(new DeleteChildElementRequest( + parentElementId: $parentElement->getId(), + childElementId: 999, + )); + } + + public function testThrowsWhenChildIsNotDirectChild(): void + { + $parentElement = $this->createElement( + parentElement: null, + title: 'Parent', + iconImageUrl: null, + shortPdfPath: null, + longPdfPath: null, + ); + $childElement = $this->createElement( + parentElement: $parentElement, + title: 'Child', + iconImageUrl: null, + shortPdfPath: null, + longPdfPath: null, + ); + $grandchildElement = $this->createElement( + parentElement: $childElement, + title: 'Grandchild', + iconImageUrl: null, + shortPdfPath: null, + longPdfPath: null, + ); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage( + 'Child element does not belong to parent' + ); + + $this->deleteChildElement->execute(new DeleteChildElementRequest( + parentElementId: $parentElement->getId(), + childElementId: $grandchildElement->getId(), + )); + } + + private function createElement( + ?Element $parentElement, + string $title, + ?string $iconImageUrl, + ?string $shortPdfPath, + ?string $longPdfPath, + ): Element { + return $this->elementRepository->create(new CreateElementDto( + set: $this->createSet(), + title: $title, + description: "$title description", + iconImageUrl: $iconImageUrl, + richText: '', + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, + youtubeUrl: null, + parentElement: $parentElement, + )); + } + + private function createSet(): DomainSet + { + return new DomainSet( + id: 1, + name: 'Baderech', + description: 'Baderech description', + iconImageUrl: '/assets/baderech-icon.png', + ); + } +} diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts index f172d92..8c1f7f3 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -139,6 +139,58 @@ describe('admin element editing', () => { cy.get('[data-cy="element-rich-text"]').should('not.exist') }) + it('adds a child element and opens it for editing', () => { + cy.resetDb() + loginAsAdmin() + cy.visit('/admin/element/1') + cy.intercept('POST', /\/api\/elements\/1\/children$/) + .as('createChildElement') + + cy.get('[data-cy="admin-child-title"]').type('Admin added child') + cy.get('[data-cy="admin-add-child"]').click() + cy.wait('@createChildElement') + + cy.location('pathname').should('match', /^\/admin\/element\/\d+$/) + cy.get('[data-cy="admin-element-title"]') + .should('have.value', 'Admin added child') + cy.contains('header.site-header a', 'View Element').click() + cy.contains('h1', 'Admin added child').should('be.visible') + + cy.visit('/element/1') + cy.get('[data-cy="child-element-list"]') + .should('contain.text', 'Admin added child') + + cy.resetDb() + }) + + it('removes a child element after confirmation', () => { + cy.resetDb() + loginAsAdmin() + cy.visit('/admin/element/1') + cy.intercept('DELETE', /\/api\/elements\/1\/children\/\d+$/) + .as('deleteChildElement') + cy.window().then((windowObject) => { + cy.stub(windowObject, 'confirm').returns(true) + }) + + cy.contains('[data-cy="admin-child-item"]', '1. Introduction') + .within(() => { + cy.contains('button', 'Remove').click() + }) + cy.wait('@deleteChildElement') + + cy.get('[data-cy="admin-child-status"]') + .should('be.visible') + .and('contain.text', 'Child element removed') + cy.contains('[data-cy="admin-child-item"]', '1. Introduction') + .should('not.exist') + cy.contains('header.site-header a', 'View Element').click() + cy.get('[data-cy="child-element-list"]') + .should('not.contain.text', '1. Introduction') + + cy.resetDb() + }) + it('uploads icon and pdf files immediately through the UI', () => { cy.resetDb() loginAsAdmin() diff --git a/frontend/rabbi_gerzi/src/components/RichTextEditor.vue b/frontend/rabbi_gerzi/src/components/RichTextEditor.vue index 735c353..7fef966 100644 --- a/frontend/rabbi_gerzi/src/components/RichTextEditor.vue +++ b/frontend/rabbi_gerzi/src/components/RichTextEditor.vue @@ -133,10 +133,12 @@ const isInTable = computed(() => { return false } - return editorInstance.isActive('table') - || editorInstance.isActive('tableCell') - || editorInstance.isActive('tableHeader') - || editorInstance.isActive('tableRow') + return ( + editorInstance.isActive('table') || + editorInstance.isActive('tableCell') || + editorInstance.isActive('tableHeader') || + editorInstance.isActive('tableRow') + ) }) watch( diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 20cca63..d72683c 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,13 +64,16 @@ export const useElementsStore = defineStore('elements', () => { const isUploadingIconImage = ref(false) const isUploadingShortPdf = ref(false) const isUploadingLongPdf = ref(false) + const isManagingChildren = ref(false) const saveError = ref