Completed
Push — develop ( e67729...75d57c )
by Bartko
02:02
created

Service::createRoot()   C

Complexity

Conditions 8
Paths 36

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 23
nc 36
nop 1
dl 0
loc 35
rs 5.3846
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 48 and the first side effect is on line 10.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * Quick end dirty solution only for demonstration purpose.
4
 */
5
declare(strict_types=1);
6
7
use StefanoTree\Exception\ValidationException;
8
use StefanoTree\TreeInterface;
9
10
session_start();
11
12
include_once __DIR__.'/../vendor/autoload.php';
13
14
$config = include_once __DIR__.'/config.php';
15
16
$dbAdapter = \Doctrine\DBAL\DriverManager::getConnection(
17
    $config['dbConnection'],
18
    new \Doctrine\DBAL\Configuration()
19
);
20
21
/**************************************
22
 *    Tree Adapter
23
 **************************************/
24
$treeAdapter = \StefanoTree\NestedSet::factory(
25
    array(
26
        'tableName' => 'categories',
27
        'idColumnName' => 'id',
28
        'sequenceName' => 'categories_id_seq',
29
        'scopeColumnName' => 'group_id',
30
    ),
31
    $dbAdapter
32
);
33
34
/***************************************
35
 * Join example
36
 ***************************************/
37
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
38
$select = $dbAdapter->createQueryBuilder();
39
$select->from('categories' ,'c')
40
       ->select('c.*', '...')
41
       ->leftJoin('c', 'metadata', 'm', 'm.id = c.id');
42
43
$adapter = $treeAdapter
44
    ->getAdapter()
45
    ->setDefaultDbSelect($select);
46
*/
47
48
class Service
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
49
{
50
    private $treeAdapter;
51
52
    public function __construct(TreeInterface $treeAdapter)
53
    {
54
        $this->treeAdapter = $treeAdapter;
55
    }
56
57
    private function getTreeAdapter(): TreeInterface
58
    {
59
        return $this->treeAdapter;
60
    }
61
62
    public function createRoot(array $data): void
63
    {
64
        $errors = array();
65
66
        $label = $data['label'] ?? '';
67
        if (0 == strlen($label)) {
68
            $errors[] = 'Root Name cannot be empty.';
69
        } elseif (15 < strlen($label)) {
70
            $errors[] = 'Root Name is too long. Max length is 15 characters.';
71
        }
72
73
        $scope = $data['scope'] ?? '';
74
        if (0 === strlen($scope)) {
75
            $errors[] = 'Scope Name cannot be empty.';
76
        } elseif (!preg_match('|^[1-9][0-9]*$|', $scope)) {
77
            $errors[] = 'Scope Name must be integer.';
78
        } elseif (15 < strlen($scope)) {
79
            $errors[] = 'Scope Name is too long. Max length is 15 characters.';
80
        }
81
82
        if (count($errors)) {
83
            throw new ValidationError($errors);
84
        }
85
86
        $data = array(
87
            'name' => $label,
88
        );
89
90
        try {
91
            $this->getTreeAdapter()
92
                ->createRootNode($data, $scope);
93
        } catch (ValidationException $e) {
94
            throw new ValidationError([$e->getMessage()]);
95
        }
96
    }
97
98
    public function createNode(array $data): void
99
    {
100
        $errors = array();
101
102
        $targetId = $_POST['target_node_id'] ?? '';
103
        if (0 === strlen($targetId)) {
104
            $errors[] = 'Target Node cannot be empty.';
105
        }
106
107
        $label = $data['label'] ?? '';
108
        if (0 == strlen($label)) {
109
            $errors[] = 'Name cannot be empty.';
110
        } elseif (15 < strlen($label)) {
111
            $errors[] = 'Name is too long. Max length is 15 characters.';
112
        }
113
114
        $placement = $data['placement'] ?? '';
115
        if (0 == strlen($placement)) {
116
            $errors[] = 'Placement cannot be empty.';
117
        }
118
119
        $data = array(
120
            'name' => $label,
121
        );
122
123
        if (count($errors)) {
124
            throw new ValidationError($errors);
125
        }
126
127
        try {
128
            $this->getTreeAdapter()
129
                ->addNode($targetId, $data, $placement);
130
        } catch (ValidationException $e) {
131
            throw new ValidationError([$e->getMessage()]);
132
        }
133
    }
134
135
    public function deleteNode(array $data): void
136
    {
137
        $errors = array();
138
139
        $id = $data['id'] ?? '';
140
141
        if (0 == strlen($id)) {
142
            $errors[] = 'Id is missing. Cannot perform delete operation.';
143
        }
144
145
        if (count($errors)) {
146
            throw new ValidationError($errors);
147
        }
148
149
        $this->getTreeAdapter()
150
            ->deleteBranch($id);
151
    }
152
153
    public function updateNode(array $data): void
154
    {
155
        $errors = array();
156
157
        $nodeId = $_POST['node_id'] ?? '';
158
        if (0 === strlen($nodeId)) {
159
            $errors[] = 'Node cannot be empty.';
160
        }
161
162
        $label = $data['label'] ?? '';
163
        if (0 == strlen($label)) {
164
            $errors[] = 'Name cannot be empty.';
165
        } elseif (15 < strlen($label)) {
166
            $errors[] = 'Name is too long. Max length is 15 characters.';
167
        }
168
169
        $data = array(
170
            'name' => $label,
171
        );
172
173
        if (count($errors)) {
174
            throw new ValidationError($errors);
175
        }
176
177
        try {
178
            $this->getTreeAdapter()
179
                ->updateNode($nodeId, $data);
180
        } catch (ValidationException $e) {
181
            throw new ValidationError([$e->getMessage()]);
182
        }
183
    }
184
185
    public function moveNode(array $data): void
186
    {
187
        $errors = array();
188
189
        $sourceId = $data['source_node_id'] ?? '';
190
        if (0 == strlen($sourceId)) {
191
            $errors[] = 'Source Node cannot be empty.';
192
        }
193
194
        $targetId = $data['target_node_id'] ?? '';
195
        if (0 == strlen($targetId)) {
196
            $errors[] = 'Target Node cannot be empty.';
197
        }
198
199
        $placement = $data['placement'] ?? '';
200
        if (0 == strlen($placement)) {
201
            $errors[] = 'Placement cannot be empty.';
202
        }
203
204
        if (count($errors)) {
205
            throw new ValidationError($errors);
206
        }
207
208
        try {
209
            $this->getTreeAdapter()
210
                ->moveNode($sourceId, $targetId, $placement);
211
        } catch (ValidationException $e) {
212
            throw new ValidationError([$e->getMessage()]);
213
        }
214
    }
215
216
    public function getRoots(): array
217
    {
218
        return $this->getTreeAdapter()
219
                    ->getRoots();
220
    }
221
222
    public function getDescendants($nodeId): array
223
    {
224
        return $this->getTreeAdapter()
225
            ->getDescendantsQueryBuilder()
226
            ->get($nodeId);
227
    }
228
229
    public function findDescendants(array $criteria): array
230
    {
231
        $queryBuilder = $this->getTreeAdapter()
232
            ->getDescendantsQueryBuilder();
233
234
        $errors = array();
235
236
        $nodeId = $criteria['node_id'] ?? '';
237
        if (0 === strlen($nodeId)) {
238
            $errors[] = 'Node cannot be empty.';
239
        }
240
241
        $excludeFirstNLevel = $criteria['exclude_first_n_level'] ?? null;
242
        if (null !== $excludeFirstNLevel) {
243
            if (!preg_match('|^[0-9]*$|', $excludeFirstNLevel)) {
244
                $errors[] = 'Exclude First N Level  must be positive integer,';
245
            } else {
246
                $queryBuilder->excludeFirstNLevel((int) $excludeFirstNLevel);
247
            }
248
        }
249
250
        $levelLimit = $criteria['level_limit'] ?? null;
251
        if (null !== $levelLimit) {
252
            if (!preg_match('|^[0-9]*$|', $levelLimit)) {
253
                $errors[] = 'Level limit must be positive integer,';
254
            } else {
255
                $queryBuilder->levelLimit((int) $levelLimit);
256
            }
257
        }
258
259
        $excludeBranch = $criteria['exclude_node_id'] ?? null;
260
        if (null !== $excludeBranch) {
261
            $queryBuilder->excludeBranch($excludeBranch);
262
        }
263
264
        if (count($errors)) {
265
            throw new ValidationError($errors);
266
        }
267
268
        return $queryBuilder->get($nodeId);
269
    }
270
271
    public function findAncestors(array $criteria): array
272
    {
273
        $queryBuilder = $this->getTreeAdapter()
274
            ->getAncestorsQueryBuilder();
275
276
        $errors = array();
277
278
        $nodeId = $criteria['node_id'] ?? '';
279
        if (0 === strlen($nodeId)) {
280
            $errors[] = 'Node cannot be empty.';
281
        }
282
283
        $excludeFirstNLevel = $criteria['exclude_first_n_level'] ?? null;
284
        if (null !== $excludeFirstNLevel) {
285
            if (!preg_match('|^[0-9]*$|', $excludeFirstNLevel)) {
286
                $errors[] = 'Exclude First N Level  must be positive integer,';
287
            } else {
288
                $queryBuilder->excludeFirstNLevel((int) $excludeFirstNLevel);
289
            }
290
        }
291
292
        $excludeLastNLevel = $criteria['exclude_last_n_level'] ?? null;
293
        if (null !== $excludeLastNLevel) {
294
            if (!preg_match('|^[0-9]*$|', $excludeLastNLevel)) {
295
                $errors[] = 'Exclude Last N Level  must be positive integer,';
296
            } else {
297
                $queryBuilder->excludeLastNLevel((int) $excludeLastNLevel);
298
            }
299
        }
300
301
        if (count($errors)) {
302
            throw new ValidationError($errors);
303
        }
304
305
        return $queryBuilder->get($nodeId);
306
    }
307
}
308
309
class ViewHelper
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
310
{
311
    public function escape($string): string
312
    {
313
        return htmlspecialchars((string) $string);
314
    }
315
316
    public function renderTree(array $nodes): string
317
    {
318
        $html = '';
319
320
        $previousLevel = -1;
321
        foreach ($nodes as $node) {
322
            if ($previousLevel > $node['level']) {
323
                for ($i = $node['level']; $previousLevel > $i; ++$i) {
324
                    $html = $html.'</li></ul>';
325
                }
326
                $html = $html.'</li>';
327
            } elseif ($previousLevel < $node['level']) {
328
                $html = $html.'<ul>';
329
            } else {
330
                $html = $html.'</li>';
331
            }
332
333
            $html = $html.'<li>';
334
            $html = $html.'<span>'.$this->escape($node['name']).'</span>'
335
                .' <a href="/?action=delete&id='.$this->escape($node['id']).'" class="badge badge-danger">Delete</a>';
336
337
            $previousLevel = $node['level'];
338
        }
339
340
        for ($i = -1; $previousLevel > $i; ++$i) {
341
            $html = $html.'</li></ul>';
342
        }
343
344
        return $html;
345
    }
346
347
    public function renderBreadcrumbs(array $nodes): string
348
    {
349
        $html = '';
350
351
        foreach ($nodes as $node) {
352
            $html .= '<a class="breadcrumb-item" href="#">'.$this->escape($node['name']).'</a>';
353
        }
354
355
        return '<nav class="breadcrumb">'.$html.'</nav>';
356
    }
357
358
    public function renderSelectOptions(array $nodes): string
359
    {
360
        $pathCache = array();
361
362
        $html = '';
363
364
        foreach ($nodes as $node) {
365
            if (!$node['parent_id']) {
366
                $pathCache[$node['id']] = '/'.$node['name'];
367
            } else {
368
                $pathCache[$node['id']] = $pathCache[$node['parent_id']].'/'.$node['name'];
369
            }
370
            $html .= '<option value="'.$this->escape($node['id']).'">'.$this->escape($pathCache[$node['id']]).'</option>';
371
        }
372
373
        return '<option value="">---</option>'.$html;
374
    }
375
376
    public function renderPlacementOptions(): string
377
    {
378
        $placements = array(
379
            TreeInterface::PLACEMENT_CHILD_TOP => 'Child Top',
380
            TreeInterface::PLACEMENT_CHILD_BOTTOM => 'Child Bottom',
381
            TreeInterface::PLACEMENT_TOP => 'Top',
382
            TreeInterface::PLACEMENT_BOTTOM => 'Bottom',
383
        );
384
385
        $html = '';
386
387
        foreach ($placements as $id => $placement) {
388
            $html .= '<option value="'.$this->escape($id).'">'.$this->escape($placement).'</option>';
389
        }
390
391
        return '<option value="">---</option>'.$html;
392
    }
393
394
    public function renderErrorMessages(array $errors): string
395
    {
396
        $html = '';
397
398
        foreach ($errors as $error) {
399
            $html .= '<li>'.$this->escape($error).'</li>';
400
        }
401
402
        return '<div class="alert alert-danger"><ul class="error-container">'.$html.'</ul></div>';
403
    }
404
405
    public function renderSuccessMessage(string $message): string
406
    {
407
        return '<div class="alert alert-success">'.$this->escape($message).'</div>';
408
    }
409
410
    public function renderFlashMessage(): string
411
    {
412
        $message = $_SESSION['flashMessage'] ?? '';
413
414
        if ($message) {
415
            unset($_SESSION['flashMessage']);
416
417
            return $this->renderSuccessMessage($message);
418
        } else {
419
            return '';
420
        }
421
    }
422
}
423
424
class ValidationError extends \Exception
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
425
{
426
    private $errorMessages = array();
427
428
    public function __construct(array $errorMessages = array())
429
    {
430
        $this->errorMessages = $errorMessages;
431
    }
432
433
    public function addError(string $error): void
434
    {
435
        $this->errorMessages[] = $error;
436
    }
437
438
    public function addErrors(array $errors): void
439
    {
440
        $this->errorMessages = array_merge(array_values($errors), $this->errorMessages);
441
    }
442
443
    public function getErrors(): array
444
    {
445
        return $this->errorMessages;
446
    }
447
}
448
449
function setFlashMessageAndRedirect(string $message, string $url)
450
{
451
    $_SESSION['flashMessage'] = $message;
452
    $redirectUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http')."://$_SERVER[HTTP_HOST]$url";
453
    header(sprintf('Location: %s', $redirectUrl));
454
    die();
455
}
456
457
/************************************
458
 *    Router
459
 ***********************************/
460
$service = new Service($treeAdapter);
461
462
try {
463
    switch ($_GET['action'] ?? '') {
464
        case 'create-scope':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
465
            $service->createRoot($_POST);
466
            setFlashMessageAndRedirect('New root node and scope was successfully created.', '/');
467
            break;
468
        case 'create-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
469
            $service->createNode($_POST);
470
            setFlashMessageAndRedirect('New node was successfully created.', '/');
471
            break;
472
        case 'move-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
473
            $service->moveNode($_POST);
474
            setFlashMessageAndRedirect('Branch/Node was successfully moved.', '/');
475
            break;
476
        case 'update-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
477
            $service->updateNode($_POST);
478
            setFlashMessageAndRedirect('Node was successfully updated.', '/');
479
            break;
480
        case 'delete':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
481
            $service->deleteNode($_GET);
482
            setFlashMessageAndRedirect('Branch/Node was successfully deleted.', '/');
483
            break;
484
        case 'descendant-test':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
485
            $descendants = $service->findDescendants($_GET);
486
            $showDescendantTestBlock = true;
487
            break;
488
        case 'ancestor-test':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
489
            $ancestors = $service->findAncestors($_GET);
490
            $showAncestorTestBlock = true;
491
            break;
492
    }
493
} catch (ValidationError $e) {
494
    $errorMessage = $e->getErrors();
495
}
496
497
/************************************
498
 *   View
499
 ***********************************/
500
$wh = new ViewHelper();
501
?>
502
503
<html>
504
    <head>
505
        <title>Demo</title>
506
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
507
        <style>
508
            .error-container {
509
                margin-bottom: 0;
510
                padding-left: 1rem;
511
            }
512
        </style>
513
    </head>
514
    <body>
515
        <div class="container-fluid">
516
            <h1><a href="/">Demo</a></h1>
517
518
            <?php
519
            if ($errorMessage ?? false) {
520
                echo $wh->renderErrorMessages($errorMessage);
521
            }
522
523
            echo $wh->renderFlashMessage();
524
            ?>
525
526
            <div class="row">
527
                <div class="col-sm-4">
528
                    <form action="/?action=create-scope" method="post">
529
                        <div class="form-group">
530
                            <label>Root name</label>
531
                            <input type="text" name="label" class="form-control" />
532
                        </div>
533
                        <div class="form-group">
534
                            <label>Scope name</label>
535
                            <input type="text" name="scope" class="form-control" />
536
                        </div>
537
                        <input type="submit" value="Create" class="btn btn-primary" />
538
                    </form>
539
                </div>
540
            </div>
541
            <?php
542
            foreach ($service->getRoots() as $root) {
543
                $nodes = $service->getDescendants($root['id']); ?>
544
                <hr />
545
                <h2>Scope - <?php echo $wh->escape($root['group_id']); ?></h2>
546
547
                <div class="row">
548
                    <div class="col-sm-2">
549
                        <h3>Create</h3>
550
                        <form action="/?action=create-node" method="post">
551
                            <div class="form-group">
552
                                <label>Name</label>
553
                                <input type="text" name="label" class="form-control" />
554
                            </div>
555
                            <div class="form-group">
556
                                <label>Target Node</label>
557
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
558
                            </div>
559
                            <div class="form-group">
560
                                <label>Placement</label>
561
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
562
                            </div>
563
                            <div class="form-group">
564
                                <input type="submit" value="Create" class="btn btn-primary" />
565
                            </div>
566
                        </form>
567
                    </div>
568
569
                    <div class="col-sm-2">
570
                        <h3>Move</h3>
571
                        <form action="/?action=move-node" method="post">
572
                            <div class="form-group">
573
                                <label>Source Node</label>
574
                                <select name="source_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
575
                            </div>
576
                            <div class="form-group">
577
                                <label>Target Node</label>
578
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
579
                            </div>
580
                            <div class="form-group">
581
                                <label>Placement</label>
582
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
583
                            </div>
584
                            <input type="submit" value="Move" class="btn btn-primary" />
585
                        </form>
586
                    </div>
587
588
                    <div class="col-sm-2">
589
                        <h3>Update</h3>
590
                        <form action="/?action=update-node" method="post">
591
                            <div class="form-group">
592
                                <label>Name</label>
593
                                <input type="text" name="label" class="form-control" />
594
                            </div>
595
                            <div class="form-group">
596
                                <label>Node</label>
597
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
598
                            </div>
599
                            <input type="submit" value="Update" class="btn btn-primary" />
600
                        </form>
601
                    </div>
602
603
                    <div class="col-sm-3">
604
                        <h3>Descendant Test</h3>
605
                        <form action="/" method="get">
606
                            <div class="form-group">
607
                                <label>Node</label>
608
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
609
                            </div>
610
                            <div class="form-group">
611
                                <label>Exclude First N Level</label>
612
                                <input type="number" min="0" step="1" name="exclude_first_n_level" class="form-control" />
613
                            </div>
614
                            <div class="form-group">
615
                                <label>Level Limit</label>
616
                                <input type="number" min="0" step="1" name="level_limit" class="form-control" />
617
                            </div>
618
                            <div class="form-group">
619
                                <label>Exclude Branch</label>
620
                                <select name="exclude_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
621
                            </div>
622
                            <input type="hidden" name="action" value="descendant-test" />
623
                            <input type="hidden" name="scope" value="<?php echo $wh->escape($root['group_id']); ?>" />
624
                            <input type="submit" value="Show" class="btn btn-primary" />
625
                        </form>
626
                    </div>
627
628
                    <div class="col-sm-3">
629
                        <h3>Ancestor Test</h3>
630
                        <form action="/" method="get">
631
                            <div class="form-group">
632
                                <label>Node</label>
633
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
634
                            </div>
635
                            <div class="form-group">
636
                                <label>Exclude First N Level</label>
637
                                <input type="number" min="0" step="1" name="exclude_first_n_level" class="form-control" />
638
                            </div>
639
                            <div class="form-group">
640
                                <label>Exclude Last N Level</label>
641
                                <input type="number" min="0" step="1" name="exclude_last_n_level" class="form-control" />
642
                            </div>
643
                            <input type="hidden" name="action" value="ancestor-test" />
644
                            <input type="hidden" name="scope" value="<?php echo $wh->escape($root['group_id']); ?>" />
645
                            <input type="submit" value="Show" class="btn btn-primary" />
646
                        </form>
647
                    </div>
648
                </div>
649
650
                <hr />
651
652
                <div class="row">
653
                    <div class="col-sm-6">
654
                        <h3>Whole Tree</h3>
655
                        <?php echo $wh->renderTree($nodes); ?>
656
                    </div>
657
                    <div class="col-sm-6">
658
                        <?php
659
                        if (($showDescendantTestBlock ?? false) && $root['group_id'] == $_GET['scope']) {
660
                            ?>
661
                            <h3>Descendants Test Result</h3>
662
                            <?php
663
                            if (0 == count($descendants)) {
664
                                echo $wh->renderErrorMessages(['No descendants was found']);
665
                            } else {
666
                                echo $wh->renderTree($descendants);
667
                            } ?>
668
                        <?php
669
                        } ?>
670
671
                        <?php
672
                        if (($showAncestorTestBlock ?? false) && $root['group_id'] == $_GET['scope']) {
673
                            ?>
674
                            <h3>Ancestors Test Result</h3>
675
                            <?php
676
                            if (0 == count($ancestors)) {
677
                                echo $wh->renderErrorMessages(['No ancestors was found']);
678
                            } else {
679
                                echo $wh->renderBreadcrumbs($ancestors);
680
                                echo $wh->renderTree($ancestors);
681
                            } ?>
682
                            <?php
683
                        } ?>
684
                    </div>
685
                </div>
686
            <?php
687
            }?>
688
        </div>
689
    </body>
690
</html>