Skip to content

Commit b9ae3b2

Browse files
authored
Merge pull request #75 from LaswitchTech/dev
General: Version bumped to v0.0.75
2 parents 4ceb812 + 7e58a92 commit b9ae3b2

File tree

5 files changed

+166
-139
lines changed

5 files changed

+166
-139
lines changed

Helper/CoreHelper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ private function item(string $id, array $menu, int $level): string
10991099
$label = $menu['label'];
11001100
$icon = $menu['icon'];
11011101
$link = $menu['link'];
1102-
$items = $menu['items'];
1102+
$items = $menu['items'] ?? [];
11031103

11041104
if ($level == 1) {
11051105
$html = '<li class="nav-item">';

Template/View/panel.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@
4343
<?php if($this->Config->get('application','show_nav_title')): ?>
4444
<div class="border border-start-0 border-end-0 p-2 px-3 mb-2"><?= $this->Locale->get('Main Navigation') ?></div>
4545
<?php endif; ?>
46-
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-main')); ?>
46+
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-main',null,3)); ?>
4747
<?php if($this->Auth->isAuthorized("Administration",1)): ?>
4848
<?php if($this->Config->get('application','show_nav_title')): ?>
4949
<div class="border border-start-0 border-end-0 p-2 px-3 mb-2"><?= $this->Locale->get('Administration') ?></div>
5050
<?php endif; ?>
51-
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-admin')); ?>
51+
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-admin',null,3)); ?>
5252
<?php endif; ?>
5353
<?php if($this->Auth->isAuthorized("Development",1)): ?>
5454
<?php if($this->Config->get('application','show_nav_title')): ?>
5555
<div class="border border-start-0 border-end-0 p-2 px-3 mb-2"><?= $this->Locale->get('Development') ?></div>
5656
<?php endif; ?>
57-
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-dev')); ?>
57+
<?= $this->Helper->Core->menu($this->Builder->menu('sidebar-dev',null,3)); ?>
5858
<?php endif; ?>
5959
</aside>
6060

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.0.74
1+
v0.0.75

assets/js/core.js

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,11 @@ document.addEventListener('DOMContentLoaded', () => {
7979

8080
// Set active link
8181
(() => {
82-
$('a[href="'+window.location.pathname + window.location.search+'"]').each(function () {
82+
$('a[href="'+window.location.pathname + window.location.search+'"],button[data-route="'+window.location.pathname+'"]').each(function () {
8383
$(this).addClass('active');
8484
$(this).parents('.collapse').addClass('show');
8585
$(this).parents('[data-bs-toggle="collapse"]').attr('aria-expanded',true);
8686
});
87-
$('button').each(function () {
88-
if ($(this).attr('data-route') === window.location.pathname) {
89-
$(this).addClass('active');
90-
$(this).parents('.collapse').addClass('show');
91-
$(this).parents('[data-bs-toggle="collapse"]').attr('aria-expanded',true);
92-
}
93-
});
9487
})();
9588

9689
// Sidebar Toggle
@@ -749,67 +742,6 @@ document.addEventListener('DOMContentLoaded', () => {
749742
// retrieveMessages();
750743
// }, 20000);
751744

752-
// // Configure Task
753-
// builder.Task._properties.callback.viewAll = function(){
754-
// window.location.href = '/tasks';
755-
// };
756-
757-
// // Handle Tasks
758-
// var statusTask = true;
759-
// const disableTask = function(){
760-
// statusTask = false;
761-
// }
762-
// const enableTask = function(){
763-
// statusTask = true;
764-
// }
765-
// const toggleTask = function(){
766-
// statusTask = !statusTask;
767-
// }
768-
// let tasks = {};
769-
// const retrieveTasks = function(){
770-
// if(!AUTHENTICATED) return;
771-
// if(!statusTask) return;
772-
// api.post('task/get',{}, {
773-
// error:function(xhr,status,error){
774-
// clearInterval(intervalTasks);
775-
// },
776-
// success:function(response){
777-
// for(const [id, task] of Object.entries(response)){
778-
// if(typeof tasks[id] === 'undefined'){
779-
// if(task.isActive){
780-
// builder.Task.add(
781-
// {
782-
// label: builder.Parser.parse(task.label),
783-
// progress: {
784-
// scale: task.scale,
785-
// color: task.color,
786-
// },
787-
// click: function(item,component){
788-
// if(task.link){
789-
// window.location.href = task.link;
790-
// }
791-
// },
792-
// },
793-
// function(item){
794-
// item.set(task.progress);
795-
// tasks[id] = {item: item, task: task};
796-
// },
797-
// );
798-
// }
799-
// } else {
800-
// const item = tasks[id].item;
801-
// tasks[id] = {item: item, task: task};
802-
// item.set(task.progress);
803-
// }
804-
// }
805-
// },
806-
// });
807-
// }
808-
// retrieveTasks();
809-
// const intervalTasks = setInterval(function(){
810-
// retrieveTasks();
811-
// }, 20000);
812-
813745
// // Add General Button Events
814746
// $(document).ready(function(){
815747

src/Builder.php

Lines changed: 160 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class Builder {
1515
protected $CSRF;
1616
protected $Auth;
1717

18+
// Properties
19+
protected $Routes;
20+
1821
/**
1922
* Constructor
2023
*/
@@ -33,94 +36,186 @@ public function __construct()
3336
}
3437

3538
/**
36-
* Get a menu
39+
* Get the routes
3740
*
38-
* @param string $location
39-
* @param string $parent
4041
* @return array
4142
*/
42-
public function menu($location = 'sidebar', $parent = null)
43+
public function routes(): array
4344
{
44-
// Import Global Variables
45-
global $AUTH;
46-
$menu = [];
47-
$routes = $this->Config->get('routes');
48-
49-
// Load Plugins Routes
50-
$pluginsPath = $this->Config->root() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'plugins';
51-
if(is_dir($pluginsPath)){
52-
foreach(array_diff(scandir($pluginsPath), array('..', '.')) as $plugin){
53-
$pluginPath = $pluginsPath . DIRECTORY_SEPARATOR . $plugin;
54-
if(is_file($pluginPath . DIRECTORY_SEPARATOR . 'routes.cfg')){
55-
foreach(json_decode(file_get_contents($pluginPath . DIRECTORY_SEPARATOR . 'routes.cfg'),true) as $route => $param){
56-
if(!isset($routes[$route])){
57-
$routes[$route] = $param;
45+
if ($this->Routes === null) {
46+
$this->Routes = $this->Config->get('routes') ?? [];
47+
48+
$pluginsPath = $this->Config->root() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'plugins';
49+
if (is_dir($pluginsPath)) {
50+
foreach (array_diff(scandir($pluginsPath), ['.', '..', '.DS_Store']) as $plugin) {
51+
$cfg = $pluginsPath . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'routes.cfg';
52+
if (is_file($cfg)) {
53+
$pluginRoutes = json_decode(file_get_contents($cfg), true) ?: [];
54+
foreach ($pluginRoutes as $r => $p) {
55+
if (!isset($this->Routes[$r])) $this->Routes[$r] = $p;
5856
}
5957
}
6058
}
6159
}
60+
61+
ksort($this->Routes, SORT_NATURAL | SORT_FLAG_CASE);
6262
}
6363

64-
// Sort the routes
65-
ksort($routes);
66-
67-
foreach($routes as $route => $param) {
68-
if(!isset($param['parent']) || is_null($param['parent'])) $param['parent'] = [];
69-
if(!is_array($param['parent'])) $param['parent'] = [$param['parent']];
70-
if($parent && !in_array($parent,$param['parent'])) continue;
71-
if(!isset($param['location'])) continue;
72-
if(is_string($param['location']) && $param['location'] !== $location) continue;
73-
if(is_array($param['location']) && !in_array($location,$param['location'])) continue;
74-
if(!$param['public'] && !$AUTH->isAuthenticated()) continue;
75-
if(!$param['public'] && !$AUTH->isAuthorized("Route>" . $route, $param['level'])) continue;
76-
77-
$parts = array_filter(explode('/', $route));
78-
if(empty($parts)) $parts = [""];
79-
80-
$param['items'] = [];
81-
$param['link'] = $route;
82-
83-
if(!empty($param['parent'])){
84-
foreach($param['parent'] as $par){
85-
if(!array_key_exists($par, $menu)){
86-
$menu[$par] = [];
87-
}
88-
if(!array_key_exists('items', $menu[$par])){
89-
$menu[$par]['items'] = [];
90-
}
91-
$menu[$par]['items'][$route] = $param;
64+
return $this->Routes;
65+
}
66+
67+
/**
68+
* Build a menu from routes with depth control.
69+
*
70+
* Behavior:
71+
* - $maxDepth === 1: return a flat list of ALL descendants under the chosen parent(s)
72+
* (or global roots if $parent is null); no nesting, no wrapper key.
73+
* - $maxDepth >= 2: return a nested tree up to $maxDepth levels; items deeper than $maxDepth are omitted.
74+
*
75+
* Output node fields: label, icon, color, items, link
76+
*
77+
* @param string $location e.g. 'sidebar-main'
78+
* @param string|array|null $parent e.g. '/crm'. If null, build from global roots (no eligible parent).
79+
* @param int $maxDepth depth cap (1 = flat)
80+
* @return array
81+
*/
82+
public function menu($location = 'sidebar', $parent = null, int $maxDepth = 2): array
83+
{
84+
global $AUTH;
85+
86+
$maxDepth = max(1, (int)$maxDepth);
87+
88+
// 1) Collect eligible routes
89+
$all = $this->routes();
90+
if (!$all) return [];
91+
92+
$eligible = []; // route => ['label','icon','color','link','parents'=>[]]
93+
foreach ($all as $route => $p) {
94+
// normalize parents
95+
$parents = [];
96+
if (isset($p['parent']) && $p['parent'] !== null) {
97+
$parents = is_array($p['parent']) ? $p['parent'] : [$p['parent']];
98+
$parents = array_values(array_filter($parents, fn($x) => is_string($x) && $x !== ''));
99+
}
100+
101+
// location filter
102+
if (!isset($p['location'])) continue;
103+
if (is_string($p['location'])) {
104+
if ($p['location'] !== $location) continue;
105+
} elseif (is_array($p['location'])) {
106+
if (!in_array($location, $p['location'], true)) continue;
107+
} else continue;
108+
109+
// auth/public filter
110+
$isPublic = $p['public'] ?? false;
111+
$level = $p['level'] ?? 0;
112+
if (!$isPublic && (!$AUTH || !$AUTH->isAuthenticated())) continue;
113+
if (!$isPublic && (!$AUTH || !$AUTH->isAuthorized('Route>' . $route, $level))) continue;
114+
115+
$eligible[$route] = [
116+
'label' => $p['label'] ?? '',
117+
'icon' => $p['icon'] ?? null,
118+
'color' => $p['color'] ?? null,
119+
'link' => $route,
120+
'parents' => $parents,
121+
];
122+
}
123+
if (!$eligible) return [];
124+
125+
// 2) Build parent->children map among eligible routes
126+
$children = []; // parentRoute => [childRoute...]
127+
foreach ($eligible as $r => $_) $children[$r] = [];
128+
foreach ($eligible as $child => $node) {
129+
foreach ($node['parents'] as $par) {
130+
if (isset($eligible[$par])) $children[$par][] = $child;
131+
}
132+
}
133+
$sortKeys = function(array &$arr) { ksort($arr, SORT_NATURAL | SORT_FLAG_CASE); };
134+
$sortList = function(array &$list) { sort($list, SORT_NATURAL | SORT_FLAG_CASE); };
135+
foreach ($children as &$lst) $sortList($lst);
136+
unset($lst);
137+
138+
// 3) Determine start nodes
139+
$starts = [];
140+
if ($parent === null) {
141+
// global roots = nodes that have no eligible parent
142+
foreach ($eligible as $route => $node) {
143+
$hasEligibleParent = false;
144+
foreach ($node['parents'] as $p) {
145+
if (isset($eligible[$p])) { $hasEligibleParent = true; break; }
92146
}
93-
} else {
94-
if(array_key_exists($route, $menu)){
95-
$menu[$route] = array_merge_recursive($menu[$route], $param);
147+
if (!$hasEligibleParent) $starts[] = $route;
148+
}
149+
$sortList($starts);
150+
} else {
151+
$want = is_array($parent) ? $parent : [$parent];
152+
$seen = [];
153+
foreach ($want as $p) {
154+
if (isset($children[$p])) {
155+
foreach ($children[$p] as $c) { $seen[$c] = true; }
96156
} else {
97-
$menu[$route] = $param;
157+
// if parent isn't itself eligible, include any eligible that declares it as parent
158+
foreach ($eligible as $route => $node) {
159+
if (in_array($p, $node['parents'], true)) $seen[$route] = true;
160+
}
98161
}
99162
}
163+
$starts = array_keys($seen);
164+
$sortList($starts);
100165
}
101166

102-
foreach($menu as $route => $param) {
103-
if((!array_key_exists('link',$param) || is_null($param['link'])) && array_key_exists('items',$param)){
104-
foreach($param['items'] as $item => $parameters){
105-
if(!is_null($parameters['link']) && !array_key_exists($parameters['link'],$menu)){
106-
$menu[$parameters['link']] = $parameters;
107-
unset($menu[$route]['items'][$item]);
108-
}
109-
}
167+
// Helpers
168+
$makeLeaf = function(string $route) use ($eligible): array {
169+
return [
170+
'label' => $eligible[$route]['label'],
171+
'icon' => $eligible[$route]['icon'],
172+
'color' => $eligible[$route]['color'],
173+
'items' => [],
174+
'link' => $eligible[$route]['link'],
175+
];
176+
};
177+
178+
// 4a) Depth = 1: FLAT list of ALL descendants of the start set
179+
if ($maxDepth === 1) {
180+
$out = [];
181+
$queue = $starts;
182+
$visited = [];
183+
while ($queue) {
184+
$cur = array_shift($queue);
185+
if (isset($visited[$cur])) continue;
186+
$visited[$cur] = true;
187+
$out[$cur] = $makeLeaf($cur);
188+
// enqueue children (we want *all* descendants in flat mode)
189+
foreach ($children[$cur] ?? [] as $ch) $queue[] = $ch;
110190
}
191+
$sortKeys($out);
192+
return $out;
111193
}
112194

113-
foreach($menu as $route => $param) {
114-
if((!array_key_exists('link',$param) || is_null($param['link'])) && array_key_exists('items',$param)){
115-
if(empty($param['items'])){
116-
unset($menu[$route]);
117-
}
195+
// 4b) Depth >= 2: build nested tree up to $maxDepth; deeper nodes are omitted
196+
$buildTree = function(string $route, int $depth) use (&$buildTree, $maxDepth, $children, $makeLeaf): array {
197+
$node = $makeLeaf($route);
198+
if ($depth >= $maxDepth) return $node; // reached cap; omit deeper nodes
199+
$items = [];
200+
foreach ($children[$route] ?? [] as $ch) {
201+
$items[$ch] = $buildTree($ch, $depth + 1);
118202
}
119-
}
203+
if ($items) {
204+
ksort($items, SORT_NATURAL | SORT_FLAG_CASE);
205+
$node['items'] = $items;
206+
}
207+
return $node;
208+
};
120209

121-
return $menu;
210+
$result = [];
211+
foreach ($starts as $s) {
212+
$result[$s] = $buildTree($s, 1);
213+
}
214+
$sortKeys($result);
215+
return $result;
122216
}
123217

218+
124219
/**
125220
* Create Crumbs
126221
*

0 commit comments

Comments
 (0)