delete child elements
This commit is contained in:
parent
ac0f404fce
commit
f4b42973cb
10 changed files with 271 additions and 17 deletions
|
|
@ -5,6 +5,8 @@ namespace App\Controllers;
|
||||||
use App\Element\Element;
|
use App\Element\Element;
|
||||||
use App\Element\UseCases\CreateChildElement\CreateChildElement;
|
use App\Element\UseCases\CreateChildElement\CreateChildElement;
|
||||||
use App\Element\UseCases\CreateChildElement\CreateChildElementRequest;
|
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\GetElement;
|
||||||
use App\Element\UseCases\GetElement\GetElementRequest;
|
use App\Element\UseCases\GetElement\GetElementRequest;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||||
|
|
@ -21,6 +23,7 @@ class ElementController
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private GetElement $getElement,
|
private GetElement $getElement,
|
||||||
private CreateChildElement $createChildElement,
|
private CreateChildElement $createChildElement,
|
||||||
|
private DeleteChildElement $deleteChildElement,
|
||||||
private UpdateElement $updateElement,
|
private UpdateElement $updateElement,
|
||||||
private FileUploader $fileUploader,
|
private FileUploader $fileUploader,
|
||||||
) {
|
) {
|
||||||
|
|
@ -82,6 +85,28 @@ class ElementController
|
||||||
], 201);
|
], 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
|
public function update(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$file = $this->uploadedFileInput($request, 'file');
|
$file = $this->uploadedFileInput($request, 'file');
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ interface ElementRepository
|
||||||
|
|
||||||
public function update(Element $element): Element;
|
public function update(Element $element): Element;
|
||||||
|
|
||||||
|
public function delete(Element $element): void;
|
||||||
|
|
||||||
public function find(int $id): ?Element;
|
public function find(int $id): ?Element;
|
||||||
|
|
||||||
public function findRootBySet(DomainSet $set): ?Element;
|
public function findRootBySet(DomainSet $set): ?Element;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,18 @@ class EloquentElementRepository implements ElementRepository
|
||||||
return $this->toDomain($model);
|
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
|
public function find(int $id): ?Element
|
||||||
{
|
{
|
||||||
$model = ElementModel::find($id);
|
$model = ElementModel::find($id);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\DeleteChildElement;
|
||||||
|
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Shared\Files\FileUploader;
|
||||||
|
|
||||||
|
class DeleteChildElement
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ElementRepository $elementRepository,
|
||||||
|
private FileUploader $fileUploader,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequestException
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function execute(DeleteChildElementRequest $request): void
|
||||||
|
{
|
||||||
|
if ($request->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Element\UseCases\DeleteChildElement;
|
||||||
|
|
||||||
|
class DeleteChildElementRequest
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $parentElementId,
|
||||||
|
public ?int $childElementId,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,5 +17,10 @@ Route::post('/elements/{parentId}/children', [
|
||||||
'createChild',
|
'createChild',
|
||||||
])
|
])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
|
Route::delete('/elements/{parentId}/children/{childId}', [
|
||||||
|
ElementController::class,
|
||||||
|
'deleteChild',
|
||||||
|
])
|
||||||
|
->middleware(AuthMiddleware::class);
|
||||||
Route::post('/element/update', [ElementController::class, 'update'])
|
Route::post('/element/update', [ElementController::class, 'update'])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@ class FakeElementRepository implements ElementRepository
|
||||||
return $this->cloneElement($updatedElement);
|
return $this->cloneElement($updatedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(Element $element): void
|
||||||
|
{
|
||||||
|
unset($this->elementsById[$element->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
public function find(int $id): ?Element
|
public function find(int $id): ?Element
|
||||||
{
|
{
|
||||||
if (! isset($this->elementsById[$id])) {
|
if (! isset($this->elementsById[$id])) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Controllers\ElementController;
|
||||||
use App\Element\CreateElementDto;
|
use App\Element\CreateElementDto;
|
||||||
use App\Element\Element;
|
use App\Element\Element;
|
||||||
use App\Element\UseCases\CreateChildElement\CreateChildElement;
|
use App\Element\UseCases\CreateChildElement\CreateChildElement;
|
||||||
|
use App\Element\UseCases\DeleteChildElement\DeleteChildElement;
|
||||||
use App\Element\UseCases\GetElement\GetElement;
|
use App\Element\UseCases\GetElement\GetElement;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
use App\Element\UseCases\UpdateElement\UpdateDescription;
|
||||||
use App\Element\UseCases\UpdateElement\UpdateElement;
|
use App\Element\UseCases\UpdateElement\UpdateElement;
|
||||||
|
|
@ -63,6 +64,7 @@ class ElementControllerTest extends TestCase
|
||||||
$this->controller = new ElementController(
|
$this->controller = new ElementController(
|
||||||
$getElement,
|
$getElement,
|
||||||
new CreateChildElement($this->elementRepo),
|
new CreateChildElement($this->elementRepo),
|
||||||
|
new DeleteChildElement($this->elementRepo, $this->fileUploader),
|
||||||
$updateElement,
|
$updateElement,
|
||||||
$this->fileUploader,
|
$this->fileUploader,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
childElements.value = []
|
childElements.value = []
|
||||||
error.value = null
|
error.value = null
|
||||||
saveError.value = null
|
saveError.value = null
|
||||||
|
childActionError.value = null
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -139,6 +140,36 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteChildElement(
|
||||||
|
parentElementId: string,
|
||||||
|
childElementId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
childActionError.value = null
|
||||||
|
isManagingChildren.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encodedParentElementId = encodeURIComponent(parentElementId)
|
||||||
|
const encodedChildElementId = encodeURIComponent(
|
||||||
|
childElementId.toString(),
|
||||||
|
)
|
||||||
|
const response = await fetch(
|
||||||
|
`${ELEMENTS_URL}/${encodedParentElementId}`
|
||||||
|
+ `/children/${encodedChildElementId}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return await handleDeleteChildElementResponse(response, childElementId)
|
||||||
|
} catch {
|
||||||
|
childActionError.value = 'Network error - could not remove child element'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isManagingChildren.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function clearElementIconImage(elementId: string): Promise<boolean> {
|
async function clearElementIconImage(elementId: string): Promise<boolean> {
|
||||||
return await saveElementPatch(
|
return await saveElementPatch(
|
||||||
elementId,
|
elementId,
|
||||||
|
|
@ -314,6 +345,34 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
return data.element
|
return data.element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteChildElementResponse(
|
||||||
|
response: Response,
|
||||||
|
childElementId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (response.status === 401) {
|
||||||
|
childActionError.value = 'Please log in again'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
childActionError.value = await errorMessage(
|
||||||
|
response,
|
||||||
|
'Could not remove child element',
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
childActionError.value = 'Could not remove child element'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
childElements.value = childElements.value.filter((childElement) => {
|
||||||
|
return childElement.id !== childElementId
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async function errorMessage(
|
async function errorMessage(
|
||||||
response: Response,
|
response: Response,
|
||||||
fallbackMessage: string,
|
fallbackMessage: string,
|
||||||
|
|
@ -341,6 +400,7 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
fetchElement,
|
fetchElement,
|
||||||
updateElement,
|
updateElement,
|
||||||
createChildElement,
|
createChildElement,
|
||||||
|
deleteChildElement,
|
||||||
uploadElementIconImage,
|
uploadElementIconImage,
|
||||||
uploadElementShortPdf,
|
uploadElementShortPdf,
|
||||||
uploadElementLongPdf,
|
uploadElementLongPdf,
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,29 @@ async function handleAddChild(): Promise<void> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemoveChild(childElementId: number): Promise<void> {
|
||||||
|
if (elementId.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
'Delete this child element and all of its descendants?',
|
||||||
|
)
|
||||||
|
if (!confirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
childStatus.value = null
|
||||||
|
const deleted = await elementsStore.deleteChildElement(
|
||||||
|
elementId.value,
|
||||||
|
childElementId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
childStatus.value = 'Child element removed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chooseIconImage(): void {
|
function chooseIconImage(): void {
|
||||||
iconImageInput.value?.click()
|
iconImageInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
@ -500,7 +523,9 @@ function resetInput(changeEvent: Event): void {
|
||||||
v-for="childElement in childElements"
|
v-for="childElement in childElements"
|
||||||
:key="childElement.id"
|
:key="childElement.id"
|
||||||
class="admin-element-page__child-item"
|
class="admin-element-page__child-item"
|
||||||
|
data-cy="admin-child-item"
|
||||||
>
|
>
|
||||||
|
<div class="admin-element-page__child-body">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{
|
:to="{
|
||||||
name: 'admin-element',
|
name: 'admin-element',
|
||||||
|
|
@ -517,6 +542,16 @@ function resetInput(changeEvent: Event): void {
|
||||||
>
|
>
|
||||||
{{ childElement.description }}
|
{{ childElement.description }}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="admin-element-page__secondary-button"
|
||||||
|
data-cy="admin-remove-child"
|
||||||
|
type="button"
|
||||||
|
:disabled="isManagingChildren"
|
||||||
|
@click="handleRemoveChild(childElement.id)"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p
|
<p
|
||||||
|
|
@ -700,13 +735,22 @@ function resetInput(changeEvent: Event): void {
|
||||||
|
|
||||||
.admin-element-page__child-item {
|
.admin-element-page__child-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-start;
|
||||||
gap: 0.25rem;
|
justify-content: space-between;
|
||||||
|
gap: 0.7rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-element-page__child-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-element-page__child-link {
|
.admin-element-page__child-link {
|
||||||
color: var(--color-slate);
|
color: var(--color-slate);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
|
@ -833,5 +877,9 @@ function resetInput(changeEvent: Event): void {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-element-page__child-item {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue