diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php
index 62a6385..2e44931 100644
--- a/backend/app/Controllers/ElementController.php
+++ b/backend/app/Controllers/ElementController.php
@@ -17,7 +17,7 @@ class ElementController
public function show(?int $id): JsonResponse
{
try {
- $element = $this->getElement->execute(
+ $result = $this->getElement->execute(
new GetElementRequest(id: $id)
);
} catch (BadRequestException $exception) {
@@ -30,7 +30,17 @@ class ElementController
], 404);
}
+ $element = $result->getElement();
+ $childElements = [];
+ foreach ($result->getChildElements() as $childElement) {
+ $childElements[] = [
+ 'id' => $childElement->getId(),
+ 'title' => $childElement->getTitle(),
+ ];
+ }
+
return new JsonResponse([
+ 'childElements' => $childElements,
'element' => [
'id' => $element->getId(),
'title' => $element->getTitle(),
diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php
index 8ed13bd..3e89628 100644
--- a/backend/app/Element/ElementRepository.php
+++ b/backend/app/Element/ElementRepository.php
@@ -16,4 +16,9 @@ interface ElementRepository
* @return Element[]
*/
public function findBySet(DomainSet $set): array;
+
+ /**
+ * @return Element[]
+ */
+ public function findByParentElement(Element $parentElement): array;
}
diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php
index 68452de..350e37a 100644
--- a/backend/app/Element/EloquentElementRepository.php
+++ b/backend/app/Element/EloquentElementRepository.php
@@ -61,6 +61,25 @@ class EloquentElementRepository implements ElementRepository
return $elements;
}
+ /**
+ * @return Element[]
+ */
+ public function findByParentElement(Element $parentElement): array
+ {
+ $models = ElementModel::where(
+ 'parent_element_id',
+ $parentElement->getId(),
+ )
+ ->orderBy('id')
+ ->get();
+ $elements = [];
+ foreach ($models as $model) {
+ $elements[] = $this->toDomain($model);
+ }
+
+ return $elements;
+ }
+
private function toDomain(ElementModel $model): Element
{
$set = $this->setRepo->find($model->set_id);
diff --git a/backend/app/Element/UseCases/GetElement/GetElement.php b/backend/app/Element/UseCases/GetElement/GetElement.php
index 2dc555e..7103e47 100644
--- a/backend/app/Element/UseCases/GetElement/GetElement.php
+++ b/backend/app/Element/UseCases/GetElement/GetElement.php
@@ -2,7 +2,6 @@
namespace App\Element\UseCases\GetElement;
-use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
@@ -17,7 +16,7 @@ class GetElement
* @throws BadRequestException
* @throws NotFoundException
*/
- public function execute(GetElementRequest $request): Element
+ public function execute(GetElementRequest $request): GetElementResult
{
if ($request->id === null) {
throw new BadRequestException('id is required');
@@ -28,6 +27,10 @@ class GetElement
throw new NotFoundException('Element not found');
}
- return $element;
+ return new GetElementResult(
+ element: $element,
+ childElements: $this->elementRepository
+ ->findByParentElement($element),
+ );
}
}
diff --git a/backend/app/Element/UseCases/GetElement/GetElementResult.php b/backend/app/Element/UseCases/GetElement/GetElementResult.php
new file mode 100644
index 0000000..ad5f51b
--- /dev/null
+++ b/backend/app/Element/UseCases/GetElement/GetElementResult.php
@@ -0,0 +1,30 @@
+element;
+ }
+
+ /**
+ * @return Element[]
+ */
+ public function getChildElements(): array
+ {
+ return $this->childElements;
+ }
+}
diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php
index 974b2fb..68ee7b9 100644
--- a/backend/database/seeders/ElementSeeder.php
+++ b/backend/database/seeders/ElementSeeder.php
@@ -14,10 +14,20 @@ class ElementSeeder extends Seeder
$setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class);
$baderechSet = $setRepository->find(1);
- $elementRepository->create(new CreateElementDto(
+ $rootElement = $elementRepository->create(new CreateElementDto(
set: $baderechSet,
title: $baderechSet->getName(),
parentElement: null,
));
+ $elementRepository->create(new CreateElementDto(
+ set: $baderechSet,
+ title: 'Avodah Foundations',
+ parentElement: $rootElement,
+ ));
+ $elementRepository->create(new CreateElementDto(
+ set: $baderechSet,
+ title: 'Daily Practice',
+ parentElement: $rootElement,
+ ));
}
}
diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php
index cbb4e13..b16be7f 100644
--- a/backend/tests/Fakes/FakeElementRepository.php
+++ b/backend/tests/Fakes/FakeElementRepository.php
@@ -66,6 +66,25 @@ class FakeElementRepository implements ElementRepository
return $elements;
}
+ /**
+ * @return Element[]
+ */
+ public function findByParentElement(Element $parentElement): array
+ {
+ $elements = [];
+ foreach ($this->elementsById as $element) {
+ $currentParentElement = $element->getParentElement();
+ if (
+ $currentParentElement !== null
+ && $currentParentElement->getId() === $parentElement->getId()
+ ) {
+ $elements[] = $this->cloneElement($element);
+ }
+ }
+
+ return $elements;
+ }
+
private function cloneElement(Element $element): Element
{
$parentElement = $element->getParentElement();
diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php
index be69aec..40834f2 100644
--- a/backend/tests/Feature/ElementsEndpointTest.php
+++ b/backend/tests/Feature/ElementsEndpointTest.php
@@ -27,11 +27,31 @@ class ElementsEndpointTest extends TestCase
title: 'Baderech HaAvodah',
parentElement: null,
));
+ $firstChildElement = $elementRepository->create(new CreateElementDto(
+ set: $set,
+ title: 'Avodah Foundations',
+ parentElement: $element,
+ ));
+ $secondChildElement = $elementRepository->create(new CreateElementDto(
+ set: $set,
+ title: 'Daily Practice',
+ parentElement: $element,
+ ));
$response = $this->getJson("/api/elements/{$element->getId()}");
$response->assertOk();
$response->assertExactJson([
+ 'childElements' => [
+ [
+ 'id' => $firstChildElement->getId(),
+ 'title' => 'Avodah Foundations',
+ ],
+ [
+ 'id' => $secondChildElement->getId(),
+ 'title' => 'Daily Practice',
+ ],
+ ],
'element' => [
'id' => $element->getId(),
'title' => 'Baderech HaAvodah',
diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php
index 93dddfc..20a18dc 100644
--- a/backend/tests/Unit/Controllers/ElementControllerTest.php
+++ b/backend/tests/Unit/Controllers/ElementControllerTest.php
@@ -25,7 +25,18 @@ class ElementControllerTest extends TestCase
public function testShowReturnsElementPayload(): void
{
- $element = $this->createElement('Baderech HaAvodah');
+ $set = $this->createSet(1, 'Baderech');
+ $element = $this->createElement($set, 'Baderech HaAvodah', null);
+ $firstChildElement = $this->createElement(
+ $set,
+ 'Avodah Foundations',
+ $element,
+ );
+ $secondChildElement = $this->createElement(
+ $set,
+ 'Daily Practice',
+ $element,
+ );
$response = $this->controller->show($element->getId());
@@ -33,6 +44,16 @@ class ElementControllerTest extends TestCase
$body = json_decode($response->getContent(), true);
$this->assertSame($element->getId(), $body['element']['id']);
$this->assertSame('Baderech HaAvodah', $body['element']['title']);
+ $this->assertSame([
+ [
+ 'id' => $firstChildElement->getId(),
+ 'title' => 'Avodah Foundations',
+ ],
+ [
+ 'id' => $secondChildElement->getId(),
+ 'title' => 'Daily Practice',
+ ],
+ ], $body['childElements']);
}
public function testShowReturns400WhenIdMissing(): void
@@ -57,19 +78,25 @@ class ElementControllerTest extends TestCase
);
}
- private function createElement(string $title): Element
+ private function createSet(int $id, string $name): DomainSet
{
- $set = new DomainSet(
- id: 1,
- name: 'Baderech',
- description: 'Baderech description',
+ return new DomainSet(
+ id: $id,
+ name: $name,
+ description: "$name description",
iconImageUrl: '/assets/baderech-icon.png',
);
+ }
+ private function createElement(
+ DomainSet $set,
+ string $title,
+ ?Element $parentElement,
+ ): Element {
return $this->elementRepo->create(new CreateElementDto(
set: $set,
title: $title,
- parentElement: null,
+ parentElement: $parentElement,
));
}
}
diff --git a/backend/tests/Unit/Element/UseCases/GetElementTest.php b/backend/tests/Unit/Element/UseCases/GetElementTest.php
index a301af6..fc665b0 100644
--- a/backend/tests/Unit/Element/UseCases/GetElementTest.php
+++ b/backend/tests/Unit/Element/UseCases/GetElementTest.php
@@ -26,17 +26,76 @@ class GetElementTest extends TestCase
public function testReturnsElementWhenFound(): void
{
- $element = $this->createElement('Baderech HaAvodah');
+ $set = $this->createSet(1, 'Baderech');
+ $element = $this->createElement(
+ $set,
+ 'Baderech HaAvodah',
+ null,
+ );
- $foundElement = $this->getElement->execute(new GetElementRequest(
+ $result = $this->getElement->execute(new GetElementRequest(
id: $element->getId(),
));
+ $foundElement = $result->getElement();
$this->assertInstanceOf(Element::class, $foundElement);
$this->assertSame($element->getId(), $foundElement->getId());
$this->assertSame('Baderech HaAvodah', $foundElement->getTitle());
}
+ public function testReturnsDirectChildElements(): void
+ {
+ $set = $this->createSet(1, 'Baderech');
+ $parentElement = $this->createElement(
+ $set,
+ 'Baderech HaAvodah',
+ null,
+ );
+ $firstChildElement = $this->createElement(
+ $set,
+ 'Avodah Foundations',
+ $parentElement,
+ );
+ $secondChildElement = $this->createElement(
+ $set,
+ 'Daily Practice',
+ $parentElement,
+ );
+ $this->createElement(
+ $set,
+ 'Nested Practice',
+ $firstChildElement,
+ );
+ $otherSet = $this->createSet(2, 'Daily Learning');
+ $otherParentElement = $this->createElement(
+ $otherSet,
+ 'Other Parent',
+ null,
+ );
+ $this->createElement(
+ $otherSet,
+ 'Other Child',
+ $otherParentElement,
+ );
+
+ $result = $this->getElement->execute(new GetElementRequest(
+ id: $parentElement->getId(),
+ ));
+ $childElements = $result->getChildElements();
+
+ $this->assertCount(2, $childElements);
+ $this->assertSame(
+ $firstChildElement->getId(),
+ $childElements[0]->getId(),
+ );
+ $this->assertSame('Avodah Foundations', $childElements[0]->getTitle());
+ $this->assertSame(
+ $secondChildElement->getId(),
+ $childElements[1]->getId(),
+ );
+ $this->assertSame('Daily Practice', $childElements[1]->getTitle());
+ }
+
public function testThrowsWhenIdMissing(): void
{
$this->expectException(BadRequestException::class);
@@ -53,19 +112,25 @@ class GetElementTest extends TestCase
$this->getElement->execute(new GetElementRequest(id: 999));
}
- private function createElement(string $title): Element
+ private function createSet(int $id, string $name): DomainSet
{
- $set = new DomainSet(
- id: 1,
- name: 'Baderech',
- description: 'Baderech description',
+ return new DomainSet(
+ id: $id,
+ name: $name,
+ description: "$name description",
iconImageUrl: '/assets/baderech-icon.png',
);
+ }
+ private function createElement(
+ DomainSet $set,
+ string $title,
+ ?Element $parentElement,
+ ): Element {
return $this->elementRepo->create(new CreateElementDto(
set: $set,
title: $title,
- parentElement: null,
+ parentElement: $parentElement,
));
}
}
diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts
index 69fe661..a260b94 100644
--- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts
+++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts
@@ -41,5 +41,15 @@ describe('media page sets', () => {
cy.location('pathname').should('eq', '/element/1')
cy.get('[data-cy="element-page"]').should('be.visible')
cy.contains('h1', 'Baderech HaAvodah').should('be.visible')
+ cy.get('[data-cy="child-element-list"]').should('be.visible')
+ cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations')
+ .should('have.attr', 'href', '/element/2')
+ cy.contains('[data-cy="child-element-link"]', 'Daily Practice')
+ .should('have.attr', 'href', '/element/3')
+
+ cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations')
+ .click()
+ cy.location('pathname').should('eq', '/element/2')
+ cy.contains('h1', 'Avodah Foundations').should('be.visible')
})
})
diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts
index 0624751..6922157 100644
--- a/frontend/rabbi_gerzi/src/stores/elements.ts
+++ b/frontend/rabbi_gerzi/src/stores/elements.ts
@@ -8,17 +8,20 @@ export interface Element {
interface ElementResponse {
element: Element
+ childElements: Element[]
}
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
export const useElementsStore = defineStore('elements', () => {
const element = ref