OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
120
app/Domain/Menu/Composers/HeadMenu.php
Normal file
120
app/Domain/Menu/Composers/HeadMenu.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Menu\Composers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Composer;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Core\UI\Theme;
|
||||
use Leantime\Domain\Auth\Services\Auth as AuthService;
|
||||
use Leantime\Domain\Help\Services\Helper;
|
||||
use Leantime\Domain\Menu\Repositories\Menu as MenuRepo;
|
||||
use Leantime\Domain\Notifications\Services\Notifications as NotificationService;
|
||||
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
|
||||
use Leantime\Domain\Users\Services\Users as UserService;
|
||||
|
||||
class HeadMenu extends Composer
|
||||
{
|
||||
public static array $views = [
|
||||
'menu::headMenu',
|
||||
];
|
||||
|
||||
private NotificationService $notificationService;
|
||||
|
||||
private TimesheetService $timesheets;
|
||||
|
||||
private UserService $userService;
|
||||
|
||||
private AuthService $authService;
|
||||
|
||||
private Helper $helperService;
|
||||
|
||||
private Theme $themeCore;
|
||||
|
||||
private MenuRepo $menuRepo;
|
||||
|
||||
public function init(
|
||||
NotificationService $notificationService,
|
||||
TimesheetService $timesheets,
|
||||
UserService $userService,
|
||||
AuthService $authService,
|
||||
Helper $helperService,
|
||||
MenuRepo $menuRepo,
|
||||
Theme $themeCore
|
||||
): void {
|
||||
$this->notificationService = $notificationService;
|
||||
$this->timesheets = $timesheets;
|
||||
$this->userService = $userService;
|
||||
$this->authService = $authService;
|
||||
$this->helperService = $helperService;
|
||||
$this->menuRepo = $menuRepo;
|
||||
$this->themeCore = $themeCore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function with(): array
|
||||
{
|
||||
// Fetch all notifications once, then filter for unread in PHP
|
||||
// instead of making two separate DB queries
|
||||
$notifications = [];
|
||||
if (session()->exists('userdata')) {
|
||||
$notifications = $this->notificationService->getAllNotifications(session('userdata.id'));
|
||||
}
|
||||
|
||||
$nCount = is_array($notifications) ? count(array_filter($notifications, fn ($n) => ($n['read'] ?? 1) == 0)) : 0;
|
||||
$totalNotificationCount =
|
||||
$totalMentionCount =
|
||||
$totalNewMentions =
|
||||
$totalNewNotifications = 0;
|
||||
|
||||
$menuType = $this->menuRepo->getSectionMenuType(FrontcontrollerCore::getCurrentRoute(), 'project');
|
||||
|
||||
foreach ($notifications as $notif) {
|
||||
if ($notif['type'] == 'mention') {
|
||||
$totalMentionCount++;
|
||||
if ($notif['read'] == 0) {
|
||||
$totalNewMentions++;
|
||||
}
|
||||
} else {
|
||||
$totalNotificationCount++;
|
||||
if ($notif['read'] == 0) {
|
||||
$totalNewNotifications++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = false;
|
||||
if (session()->exists('userdata')) {
|
||||
$user = $this->userService->getUser(session('userdata.id'));
|
||||
}
|
||||
|
||||
if (! $user) {
|
||||
$this->authService->logout();
|
||||
FrontcontrollerCore::redirect(BASE_URL.'/auth/login');
|
||||
}
|
||||
|
||||
$modal = $this->helperService->getHelperModalByRoute(FrontcontrollerCore::getCurrentRoute());
|
||||
|
||||
if (! session()->exists('companysettings.logoPath')) {
|
||||
session(['companysettings.logoPath' => $this->themeCore->getLogoUrl()]);
|
||||
}
|
||||
|
||||
return [
|
||||
'newNotificationCount' => $nCount,
|
||||
'totalNotificationCount' => $totalNotificationCount,
|
||||
'totalMentionCount' => $totalMentionCount,
|
||||
'totalNewMentions' => $totalNewMentions,
|
||||
'totalNewNotifications' => $totalNewNotifications,
|
||||
'menuType' => $menuType,
|
||||
'notifications' => $notifications,
|
||||
'onTheClock' => session()->exists('userdata') ? $this->timesheets->isClocked(session('userdata.id')) : false,
|
||||
'activePath' => FrontcontrollerCore::getCurrentRoute(),
|
||||
'action' => FrontcontrollerCore::getActionName(),
|
||||
'module' => FrontcontrollerCore::getModuleName(),
|
||||
'user' => $user,
|
||||
'modal' => $modal,
|
||||
];
|
||||
}
|
||||
}
|
||||
130
app/Domain/Menu/Composers/Menu.php
Normal file
130
app/Domain/Menu/Composers/Menu.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Menu\Composers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Composer;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\Http\IncomingRequest as IncomingRequestCore;
|
||||
use Leantime\Domain\Menu\Repositories\Menu as MenuRepository;
|
||||
|
||||
class Menu extends Composer
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
public static array $views = [
|
||||
'menu::menu',
|
||||
];
|
||||
|
||||
private MenuRepository $menuRepo;
|
||||
|
||||
private IncomingRequestCore $incomingRequest;
|
||||
|
||||
private \Leantime\Domain\Menu\Services\Menu $menuService;
|
||||
|
||||
public function init(
|
||||
MenuRepository $menuRepo,
|
||||
\Leantime\Domain\Menu\Services\Menu $menuService,
|
||||
IncomingRequestCore $request
|
||||
): void {
|
||||
$this->menuRepo = $menuRepo;
|
||||
$this->menuService = $menuService;
|
||||
$this->incomingRequest = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function with(): array
|
||||
{
|
||||
$allAssignedprojects = $showSettingsIndicator = false;
|
||||
$allAvailableProjects =
|
||||
$recentProjects =
|
||||
$favoriteProjects =
|
||||
$clients =
|
||||
$allAvailableProjectsHierarchy =
|
||||
$allAssignedprojectsHierarchy = [];
|
||||
|
||||
$currentClient = '';
|
||||
$currentProject = '';
|
||||
$projectType = '';
|
||||
$menuType = 'default';
|
||||
|
||||
$projectSelectFilter = session('usersettings.projectSelectFilter') ?? [
|
||||
'groupBy' => 'structure',
|
||||
'client' => null,
|
||||
];
|
||||
|
||||
if (session()->exists('userdata')) {
|
||||
// Getting all projects (ignoring client filter, clients are filtered on the frontend)
|
||||
$projectVars = $this->menuService->getUserProjectList(session('userdata.id'), $projectSelectFilter['client']);
|
||||
|
||||
$allAssignedprojects = $projectVars['assignedProjects'];
|
||||
$allAvailableProjects = $projectVars['availableProjects'];
|
||||
$allAvailableProjectsHierarchy = $projectVars['availableProjectsHierarchy'];
|
||||
$allAssignedprojectsHierarchy = $projectVars['assignedHierarchy'];
|
||||
$currentClient = $projectVars['currentClient'];
|
||||
$menuType = $projectVars['menuType'];
|
||||
$projectType = $projectVars['projectType'];
|
||||
$recentProjects = $projectVars['recentProjects'];
|
||||
$favoriteProjects = $projectVars['favoriteProjects'];
|
||||
$clients = $projectVars['clients'];
|
||||
$currentProject = $projectVars['currentProject'];
|
||||
}
|
||||
|
||||
$menuType = $this->menuRepo->getSectionMenuType(FrontcontrollerCore::getCurrentRoute(), $menuType);
|
||||
|
||||
if (str_contains($redirectUrl = $this->incomingRequest->getRequestUri(), 'showProject')) {
|
||||
$redirectUrl = '/dashboard/show';
|
||||
}
|
||||
|
||||
$projectTypeAvatars = $this->menuService->getProjectTypeAvatars();
|
||||
$projectSelectGroupOptions = $this->menuService->getProjectSelectorGroupingOptions();
|
||||
|
||||
$settingsLink = [
|
||||
'label' => '',
|
||||
'module' => '',
|
||||
'action' => '',
|
||||
'settingsIcon' => '',
|
||||
'settingsTooltip' => '',
|
||||
];
|
||||
|
||||
if ($menuType == 'project' || $menuType == 'default') {
|
||||
$settingsLink = [
|
||||
'label' => __('menu.project_settings'),
|
||||
'module' => 'projects',
|
||||
'action' => 'showProject',
|
||||
'settingsIcon' => __('menu.project_settings_icon'),
|
||||
'settingsTooltip' => __('menu.project_settings_tooltip'),
|
||||
];
|
||||
}
|
||||
|
||||
$settingsLink = self::dispatch_filter('settingsLink', $settingsLink, ['type' => $menuType]);
|
||||
|
||||
$newProjectUrl = self::dispatch_filter('startSomething', BASE_URL.'/projects/newProject');
|
||||
|
||||
return [
|
||||
'currentClient' => $currentClient,
|
||||
'module' => FrontcontrollerCore::getModuleName(),
|
||||
'action' => FrontcontrollerCore::getActionName(),
|
||||
'currentProjectType' => $projectType,
|
||||
'allAssignedProjects' => $allAssignedprojects,
|
||||
'allAvailableProjects' => $allAvailableProjects,
|
||||
'allAvailableProjectsHierarchy' => $allAvailableProjectsHierarchy,
|
||||
'projectHierarchy' => $allAssignedprojectsHierarchy,
|
||||
'recentProjects' => $recentProjects,
|
||||
'currentProject' => $currentProject,
|
||||
'menuStructure' => $this->menuRepo->getMenuStructure($menuType ?? ''),
|
||||
'menuType' => $menuType,
|
||||
'settingsLink' => $settingsLink,
|
||||
'redirectUrl' => $redirectUrl,
|
||||
'projectTypeAvatars' => $projectTypeAvatars,
|
||||
'favoriteProjects' => $favoriteProjects,
|
||||
'projectSelectGroupOptions' => $projectSelectGroupOptions,
|
||||
'projectSelectFilter' => $projectSelectFilter,
|
||||
'clients' => $clients,
|
||||
'startSomethingUrl' => $newProjectUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Domain/Menu/Composers/ProjectSelector.php
Normal file
107
app/Domain/Menu/Composers/ProjectSelector.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Menu\Composers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Composer;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\Http\IncomingRequest as IncomingRequestCore;
|
||||
|
||||
class ProjectSelector extends Composer
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
public static array $views = [
|
||||
'menu::projectSelector',
|
||||
];
|
||||
|
||||
private IncomingRequestCore $incomingRequest;
|
||||
|
||||
private \Leantime\Domain\Menu\Services\Menu $menuService;
|
||||
|
||||
public function init(
|
||||
\Leantime\Domain\Menu\Services\Menu $menuService,
|
||||
IncomingRequestCore $request
|
||||
): void {
|
||||
$this->menuService = $menuService;
|
||||
$this->incomingRequest = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function with(): array
|
||||
{
|
||||
$allAssignedprojects =
|
||||
$allAvailableProjects =
|
||||
$recentProjects =
|
||||
$favoriteProjects =
|
||||
$clients =
|
||||
$allAvailableProjectsHierarchy =
|
||||
$allAssignedprojectsHierarchy = [];
|
||||
|
||||
$currentClient = '';
|
||||
$currentProject = '';
|
||||
$projectType = '';
|
||||
$menuType = 'default';
|
||||
|
||||
$projectSelectFilter = session('usersettings.projectSelectFilter', [
|
||||
'groupBy' => 'structure',
|
||||
'client' => null,
|
||||
]);
|
||||
|
||||
if (session()->exists('userdata')) {
|
||||
// Getting all projects (ignoring client filter, clients are filtered on the frontend)
|
||||
$projectVars = $this->menuService->getUserProjectList(session('userdata.id'), $projectSelectFilter['client']);
|
||||
|
||||
$allAssignedprojects = $projectVars['assignedProjects'];
|
||||
$allAvailableProjects = $projectVars['availableProjects'];
|
||||
$allAvailableProjectsHierarchy = $projectVars['availableProjectsHierarchy'];
|
||||
$allAssignedprojectsHierarchy = $projectVars['assignedHierarchy'];
|
||||
$currentClient = $projectVars['currentClient'];
|
||||
|
||||
$projectType = $projectVars['projectType'];
|
||||
$recentProjects = $projectVars['recentProjects'];
|
||||
$favoriteProjects = $projectVars['favoriteProjects'];
|
||||
$clients = $projectVars['clients'];
|
||||
$currentProject = $projectVars['currentProject'];
|
||||
}
|
||||
|
||||
if (str_contains($redirectUrl = $this->incomingRequest->getRequestUri(), 'showProject')) {
|
||||
$redirectUrl = '/dashboard/show';
|
||||
}
|
||||
|
||||
$projectTypeAvatars = $this->menuService->getProjectTypeAvatars();
|
||||
$projectSelectGroupOptions = $this->menuService->getProjectSelectorGroupingOptions();
|
||||
|
||||
$newProjectUrl = self::dispatch_filter('startSomething', BASE_URL.'/projects/newProject');
|
||||
|
||||
return [
|
||||
'currentClient' => $currentClient,
|
||||
'module' => FrontcontrollerCore::getModuleName(),
|
||||
'action' => FrontcontrollerCore::getActionName(),
|
||||
'currentProjectType' => $projectType,
|
||||
'allAssignedProjects' => $allAssignedprojects,
|
||||
'allAvailableProjects' => $allAvailableProjects,
|
||||
'allAvailableProjectsHierarchy' => $allAvailableProjectsHierarchy,
|
||||
'projectHierarchy' => $allAssignedprojectsHierarchy,
|
||||
'recentProjects' => $recentProjects,
|
||||
'currentProject' => $currentProject,
|
||||
'settingsLink' => [
|
||||
'label' => __('menu.project_settings'),
|
||||
'module' => 'projects',
|
||||
'action' => 'showProject',
|
||||
'settingsIcon' => __('menu.project_settings_icon'),
|
||||
'settingsTooltip' => __('menu.project_settings_tooltip'),
|
||||
],
|
||||
'redirectUrl' => $redirectUrl,
|
||||
'projectTypeAvatars' => $projectTypeAvatars,
|
||||
'favoriteProjects' => $favoriteProjects,
|
||||
'projectSelectGroupOptions' => $projectSelectGroupOptions,
|
||||
'projectSelectFilter' => $projectSelectFilter,
|
||||
'clients' => $clients,
|
||||
'startSomethingUrl' => $newProjectUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Domain/Menu/Hxcontrollers/ProjectSelector.php
Normal file
53
app/Domain/Menu/Hxcontrollers/ProjectSelector.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Menu\Hxcontrollers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Core\Controller\HtmxController;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Domain\Menu\Services\Menu;
|
||||
|
||||
class ProjectSelector extends HtmxController
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
protected static string $view = 'menu::partials.projectSelector';
|
||||
|
||||
private Menu $menuService;
|
||||
|
||||
/**
|
||||
* Controller constructor
|
||||
*/
|
||||
public function init(Menu $menuService): void
|
||||
{
|
||||
$this->menuService = $menuService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function updateMenu(): void
|
||||
{
|
||||
$projectSelectFilter = [
|
||||
'groupBy' => $_POST['groupBy'] ?? 'none',
|
||||
'client' => (int) ($_POST['client'] ?? null),
|
||||
];
|
||||
|
||||
$userId = session()->exists('userdata') ? (int) session('userdata.id') : null;
|
||||
|
||||
$viewData = $this->menuService->getProjectSelectorViewData(
|
||||
$userId,
|
||||
$projectSelectFilter,
|
||||
FrontcontrollerCore::getCurrentRoute(),
|
||||
$this->incomingRequest->getRequestUri()
|
||||
);
|
||||
|
||||
array_map([$this->tpl, 'assign'], array_keys($viewData), array_values($viewData));
|
||||
|
||||
$this->tpl->assign('module', FrontcontrollerCore::getModuleName());
|
||||
$this->tpl->assign('action', FrontcontrollerCore::getActionName());
|
||||
}
|
||||
|
||||
public function filter(): void {}
|
||||
}
|
||||
204
app/Domain/Menu/Js/menuController.js
Normal file
204
app/Domain/Menu/Js/menuController.js
Normal file
@@ -0,0 +1,204 @@
|
||||
leantime.menuController = (function () {
|
||||
|
||||
|
||||
//Functions
|
||||
|
||||
var toggleSubmenu = function (submenuName) {
|
||||
|
||||
if (submenuName === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
var submenuDisplay = jQuery('#submenu-' + submenuName).css('display');
|
||||
var submenuState = '';
|
||||
|
||||
if (submenuDisplay == 'none') {
|
||||
jQuery('#submenu-' + submenuName).css('display', 'block');
|
||||
jQuery('#submenu-icon-' + submenuName).removeClass('fa-angle-right');
|
||||
jQuery('#submenu-icon-' + submenuName).addClass('fa-angle-down');
|
||||
submenuState = 'open';
|
||||
} else {
|
||||
jQuery('#submenu-' + submenuName).css('display', 'none');
|
||||
jQuery('#submenu-icon-' + submenuName).removeClass('fa-angle-down');
|
||||
jQuery('#submenu-icon-' + submenuName).addClass('fa-angle-right');
|
||||
submenuState = 'closed';
|
||||
}
|
||||
|
||||
leantime.rpc('Api.Api.setSubmenuState', {
|
||||
submenu: submenuName,
|
||||
state: submenuState
|
||||
}).catch(function (e) { console.error('Could not update submenu state', e); });
|
||||
}
|
||||
|
||||
var initProjectSelector = function () {
|
||||
|
||||
jQuery(".project-select").chosen();
|
||||
|
||||
jQuery(document).on('click', '.projectselector.dropdown-menu', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
let currentTab = localStorage.getItem("currentMenuTab");
|
||||
|
||||
if (typeof currentTab === 'undefined') {
|
||||
activeTabIndex = 0;
|
||||
} else {
|
||||
activeTabIndex = jQuery('.projectSelectorTabs').find('a[href="#' + currentTab + '"]').parent().index();
|
||||
}
|
||||
|
||||
jQuery('.projectSelectorTabs').tabs({
|
||||
create: function ( event, ui ) {
|
||||
|
||||
},
|
||||
activate: function (event, ui) {
|
||||
localStorage.setItem("currentMenuTab", ui.newPanel[0].id);
|
||||
},
|
||||
load: function () {
|
||||
|
||||
},
|
||||
enable: function () {
|
||||
|
||||
},
|
||||
active: activeTabIndex
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Below this width the sidebar is an off-canvas drawer (matches the
|
||||
// mobile-like CSS breakpoint in mobile.css). See #3185, #2878.
|
||||
var isMobileMenu = function () {
|
||||
return window.innerWidth < 1200;
|
||||
};
|
||||
|
||||
var initLeftMenuHamburgerButton = function () {
|
||||
|
||||
// On mobile/tablet always start with the drawer closed, regardless of
|
||||
// the saved desktop preference. Don't persist this — it's view-only.
|
||||
if (isMobileMenu()) {
|
||||
jQuery(".mainwrapper").removeClass("menuopen");
|
||||
jQuery(".mainwrapper").addClass("menuclosed");
|
||||
}
|
||||
|
||||
jQuery('.barmenu').click(function (e) {
|
||||
|
||||
// Don't let this click bubble to the close-on-outside handler below,
|
||||
// otherwise opening the drawer immediately closes it again.
|
||||
e.stopPropagation();
|
||||
|
||||
// On mobile/tablet the drawer is view-only: toggle it but do NOT
|
||||
// persist, otherwise opening the drawer on a phone would overwrite
|
||||
// the user's saved desktop sidebar preference.
|
||||
var persistState = !isMobileMenu();
|
||||
|
||||
if (jQuery(".mainwrapper").hasClass('menuopen')) {
|
||||
jQuery(".mainwrapper").removeClass("menuopen");
|
||||
jQuery(".mainwrapper").addClass("menuclosed");
|
||||
|
||||
//If it doesn't have the class open, the user wants it to be open.
|
||||
if (persistState) {
|
||||
leantime.menuRepository.updateUserMenuSettings("closed");
|
||||
}
|
||||
} else {
|
||||
jQuery(".mainwrapper").removeClass("menuclosed");
|
||||
jQuery(".mainwrapper").addClass("menuopen");
|
||||
|
||||
//If it doesn't have the class open, the user wants it to be open.
|
||||
if (persistState) {
|
||||
leantime.menuRepository.updateUserMenuSettings("open");
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Mobile drawer: tapping the dimmed backdrop closes it. The backdrop is
|
||||
// a real element covering the content, so this is reliable even when
|
||||
// content widgets stopPropagation on their own clicks. View-only.
|
||||
jQuery(".menu-backdrop").on('click', function () {
|
||||
jQuery(".mainwrapper").removeClass("menuopen").addClass("menuclosed");
|
||||
});
|
||||
|
||||
// When the viewport crosses from desktop into mobile width, collapse
|
||||
// the drawer so it doesn't sit open over the content.
|
||||
var wasMobile = isMobileMenu();
|
||||
jQuery(window).on('resize', function () {
|
||||
var nowMobile = isMobileMenu();
|
||||
if (nowMobile && !wasMobile) {
|
||||
jQuery(".mainwrapper").removeClass("menuopen").addClass("menuclosed");
|
||||
}
|
||||
wasMobile = nowMobile;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
var toggleProjectDropDownList = function (id, set="", prefix) {
|
||||
|
||||
//toggler-ID (link to click on open/close)
|
||||
//dropdown-ID (dropdown to open/close)
|
||||
|
||||
//Part 1 allow devs to set open/closed state.
|
||||
//This means we need to do the opposite of what the current state is.
|
||||
if (set === "closed") {
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("closed");
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("open");
|
||||
jQuery("#" + prefix + "-toggler-" + id).addClass("open");
|
||||
} else if (set === "open") {
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("open");
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("closed");
|
||||
jQuery("#" + prefix + "-toggler-" + id).addClass("closed");
|
||||
}
|
||||
|
||||
//Part 2
|
||||
//Do the toggle. If the link has the class open, we need to close it.
|
||||
if (jQuery("#" + prefix + "-toggler-" + id).hasClass("open")) {
|
||||
//Update class on link
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("open");
|
||||
jQuery("#" + prefix + "-toggler-" + id).addClass("closed");
|
||||
|
||||
//Update icon on link
|
||||
jQuery("#" + prefix + "-toggler-" + id).find("i").removeClass("fa-angle-down");
|
||||
jQuery("#" + prefix + "-toggler-" + id).find("i").addClass("fa-angle-right");
|
||||
|
||||
|
||||
jQuery("#" + prefix + "-projectSelectorlist-group-" + id).addClass("closed");
|
||||
jQuery("#" + prefix + "-projectSelectorlist-group-" + id).removeClass("open");
|
||||
|
||||
updateGroupDropdownSetting(id, "closed", prefix);
|
||||
} else {
|
||||
//Update class on link
|
||||
jQuery("#" + prefix + "-toggler-" + id).removeClass("closed");
|
||||
jQuery("#" + prefix + "-toggler-" + id).addClass("open");
|
||||
|
||||
//Update icon on link
|
||||
jQuery("#" + prefix + "-toggler-" + id).find("i").removeClass("fa-angle-right");
|
||||
jQuery("#" + prefix + "-toggler-" + id).find("i").addClass("fa-angle-down");
|
||||
|
||||
|
||||
jQuery("#" + prefix + "-projectSelectorlist-group-" + id).addClass("open");
|
||||
jQuery("#" + prefix + "-projectSelectorlist-group-" + id).removeClass("closed");
|
||||
|
||||
updateGroupDropdownSetting(id, "open", prefix);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let updateGroupDropdownSetting = function (ID, state, prefix) {
|
||||
|
||||
leantime.rpc('Api.Api.setSubmenuState', {
|
||||
submenu: prefix + "-projectSelectorlist-group-" + ID,
|
||||
state: state
|
||||
}).catch(function (e) { console.error('Could not update submenu state', e); });
|
||||
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
toggleSubmenu:toggleSubmenu,
|
||||
initProjectSelector:initProjectSelector,
|
||||
initLeftMenuHamburgerButton:initLeftMenuHamburgerButton,
|
||||
updateGroupDropdownSetting: updateGroupDropdownSetting,
|
||||
toggleProjectDropDownList:toggleProjectDropDownList
|
||||
};
|
||||
|
||||
})();
|
||||
18
app/Domain/Menu/Js/menuRepository.js
Normal file
18
app/Domain/Menu/Js/menuRepository.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var leantime = leantime || {};
|
||||
|
||||
leantime.menuRepository = (function () {
|
||||
|
||||
//Functions
|
||||
|
||||
var updateUserMenuSettings = function (menuStateValue) {
|
||||
|
||||
leantime.rpc('Api.Api.setMainMenuState', { state: menuStateValue })
|
||||
.catch(function (e) { console.error('Could not update menu state', e); });
|
||||
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
updateUserMenuSettings: updateUserMenuSettings
|
||||
};
|
||||
})();
|
||||
447
app/Domain/Menu/Repositories/Menu.php
Normal file
447
app/Domain/Menu/Repositories/Menu.php
Normal file
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* menu class - Menu definitions
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Menu\Repositories;
|
||||
|
||||
use Leantime\Core\Configuration\Environment as EnvironmentCore;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\Language as LanguageCore;
|
||||
use Leantime\Domain\Auth\Services\Auth as AuthService;
|
||||
use Leantime\Domain\Setting\Repositories\Setting as SettingRepository;
|
||||
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
|
||||
|
||||
class Menu
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
// Default menu
|
||||
public const DEFAULT_MENU = 'default';
|
||||
|
||||
// Menu structures
|
||||
public array $menuStructures = [
|
||||
'default' => [
|
||||
5 => ['type' => 'item', 'module' => 'dashboard', 'title' => 'menu.overview', 'icon' => 'fa fa-fw fa-gauge-high', 'tooltip' => 'menu.overview_tooltip', 'href' => '/dashboard/show', 'active' => ['show']],
|
||||
10 => [
|
||||
'type' => 'submenu', 'id' => 'materialize', 'title' => 'menu.make', 'visual' => 'open',
|
||||
'submenu' => [
|
||||
15 => ['type' => 'item', 'module' => 'tickets', 'title' => 'menu.todos', 'icon' => 'fa fa-fw fa-thumb-tack', 'tooltip' => 'menu.todos_tooltip', 'href' => '', 'hrefFunction' => 'getTicketMenu', 'active' => ['showKanban', 'showAll', 'showTicket', 'showList']],
|
||||
25 => ['type' => 'item', 'module' => 'tickets', 'title' => 'menu.milestones', 'icon' => 'fa fa-fw fa-chart-gantt', 'tooltip' => 'menu.milestones_tooltip', 'href' => '', 'hrefFunction' => 'getTimelineMenu', 'active' => ['roadmap', 'showAllMilestones', 'showProjectCalendar']],
|
||||
40 => ['type' => 'item', 'module' => 'goalcanvas', 'title' => 'menu.goals', 'icon' => 'fa fa-fw fa-bullseye', 'tooltip' => 'menu.goals_tooltip', 'href' => '/goalcanvas/dashboard', 'active' => ['showCanvas', 'dashboard']],
|
||||
],
|
||||
],
|
||||
30 => [
|
||||
'type' => 'submenu', 'id' => 'understand', 'title' => 'menu.think', 'visual' => 'closed',
|
||||
'submenu' => [
|
||||
30 => ['type' => 'item', 'module' => 'ideas', 'title' => 'menu.ideas', 'icon' => 'fa fa-fw fa-lightbulb', 'tooltip' => 'menu.ideas_tooltip', 'href' => '', 'hrefFunction' => 'getIdeaMenu', 'active' => ['showBoards', 'advancedBoards']],
|
||||
50 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.blueprints', 'icon' => 'fa fa-fw fa-compass-drafting', 'tooltip' => 'menu.blueprints_tooltip', 'href' => '/blueprints/showBoards'],
|
||||
],
|
||||
],
|
||||
40 => [
|
||||
'type' => 'submenu', 'id' => 'dataroom', 'title' => 'menu.dataroom', 'visual' => 'closed',
|
||||
'submenu' => [
|
||||
55 => ['type' => 'item', 'module' => 'bom', 'title' => 'menu.bom', 'icon' => 'fa fa-fw fa-list-check', 'tooltip' => 'menu.bom_tooltip', 'href' => '/bom/show', 'active' => ['show']],
|
||||
56 => ['type' => 'item', 'module' => 'bom', 'title' => 'menu.process', 'icon' => 'fa fa-fw fa-file-lines', 'tooltip' => 'menu.process_tooltip', 'href' => '/bom/show?type=process', 'active' => ['show']],
|
||||
57 => ['type' => 'item', 'module' => 'bom', 'title' => 'menu.tooling', 'icon' => 'fa fa-fw fa-wrench', 'tooltip' => 'menu.tooling_tooltip', 'href' => '/bom/show?type=tooling', 'active' => ['show']],
|
||||
60 => ['type' => 'item', 'module' => 'wiki', 'title' => 'menu.wiki', 'icon' => 'fa fa-fw fa-book', 'tooltip' => 'menu.wiki_tooltip', 'href' => '/wiki/show'],
|
||||
70 => ['type' => 'item', 'module' => 'files', 'title' => 'menu.files', 'icon' => 'fa fa-fw fa-file', 'tooltip' => 'menu.files_tooltip', 'href' => '/files/browse'],
|
||||
80 => ['type' => 'item', 'module' => 'reports', 'title' => 'menu.reports', 'icon' => 'fa fa-fw fa-chart-bar', 'tooltip' => 'menu.reports_tooltip', 'href' => '/reports/project', 'active' => ['project', 'show'], 'role' => 'editor'],
|
||||
],
|
||||
],
|
||||
],
|
||||
// Display all menu items
|
||||
'full_menu' => [
|
||||
10 => [
|
||||
'type' => 'submenu', 'id' => 'planning', 'title' => 'menu.planning_execution', 'visual' => 'open',
|
||||
'submenu' => [
|
||||
11 => ['type' => 'item', 'module' => 'dashboard', 'title' => 'menu.dashboard', 'icon' => 'fa fa-fw fa-home', 'tooltip' => 'menu.dashboard_tooltip', 'href' => '/dashboard/show', 'active' => ['show']],
|
||||
21 => ['type' => 'item', 'module' => 'tickets', 'title' => 'menu.todos', 'icon' => 'fa fa-fw fa-thumb-tack', 'tooltip' => 'menu.todos_tooltip', 'href' => '', 'hrefFunction' => 'getTicketMenu', 'active' => ['showKanban', 'showAll', 'showTicket']],
|
||||
31 => ['type' => 'item', 'module' => 'tickets', 'title' => 'menu.milestones', 'icon' => 'fa fa-fw fa-sliders', 'tooltip' => 'menu.milestones_tooltip', 'href' => '/tickets/roadmap', 'active' => ['roadmap']],
|
||||
40 => ['type' => 'item', 'module' => 'goalcanvas', 'title' => 'menu.goals', 'icon' => 'fa fa-fw fa-bullseye', 'tooltip' => 'menu.goals_tooltip', 'href' => '/goalcanvas/showCanvas'],
|
||||
],
|
||||
],
|
||||
50 => [
|
||||
'type' => 'submenu', 'id' => 'dts-process', 'title' => 'menu.dts.process', 'visual' => 'closed',
|
||||
'submenu' => [
|
||||
51 => ['type' => 'item', 'module' => 'blueprints', 'icon' => 'far fa-fw fa-note-sticky', 'tooltip' => 'menu.insightscanvas_tooltip', 'title' => 'menu.insightscanvas', 'href' => '/blueprints/insights/showCanvas', 'active' => ['insights']],
|
||||
52 => ['type' => 'item', 'module' => 'ideas', 'icon' => 'fa fa-fw fa-lightbulb', 'tooltip' => 'menu.ideas_tooltip', 'title' => 'menu.ideation', 'href' => '/ideas/showBoards'],
|
||||
],
|
||||
],
|
||||
60 => [
|
||||
'type' => 'submenu', 'id' => 'dts-frameworks', 'title' => 'menu.dts.frameworks', 'visual' => 'closed',
|
||||
'submenu' => [
|
||||
61 => ['type' => 'header', 'title' => 'menu.dts.observe'],
|
||||
62 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.sbcanvas', 'icon' => 'fas fa-fw fa-list-check', 'tooltip' => 'menu.sbcanvas_tooltip', 'href' => '/blueprints/sb/showCanvas', 'active' => ['sb']],
|
||||
63 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.riskscanvas', 'icon' => 'fas fa-fw fa-person-falling', 'tooltip' => 'menu.riskscanvas_tooltip', 'href' => '/blueprints/risks/showCanvas', 'active' => ['risks']],
|
||||
64 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.eacanvas', 'icon' => 'fas fa-fw fa-tree', 'tooltip' => 'menu.eacanvas_tooltip', 'href' => '/blueprints/ea/showCanvas', 'active' => ['ea']],
|
||||
65 => ['type' => 'header', 'title' => 'menu.dts.design'],
|
||||
66 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.lbmcanvas', 'icon' => 'fas fa-fw fa-building', 'tooltip' => 'menu.lbmcanvas_tooltip', 'href' => '/blueprints/lbm/showCanvas', 'active' => ['lbm']],
|
||||
67 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.dbmcanvas', 'icon' => 'fas fa-fw fa-building', 'tooltip' => 'menu.dbmcanvas_tooltip', 'href' => '/blueprints/dbm/showCanvas', 'active' => ['dbm']],
|
||||
68 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.cpcanvas', 'icon' => 'fas fa-fw fa-city', 'tooltip' => 'menu.cpcanvas_tooltip', 'href' => '/blueprints/cp/showCanvas', 'active' => ['cp']],
|
||||
69 => ['type' => 'header', 'title' => 'menu.dts.validate'],
|
||||
70 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.sqcanvas', 'icon' => 'fas fa-fw fa-chess', 'tooltip' => 'menu.sqcanvas_tooltip', 'href' => '/blueprints/sq/showCanvas', 'active' => ['sq']],
|
||||
71 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.smcanvas', 'icon' => 'fas fa-fw fa-message', 'tooltip' => 'menu.smcanvas_tooltip', 'href' => '/blueprints/sm/showCanvas', 'active' => ['sm']],
|
||||
],
|
||||
],
|
||||
80 => [
|
||||
'type' => 'submenu', 'id' => 'dts-admin', 'title' => 'menu.dts.admin', 'visual' => 'open',
|
||||
'submenu' => [
|
||||
81 => ['type' => 'item', 'module' => 'wiki', 'title' => 'menu.wiki', 'icon' => 'fa fa-fw fa-book', 'tooltip' => 'menu.wiki_tooltip', 'href' => '/wiki/show'],
|
||||
82 => ['type' => 'item', 'module' => 'blueprints', 'title' => 'menu.retroscanvas', 'icon' => 'fa fa-fw fa-hand-spock', 'tooltip' => 'menu.retroscanvas_tooltip', 'href' => '/blueprints/retros/showCanvas', 'active' => ['retros']],
|
||||
83 => ['type' => 'item', 'module' => 'reports', 'title' => 'menu.reports', 'icon' => 'fa fa-fw fa-chart-bar', 'tooltip' => 'menu.reports_tooltip', 'href' => '/reports/project', 'active' => ['project', 'show'], 'role' => 'editor'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'personal' => [
|
||||
5 => ['type' => 'item', 'module' => 'dashboard', 'title' => 'menu.sidemenu_home', 'icon' => 'fa fa-house', 'tooltip' => 'menu.overview_tooltip', 'href' => '/dashboard/home', 'active' => ['home']],
|
||||
7 => ['type' => 'item', 'module' => 'projects', 'title' => 'menu.sidemenu_my_project_hub', 'icon' => 'fa fa-solid fa-house-flag', 'tooltip' => 'menu.projecthub_tooltip', 'href' => '/projects/showMy', 'active' => ['showMy'], 'role' => 'editor'],
|
||||
15 => ['type' => 'item', 'module' => 'timesheets', 'title' => 'menu.sidemenu_my_timesheets', 'icon' => 'fa-clock', 'tooltip' => 'menu.my_timesheets_tooltip', 'href' => '/timesheets/showMy', 'active' => ['showMy']],
|
||||
20 => ['type' => 'item', 'module' => 'calendar', 'title' => 'menu.sidemenu_my_calendar', 'icon' => 'fa fa-calendar', 'tooltip' => 'menu.my_calendar_tooltip', 'href' => '/calendar/showMyCalendar', 'active' => ['showMyCalendar']],
|
||||
],
|
||||
'projecthub' => [
|
||||
10 => ['type' => 'item', 'module' => 'projects', 'title' => 'menu.sidemenu_my_project_hub', 'icon' => 'fa-solid fa-house-flag', 'tooltip' => 'menu.my_projects_tooltip', 'href' => '/projects/showMy', 'active' => ['showMy']],
|
||||
],
|
||||
'company' => [
|
||||
10 => [
|
||||
'type' => 'submenu', 'id' => 'Management', 'title' => 'menu.sidemenu_management', 'visual' => 'open', 'role' => 'manager',
|
||||
'submenu' => [
|
||||
5 => ['type' => 'item', 'module' => 'timesheets', 'role' => 'manager', 'title' => 'menu.all_timesheets', 'icon' => 'fa fa-fw fa-business-time', 'tooltip' => 'menu.all_timesheets_tooltip', 'href' => '/timesheets/showAll', 'active' => ['showAll']],
|
||||
10 => ['type' => 'item', 'module' => 'projects', 'role' => 'manager', 'title' => 'menu.all_projects', 'icon' => 'fa fa-fw fa-briefcase', 'tooltip' => 'menu.all_projects_tooltip', 'href' => '/projects/showAll', 'active' => ['showAll']],
|
||||
15 => ['type' => 'item', 'module' => 'clients', 'role' => 'admin', 'title' => 'menu.all_clients', 'icon' => 'fa fa-fw fa-address-book', 'tooltip' => 'menu.all_clients_tooltip', 'href' => '/clients/showAll', 'active' => ['showAll']],
|
||||
20 => ['type' => 'item', 'module' => 'users', 'role' => 'admin', 'title' => 'menu.all_users', 'icon' => 'fa fa-fw fa-users', 'tooltip' => 'menu.all_users_tooltip', 'href' => '/users/showAll', 'active' => ['showAll']],
|
||||
],
|
||||
],
|
||||
15 => [
|
||||
'type' => 'submenu', 'id' => 'administration', 'title' => 'menu.sidemenu_administration', 'visual' => 'open', 'role' => 'admin',
|
||||
'submenu' => [
|
||||
5 => ['type' => 'item', 'module' => 'plugins', 'title' => 'menu.leantime_apps', 'icon' => 'fa fa-fw fa-puzzle-piece', 'tooltip' => 'menu.leantime_apps_tooltip', 'href' => '/plugins/marketplace', 'active' => ['marketplace', 'myapps']],
|
||||
10 => ['type' => 'item', 'module' => 'connector', 'title' => 'menu.integrations', 'icon' => 'fa fa-fw fa-circle-nodes', 'tooltip' => 'menu.connector_tooltip', 'href' => '/connector/show', 'active' => ['show']],
|
||||
15 => ['type' => 'item', 'module' => 'setting', 'title' => 'menu.company_settings', 'icon' => 'fa fa-fw fa-cogs', 'tooltip' => 'menu.company_settings_tooltip', 'href' => '/setting/editCompanySettings', 'active' => ['editCompanySettings']],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
/** @var SettingRepository */
|
||||
private SettingRepository $settingsRepo,
|
||||
/** @var LanguageCore */
|
||||
private LanguageCore $language,
|
||||
/** @var EnvironmentCore */
|
||||
private EnvironmentCore $config,
|
||||
/** @var TicketService */
|
||||
private TicketService $ticketsService,
|
||||
) {
|
||||
if (session()->exists('usersettings.submenuToggle') === false && session()->exists('userdata') === true) {
|
||||
$setting = $this->settingsRepo;
|
||||
session([
|
||||
'usersettings.submenuToggle' => safe_unserialize(
|
||||
$setting->getSetting('usersetting.'.session('userdata.id').'.submenuToggle'), []
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getMenuTypes - Return an array of a currently supported menu types
|
||||
*
|
||||
* @return array Array of supported menu types
|
||||
*/
|
||||
public function getMenuTypes(): array
|
||||
{
|
||||
$language = $this->language;
|
||||
$config = $this->config;
|
||||
|
||||
if (! isset($config->enableMenuType) || (isset($config->enableMenuType) && $config->enableMenuType === false)) {
|
||||
return [self::DEFAULT_MENU => $language->__('label.menu_type.'.self::DEFAULT_MENU)];
|
||||
}
|
||||
|
||||
$menuTypes = [];
|
||||
|
||||
foreach ($this->menuStructures as $key => $menu) {
|
||||
$menuTypes[$key] = $language->__("label.menu_type.$key");
|
||||
}
|
||||
|
||||
return $menuTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* setSubmenuState - Set the state of the submenu (open or closed)
|
||||
*
|
||||
* @param string $submenu Submenu identifier
|
||||
* @param string $state New state (open / closed)
|
||||
*/
|
||||
public function setSubmenuState(string $submenu, string $state): void
|
||||
{
|
||||
|
||||
if (session()->exists('usersettings.submenuToggle') && is_array(session('usersettings.submenuToggle')) && $submenu !== false) {
|
||||
session(['usersettings.submenuToggle.'.$submenu => $state]);
|
||||
}
|
||||
|
||||
$setting = $this->settingsRepo;
|
||||
$setting->saveSetting('usersetting.'.session('userdata.id').'.submenuToggle', serialize(session('usersettings.submenuToggle')));
|
||||
}
|
||||
|
||||
/**
|
||||
* getSubmenuState - Gets the state of the submenu (open or closed)
|
||||
*
|
||||
* @param string $submenu Submenu identifier
|
||||
*/
|
||||
public function getSubmenuState(string $submenu)
|
||||
{
|
||||
$setting = $this->settingsRepo;
|
||||
$subStructure = $setting->getSetting('usersetting.'.session('userdata.id').'.submenuToggle');
|
||||
|
||||
session(['usersettings.submenuToggle' => safe_unserialize($subStructure, [])]);
|
||||
|
||||
return session('usersettings.submenuToggle.'.$submenu) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the menu structure recursively.
|
||||
*
|
||||
* @param array &$menuStructure The menu structure to build. Passed by reference.
|
||||
* @param string $filter The filter to apply to the menu structure.
|
||||
* @return array The built menu structure.
|
||||
*/
|
||||
protected function buildMenuStructure(array &$menuStructure, string $filter): array
|
||||
{
|
||||
|
||||
foreach ($menuStructure as &$menuItem) {
|
||||
|
||||
if ($menuItem['type'] !== 'submenu') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$menuItem['submenu'] = $this->buildMenuStructure($menuItem['submenu'], $filter);
|
||||
|
||||
$filter = $filter.'.'.$menuItem['id'];
|
||||
|
||||
return self::dispatch_filter(
|
||||
hook: $filter,
|
||||
payload: $menuItem['submenu'],
|
||||
function: 'getMenuStructure'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return $menuStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* getMenu - Return a specific menu structure
|
||||
*
|
||||
* @param string $menuType Menu type to return
|
||||
* @return array Array of menu structrue
|
||||
*/
|
||||
public function getMenuStructure(string $menuType = ''): array
|
||||
{
|
||||
|
||||
if (empty($menuType)) {
|
||||
$menuType = self::DEFAULT_MENU;
|
||||
}
|
||||
|
||||
$this->menuStructures = self::dispatch_filter(
|
||||
'menuStructures',
|
||||
$this->menuStructures,
|
||||
['menuType' => $menuType]
|
||||
);
|
||||
|
||||
// If menu structure cannot be found, don't return anything
|
||||
if (! isset($this->menuStructures[$menuType])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$language = $this->language;
|
||||
$filter = "menuStructures.$menuType";
|
||||
|
||||
$this->menuStructures[$menuType] = self::dispatch_filter(
|
||||
$filter,
|
||||
$this->menuStructures[$menuType],
|
||||
['menuType' => $menuType]
|
||||
);
|
||||
|
||||
$menuStructure = $this->menuStructures[$menuType];
|
||||
|
||||
if (session()->exists('usersettings.submenuToggle') === false || is_array(session('usersettings.submenuToggle')) === false) {
|
||||
session(['usersettings.submenuToggle' => []]);
|
||||
}
|
||||
|
||||
ksort($menuStructure);
|
||||
|
||||
foreach ($menuStructure as $key => $element) {
|
||||
if (isset($menuStructure[$key]['title'])) {
|
||||
$menuStructure[$key]['title'] = $language->__($element['title']);
|
||||
}
|
||||
|
||||
switch ($element['type']) {
|
||||
case 'header':
|
||||
case 'separator':
|
||||
break;
|
||||
|
||||
case 'item':
|
||||
// TO DO: Check if menu is enabled, e.g. `$moduleManagerRepo->isModuleEnabled($element['module'])`
|
||||
$this->processMenuItem($element, $menuStructure[$key]);
|
||||
break;
|
||||
|
||||
case 'submenu':
|
||||
if (isset($element['submenuFunction'])) {
|
||||
if (method_exists($this, $this->{$element['submenuFunction']})) {
|
||||
$menuStructure[$key]['submenu'] = $this->{$element['submenuFunction']}();
|
||||
}
|
||||
}
|
||||
|
||||
// Update menu toggle
|
||||
if ($element['visual'] == 'always') {
|
||||
$submenuState = 'open';
|
||||
} else {
|
||||
$submenuState = session('usersettings.submenuToggle.'.$element['id']) ?? $element['visual'];
|
||||
session(['usersettings.submenuToggle.'.$element['id'] => $submenuState]);
|
||||
}
|
||||
$menuStructure[$key]['visual'] = $submenuState;
|
||||
|
||||
// Parse submenu
|
||||
foreach ($element['submenu'] as $subkey => $subelement) {
|
||||
ksort($menuStructure[$key]['submenu']);
|
||||
$menuStructure[$key]['submenu'][$subkey]['title'] = $language->__($menuStructure[$key]['submenu'][$subkey]['title']);
|
||||
|
||||
switch ($subelement['type']) {
|
||||
case 'header':
|
||||
break;
|
||||
|
||||
case 'item':
|
||||
$this->processMenuItem($subelement, $menuStructure[$key]['submenu'][$subkey]);
|
||||
break;
|
||||
|
||||
default:
|
||||
exit("Cannot proceed due to invalid submenu element: '".$subelement['type']."'");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
exit("Cannot proceed due to invalid menu element: '".$element['type']."'");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add menu filter here!
|
||||
|
||||
return $menuStructure;
|
||||
}
|
||||
|
||||
public function processMenuItem($element, &$structure): void
|
||||
{
|
||||
|
||||
// Update security
|
||||
if (isset($element['role'])) {
|
||||
$accessGranted = AuthService::userIsAtLeast($element['role'], true);
|
||||
|
||||
if (! $accessGranted) {
|
||||
$structure['type'] = 'disabled';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($element['hrefFunction'])) {
|
||||
if (method_exists($this, $element['hrefFunction'])) {
|
||||
$structure['href'] = $this->{$element['hrefFunction']}();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed|string|string[]
|
||||
*/
|
||||
public function getTicketMenu(): mixed
|
||||
{
|
||||
|
||||
$ticketService = $this->ticketsService;
|
||||
|
||||
// Removing base URL from here since it is being added in the menu for loop in the template
|
||||
$base_url = ! empty($this->config->appUrl) ? $this->config->appUrl : BASE_URL;
|
||||
|
||||
return str_replace($base_url, '', $ticketService->getLastTicketViewUrl());
|
||||
}
|
||||
|
||||
public function getTimelineMenu(): mixed
|
||||
{
|
||||
|
||||
$ticketService = $this->ticketsService;
|
||||
|
||||
// Removing base URL from here since it is being added in the menu for loop in the template
|
||||
$base_url = ! empty($this->config->appUrl) ? $this->config->appUrl : BASE_URL;
|
||||
|
||||
return str_replace($base_url, '', $ticketService->getLastTimelineViewUrl());
|
||||
}
|
||||
|
||||
public function getIdeaMenu(): string
|
||||
{
|
||||
$url = '/ideas/showBoards';
|
||||
if (session()->exists('lastIdeaView')) {
|
||||
if (session('lastIdeaView') == 'kanban') {
|
||||
$url = '/ideas/advancedBoards';
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request-level cache for getSectionMenuType results.
|
||||
* Prevents redundant computation when called from multiple composers (App, Menu, HeadMenu).
|
||||
* Keyed by route+default since different callers may pass different defaults.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private static array $sectionMenuTypeCache = [];
|
||||
|
||||
public function getSectionMenuType($currentRoute, $default = 'default')
|
||||
{
|
||||
// Cache key includes both route and default since the result depends on both.
|
||||
// Different composers may pass different defaults for routes not in the sections map.
|
||||
$cacheKey = $currentRoute.'|'.$default;
|
||||
if (isset(self::$sectionMenuTypeCache[$cacheKey])) {
|
||||
return self::$sectionMenuTypeCache[$cacheKey];
|
||||
}
|
||||
|
||||
$sections = [
|
||||
'dashboard.home' => 'personal',
|
||||
'projects.showMy' => 'personal',
|
||||
'timesheets.showMy' => 'personal',
|
||||
'calendar.showMyCalendar' => 'personal',
|
||||
'calendar.showMyList' => 'personal',
|
||||
'tickets.roadmapAll' => 'personal',
|
||||
'notes.showNotes' => 'personal',
|
||||
'notes.showNotesList' => 'personal',
|
||||
'tickets.showAllMilestonesOverview' => 'personal',
|
||||
'users.editOwn' => 'personal',
|
||||
'setting.editCompanySettings' => 'company',
|
||||
'timesheets.showAll' => 'company',
|
||||
'projects.showAll' => 'company',
|
||||
'clients.showAll' => 'company',
|
||||
'clients.newClient' => 'company',
|
||||
'clients.showClient' => 'company',
|
||||
'users.showAll' => 'company',
|
||||
'users.editUser' => 'company',
|
||||
'plugins.show' => 'company',
|
||||
'plugins.marketplace' => 'company',
|
||||
'plugins.myapps' => 'company',
|
||||
'connector.show' => 'company',
|
||||
'connector.integration' => 'company',
|
||||
'billing.subscriptions' => 'company',
|
||||
'llamadorian.statusCollector' => 'personal',
|
||||
];
|
||||
|
||||
$sections = self::dispatch_filter('menuSections', $sections, ['currentRoute' => $currentRoute, 'default' => $default]);
|
||||
|
||||
$result = $sections[$currentRoute] ?? $default;
|
||||
|
||||
self::$sectionMenuTypeCache[$cacheKey] = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
266
app/Domain/Menu/Services/Menu.php
Normal file
266
app/Domain/Menu/Services/Menu.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Menu\Services;
|
||||
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Domain\Menu\Repositories\Menu as MenuRepository;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Setting\Services\Setting;
|
||||
use Leantime\Domain\Users\Services\Users;
|
||||
|
||||
class Menu
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private Users $userService;
|
||||
|
||||
private Setting $settingSvc;
|
||||
|
||||
private MenuRepository $menuRepo;
|
||||
|
||||
public function __construct(
|
||||
ProjectService $projectService,
|
||||
Users $userService,
|
||||
Setting $settingSvc,
|
||||
MenuRepository $menuRepo
|
||||
) {
|
||||
|
||||
$this->projectService = $projectService;
|
||||
$this->userService = $userService;
|
||||
$this->settingSvc = $settingSvc;
|
||||
$this->menuRepo = $menuRepo;
|
||||
}
|
||||
|
||||
public function getUserProjectList(int $userId, null|int|string $client = null): array
|
||||
{
|
||||
|
||||
$allAssignedprojects =
|
||||
$allAvailableProjects =
|
||||
$recentProjects =
|
||||
$returnVars = [];
|
||||
|
||||
$user = $this->userService->getUser($userId);
|
||||
|
||||
$projects = $this->projectService->getProjectHierarchyAssignedToUser($userId, 'open', $client);
|
||||
$allAssignedprojects = $projects['allAssignedProjects'];
|
||||
$allAssignedprojectsHierarchy = $projects['allAssignedProjectsHierarchy'];
|
||||
$favoriteProjects = $projects['favoriteProjects'];
|
||||
|
||||
// Filtered
|
||||
$projects = $this->projectService->getProjectHierarchyAvailableToUser($userId, 'open', empty($client) ? session('userdata.clientId') : $client);
|
||||
$allAvailableProjects = $projects['allAvailableProjects'];
|
||||
$allAvailableProjectsHierarchy = $projects['allAvailableProjectsHierarchy'];
|
||||
|
||||
$clients = $this->projectService->getAllClientsAvailableToUser($userId, 'open');
|
||||
|
||||
$recent = $this->settingSvc->getSetting('usersettings.'.$userId.'.recentProjects');
|
||||
$recentArr = safe_unserialize($recent, []);
|
||||
|
||||
// Make sure the suer has access to the project
|
||||
if (is_array($recentArr) && is_array($allAvailableProjects)) {
|
||||
$availableProjectColumn = array_column($allAvailableProjects, 'id');
|
||||
foreach ($recentArr as $recentItem) {
|
||||
$found_key = array_search($recentItem, $availableProjectColumn);
|
||||
if ($found_key !== false) {
|
||||
$recentProjects[] = $allAvailableProjects[$found_key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$projectType = 'project';
|
||||
$project = [];
|
||||
if ($currentProjectId = $this->projectService->getCurrentProjectId()) {
|
||||
$project = $this->projectService->getProject($currentProjectId);
|
||||
|
||||
$projectType = ($project !== false && isset($project['type']))
|
||||
? $project['type']
|
||||
: 'project';
|
||||
|
||||
if ($projectType != '' && $projectType != 'project') {
|
||||
$menuType = $projectType;
|
||||
} else {
|
||||
$menuType = \Leantime\Domain\Menu\Repositories\Menu::DEFAULT_MENU;
|
||||
}
|
||||
|
||||
if ($project !== false && isset($project['clientId'])) {
|
||||
$currentClient = $project['clientId'];
|
||||
} else {
|
||||
$currentClient = '';
|
||||
}
|
||||
} else {
|
||||
$menuType = \Leantime\Domain\Menu\Repositories\Menu::DEFAULT_MENU;
|
||||
$currentClient = '';
|
||||
}
|
||||
|
||||
return [
|
||||
'assignedProjects' => $allAssignedprojects,
|
||||
'availableProjects' => $allAvailableProjects,
|
||||
'assignedHierarchy' => $allAssignedprojectsHierarchy,
|
||||
'availableProjectsHierarchy' => $allAvailableProjectsHierarchy,
|
||||
'currentClient' => $currentClient,
|
||||
'menuType' => $menuType,
|
||||
'recentProjects' => $recentProjects,
|
||||
'projectType' => $projectType,
|
||||
'favoriteProjects' => $favoriteProjects,
|
||||
'clients' => $clients,
|
||||
'currentProject' => $project,
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
public function getProjectTypeAvatars(): array
|
||||
{
|
||||
|
||||
$projectTypeAvatars = [
|
||||
'project' => 'avatar',
|
||||
'strategy' => 'fa fa-chess',
|
||||
'program' => 'fa fa-layer-group',
|
||||
];
|
||||
|
||||
return self::dispatch_filter('projectTypeAvatars', $projectTypeAvatars);
|
||||
}
|
||||
|
||||
public function getProjectSelectorGroupingOptions(): array
|
||||
{
|
||||
|
||||
$projectSelectGrouping =
|
||||
[
|
||||
'structure' => 'Group by Project Structure',
|
||||
'client' => 'Group by Client',
|
||||
'none' => 'No Grouping',
|
||||
];
|
||||
|
||||
return self::dispatch_filter('projectSelectorGrouping', $projectSelectGrouping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the settings link shown in the project selector for a given menu type.
|
||||
*
|
||||
* Returns the project settings link for project/default menus and an empty
|
||||
* link structure for all other menu types.
|
||||
*
|
||||
* @param string $menuType The resolved menu type (e.g. 'project', 'default', 'personal').
|
||||
* @return array<string, string> The settings link template structure.
|
||||
*/
|
||||
public function getProjectSelectorSettingsLink(string $menuType): array
|
||||
{
|
||||
if ($menuType == 'project' || $menuType == 'default') {
|
||||
return [
|
||||
'label' => __('menu.project_settings'),
|
||||
'module' => 'projects',
|
||||
'action' => 'showProject',
|
||||
'settingsIcon' => __('menu.project_settings_icon'),
|
||||
'settingsTooltip' => __('menu.project_settings_tooltip'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => '',
|
||||
'module' => '',
|
||||
'action' => '',
|
||||
'settingsIcon' => '',
|
||||
'settingsTooltip' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the redirect URL used by the project selector.
|
||||
*
|
||||
* Requests originating from the project overview ('showProject') are
|
||||
* redirected to the dashboard so that switching projects does not keep the
|
||||
* user on a project-specific settings page.
|
||||
*
|
||||
* @param string $requestUri The incoming request URI.
|
||||
* @return string The redirect URL to use after a project change.
|
||||
*/
|
||||
public function getProjectSelectorRedirectUrl(string $requestUri): string
|
||||
{
|
||||
if (str_contains($requestUri, 'showProject')) {
|
||||
return '/dashboard/show';
|
||||
}
|
||||
|
||||
return $requestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the complete set of template variables for the project selector partial.
|
||||
*
|
||||
* Persists the project select filter to the user's session, gathers the user's
|
||||
* project list (only when a user is logged in), resolves the section menu type
|
||||
* and menu structure, and computes the settings link, redirect URL and
|
||||
* "start something" new project URL.
|
||||
*
|
||||
* @param int|null $userId The current user's id, or null when no user is logged in.
|
||||
* @param array{groupBy: string, client: int} $projectSelectFilter Group/client filter from the request.
|
||||
* @param string $currentRoute The current Frontcontroller route (module.action).
|
||||
* @param string $requestUri The incoming request URI.
|
||||
* @return array<string, mixed> Flat map of template variable names to values.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getProjectSelectorViewData(?int $userId, array $projectSelectFilter, string $currentRoute, string $requestUri): array
|
||||
{
|
||||
session(['usersettings.projectSelectFilter' => $projectSelectFilter]);
|
||||
|
||||
$allAssignedprojects =
|
||||
$allAvailableProjects =
|
||||
$recentProjects =
|
||||
$favoriteProjects =
|
||||
$clients =
|
||||
$allAvailableProjectsHierarchy =
|
||||
$allAssignedprojectsHierarchy = [];
|
||||
|
||||
$currentClient = '';
|
||||
$currentProject = '';
|
||||
$projectType = '';
|
||||
$menuType = 'project';
|
||||
|
||||
if ($userId !== null) {
|
||||
// Getting all projects (ignoring client filter, clients are filtered on the frontend)
|
||||
$projectVars = $this->getUserProjectList($userId, $projectSelectFilter['client']);
|
||||
|
||||
$allAssignedprojects = $projectVars['assignedProjects'];
|
||||
$allAvailableProjects = $projectVars['availableProjects'];
|
||||
$allAvailableProjectsHierarchy = $projectVars['availableProjectsHierarchy'];
|
||||
$allAssignedprojectsHierarchy = $projectVars['assignedHierarchy'];
|
||||
$currentClient = $projectVars['currentClient'];
|
||||
$menuType = $projectVars['menuType'];
|
||||
$projectType = $projectVars['projectType'];
|
||||
$recentProjects = $projectVars['recentProjects'];
|
||||
$favoriteProjects = $projectVars['favoriteProjects'];
|
||||
$clients = $projectVars['clients'];
|
||||
$currentProject = $projectVars['currentProject'];
|
||||
}
|
||||
|
||||
$menuType = $this->menuRepo->getSectionMenuType($currentRoute, $menuType);
|
||||
|
||||
$redirectUrl = $this->getProjectSelectorRedirectUrl($requestUri);
|
||||
|
||||
$settingsLink = $this->getProjectSelectorSettingsLink($menuType);
|
||||
|
||||
$newProjectUrl = self::dispatch_filter('startSomething', BASE_URL.'/projects/newProject');
|
||||
|
||||
return [
|
||||
'currentClient' => $currentClient,
|
||||
'currentProjectType' => $projectType,
|
||||
'allAssignedProjects' => $allAssignedprojects,
|
||||
'allAvailableProjects' => $allAvailableProjects,
|
||||
'allAvailableProjectsHierarchy' => $allAvailableProjectsHierarchy,
|
||||
'projectHierarchy' => $allAssignedprojectsHierarchy,
|
||||
'recentProjects' => $recentProjects,
|
||||
'currentProject' => $currentProject,
|
||||
'menuStructure' => $this->menuRepo->getMenuStructure($menuType),
|
||||
'menuType' => $menuType,
|
||||
'settingsLink' => $settingsLink,
|
||||
'redirectUrl' => $redirectUrl,
|
||||
'projectTypeAvatars' => $this->getProjectTypeAvatars(),
|
||||
'favoriteProjects' => $favoriteProjects,
|
||||
'projectSelectGroupOptions' => $this->getProjectSelectorGroupingOptions(),
|
||||
'projectSelectFilter' => $projectSelectFilter,
|
||||
'clients' => $clients,
|
||||
'startSomethingUrl' => $newProjectUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
248
app/Domain/Menu/Templates/headMenu.blade.php
Normal file
248
app/Domain/Menu/Templates/headMenu.blade.php
Normal file
@@ -0,0 +1,248 @@
|
||||
@php use Leantime\Domain\Auth\Models\Roles; @endphp
|
||||
@dispatchEvent('beforeHeadMenu')
|
||||
|
||||
<ul class="headmenu pull-right">
|
||||
@dispatchEvent('insideHeadMenu')
|
||||
|
||||
@include('timesheets::partials.stopwatch', [
|
||||
'onTheClock' => $onTheClock
|
||||
])
|
||||
|
||||
@if ($login::userIsAtLeast("manager", true))
|
||||
<li class="notificationDropdown appsLink">
|
||||
<a
|
||||
class="dropdown-toggle profileHandler newsDropDownHandler"
|
||||
hx-get="{{ BASE_URL }}/plugins/marketplaceplugins/getLatest"
|
||||
hx-target="#pluginNewsDropdown"
|
||||
hx-indicator=".htmx-news-indicator"
|
||||
hx-trigger="click"
|
||||
preload="mouseover"
|
||||
data-toggle='dropdown'
|
||||
data-tippy-content='{{ __('popover.latest_plugins') }}'
|
||||
>
|
||||
<i class="fa-solid fa-puzzle-piece"></i>
|
||||
|
||||
</a>
|
||||
|
||||
<div class='dropdown-menu tw-p-m tw-h-screen tw-overflow-y-auto' id='pluginNewsDropdown'>
|
||||
<div class="htmx-indicator htmx-news-indicator">
|
||||
<x-global::loadingText type="text" count="3" includeHeadline="true" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<li class="notificationDropdown">
|
||||
<a
|
||||
class="dropdown-toggle profileHandler newsDropDownHandler"
|
||||
hx-get="{{ BASE_URL }}/notifications/news/get"
|
||||
hx-target="#newsDropdown"
|
||||
hx-indicator=".htmx-news-indicator"
|
||||
hx-trigger="click"
|
||||
preload="mouseover"
|
||||
data-toggle='dropdown'
|
||||
data-tippy-content='{{ __('popover.latest_updates') }}'
|
||||
>
|
||||
<span class="fa-solid fa-bolt-lightning"></span>
|
||||
<span hx-get="{{ BASE_URL }}/notifications/news-badge/get" hx-trigger="load" hx-target="this"></span>
|
||||
|
||||
</a>
|
||||
|
||||
<div class='dropdown-menu tw-p-m tw-h-screen tw-overflow-y-auto' id='newsDropdown'>
|
||||
<div class="htmx-indicator htmx-news-indicator">
|
||||
<x-global::loadingText type="text" count="3" includeHeadline="true" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="notificationDropdown">
|
||||
<a
|
||||
href='javascript:void(0);'
|
||||
class="dropdown-toggle profileHandler notificationHandler"
|
||||
data-toggle='dropdown'
|
||||
data-tippy-content='{{ __('popover.notifications') }}'
|
||||
>
|
||||
<span class="fa-solid fa-bell"></span>
|
||||
@if($newNotificationCount>0)
|
||||
<span class='notificationCounter'>{{ $newNotificationCount }}</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<div class='dropdown-menu' id='notificationsDropdown'>
|
||||
|
||||
<div class='dropdownTabs'>
|
||||
<a
|
||||
href='javascript:void(0);'
|
||||
class='notifcationTabs active'
|
||||
id="notificationsListLink"
|
||||
onclick="toggleNotificationTabs('notifications')"
|
||||
>Notification ({{ $totalNewNotifications }})</a>
|
||||
<a
|
||||
href='javascript:void(0);'
|
||||
class='notifcationTabs'
|
||||
id="mentionsListLink"
|
||||
onclick="toggleNotificationTabs('mentions')"
|
||||
>Mentions ({{ $totalNewMentions }})</a>
|
||||
</div>
|
||||
|
||||
<div class="scroll-wrapper">
|
||||
|
||||
<ul id='notificationsList' class='notifcationViewLists'>
|
||||
@if ($totalNotificationCount === 0)
|
||||
<p style='padding: 10px'>{{ __('text.no_notifications') }}</p>
|
||||
@endif
|
||||
|
||||
@foreach ($notifications as $notif)
|
||||
@if ($notif['type'] == 'mention')
|
||||
@continue
|
||||
@endif
|
||||
|
||||
<li
|
||||
@if ($notif['read'] == 0)
|
||||
class='new'
|
||||
@endif
|
||||
data-url="{{ $notif['url'] }}"
|
||||
data-id="{{ $notif['id'] }}"
|
||||
>
|
||||
<a href="{{ $notif['url'] }}">
|
||||
<span class="notificationProfileImage">
|
||||
<img src="{{ BASE_URL }}/api/users?profileImage={{ $notif['authorId'] }}"/>
|
||||
</span>
|
||||
<span class="notificationDate">
|
||||
{{ format($notif['datetime'])->date() }}
|
||||
{{ format($notif['datetime'])->time() }}
|
||||
</span>
|
||||
<span class="notificationTitle">{!! strip_tags($tpl->convertRelativePaths($notif['message'])) !!}</span>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<ul id='mentionsList' style='display:none;' class='notificationViewLists'>
|
||||
@if ($totalMentionCount === 0)
|
||||
<p style="padding: 10px">{{ __('text.no_notifications') }}</p>
|
||||
@endif
|
||||
|
||||
@foreach ($notifications as $notif)
|
||||
@if ($notif['type'] != 'mention')
|
||||
@continue
|
||||
@endif
|
||||
|
||||
<li
|
||||
@if ($notif['read'] == 0)
|
||||
class='new'
|
||||
@endif
|
||||
data-url="{{ $notif['url'] }}"
|
||||
data-id="{{ $notif['id'] }}"
|
||||
>
|
||||
<a href="{{ $notif['url'] }}">
|
||||
<span class="notificationProfileImage">
|
||||
<img src="{{ BASE_URL }}/api/users?profileImage={{ $notif['authorId'] }}"/>
|
||||
</span>
|
||||
<span class="notificationDate">
|
||||
{{ format($notif['datetime'])->date() }}
|
||||
{{ format($notif['datetime'])->time() }}
|
||||
</span>
|
||||
<span class="notificationTitle">{!! strip_tags($tpl->convertRelativePaths($notif['message'])) !!}</span>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div class="userloggedinfo">
|
||||
|
||||
@include("auth::partials.loginInfo")
|
||||
|
||||
</div>
|
||||
|
||||
@dispatchEvent('afterUser')
|
||||
|
||||
</li>
|
||||
|
||||
@dispatchEvent('beforeHeadMenuClose')
|
||||
|
||||
</ul>
|
||||
|
||||
<ul class="headmenu work-modes" style="height: 50px; float: left;">
|
||||
|
||||
@dispatchEvent('afterHeadMenuOpen')
|
||||
<li>
|
||||
@include('menu::projectSelector')
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="{{ BASE_URL }}/dashboard/home"
|
||||
@if ($menuType == 'personal')
|
||||
class="active"
|
||||
@endif
|
||||
data-tippy-content="{{ __('popover.my_work') }}"
|
||||
>{!! __('menu.my_work') !!}</a>
|
||||
</li>
|
||||
@if ($login::userIsAtLeast("manager", true))
|
||||
<li>
|
||||
@if($login::userHasRole("manager"))
|
||||
<a
|
||||
href="{{ BASE_URL }}/projects/showAll/"
|
||||
@if ($menuType == 'company')
|
||||
class="active"
|
||||
@endif
|
||||
data-tippy-content="{{ __('popover.company') }}"
|
||||
>{!! __('menu.company') !!}</a>
|
||||
@else
|
||||
<a
|
||||
href="{{ BASE_URL }}/setting/editCompanySettings/"
|
||||
@if ($menuType == 'company')
|
||||
class="active"
|
||||
@endif
|
||||
data-tippy-content="{{ __('popover.company') }}"
|
||||
>{!! __('menu.company') !!}</a>
|
||||
@endif
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
@dispatchEvent('afterHeadMenu')
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
function toggleNotificationTabs(active) {
|
||||
jQuery(".notifcationTabs").removeClass("active");
|
||||
jQuery('#' + active + 'ListLink').addClass("active");
|
||||
jQuery('.notifcationViewLists').hide();
|
||||
jQuery('#' + active + 'List').show();
|
||||
}
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
jQuery('.notificationHandler').on('click', function () {
|
||||
leantime.rpc('Notifications.Notifications.markRead', { id: 'all' })
|
||||
.then(function () {
|
||||
jQuery(".notifcationViewLists li.new").removeClass("new");
|
||||
jQuery(".notificationCounter").fadeOut();
|
||||
})
|
||||
.catch(function (e) { console.error('Could not mark notifications read', e); });
|
||||
});
|
||||
|
||||
jQuery('.notificationDropdown .dropdown-menu').on('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
jQuery('notificationsDropdown li').click(function () {
|
||||
const url = jQuery(this).data('url');
|
||||
const id = jQuery(this).data('id');
|
||||
|
||||
window.location.href = url;
|
||||
})
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
64
app/Domain/Menu/Templates/menu.blade.php
Normal file
64
app/Domain/Menu/Templates/menu.blade.php
Normal file
@@ -0,0 +1,64 @@
|
||||
@php
|
||||
/**
|
||||
* @todo Move this to Composer, or find a better
|
||||
* way to add filters for all passed variables
|
||||
*/
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
$settingsLink = $tpl->dispatchTplFilter(
|
||||
'settingsLink',
|
||||
$settingsLink,
|
||||
['type' => $menuType]
|
||||
);
|
||||
@endphp
|
||||
|
||||
|
||||
@dispatchEvent('beforeMenu')
|
||||
|
||||
<ul class="nav nav-tabs nav-stacked">
|
||||
|
||||
@dispatchEvent('afterMenuOpen')
|
||||
|
||||
@if ($allAvailableProjects
|
||||
|| !session()->has("currentProject")
|
||||
|| $menuType == "personal"
|
||||
|| $menuType == "company")
|
||||
|
||||
<li class="dropdown scrollableMenu">
|
||||
|
||||
<ul style="display:block;">
|
||||
|
||||
@foreach ($menuStructure as $key => $menuItem)
|
||||
|
||||
@includeIf("menu::partials.leftnav.".$menuItem['type'], ["menuItem" => $menuItem, "module" => $module, "action" => $action])
|
||||
|
||||
@endforeach
|
||||
|
||||
@if ($login::userIsAtLeast(Roles::$manager) && $menuType != 'company' && $menuType != 'personal' && $menuType != 'projecthub')
|
||||
<li class="fixedMenuPoint {{ $module == $settingsLink['module'] && $action == $settingsLink['action'] ? 'active' : '' }}">
|
||||
<a href="{{ BASE_URL }}/{{ $settingsLink['module'] }}/{{ $settingsLink['action'] }}/{{ session("currentProject") }}">
|
||||
{!! $settingsLink['label'] !!}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
|
||||
@endif
|
||||
|
||||
@dispatchEvent('beforeMenuClose')
|
||||
|
||||
</ul>
|
||||
@dispatchEvent('afterMenuClose')
|
||||
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
jQuery(document).ready(function () {
|
||||
leantime.menuController.initProjectSelector();
|
||||
leantime.menuController.initLeftMenuHamburgerButton();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
53
app/Domain/Menu/Templates/partials/clientGroup.blade.php
Normal file
53
app/Domain/Menu/Templates/partials/clientGroup.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
@php
|
||||
$lastClient = '';
|
||||
@endphp
|
||||
|
||||
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $parent }}" class="level-{{ $level }} projectGroup">
|
||||
@foreach($projects as $project)
|
||||
|
||||
@php
|
||||
$parentState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$project['clientId'], 'closed');
|
||||
@endphp
|
||||
|
||||
@if(
|
||||
!session()->exists("usersettings.projectSelectFilter.client")
|
||||
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|
||||
|| session("usersettings.projectSelectFilter.client") == 0
|
||||
|| session("usersettings.projectSelectFilter.client") == ""
|
||||
)
|
||||
|
||||
@if ($lastClient != $project['clientName'])
|
||||
|
||||
@php
|
||||
$lastClient = $project['clientName']
|
||||
@endphp
|
||||
|
||||
@if(!$loop->first)
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
<li class='projectLineItem clientIdHead-{{$project['clientId'] }}'>
|
||||
<a href="javascript:void(0);"
|
||||
class="toggler {{ $parentState }}"
|
||||
id="{{ $prefix }}-toggler-{{ $project["clientId"] }}"
|
||||
onclick="leantime.menuController.toggleProjectDropDownList('{{ $project["clientId"] }}', '', '{{ $prefix }}')">
|
||||
@if($parentState == 'closed')
|
||||
<i class="fa fa-angle-right"></i>
|
||||
@else
|
||||
<i class="fa fa-angle-down"></i>
|
||||
@endif
|
||||
</a>
|
||||
<a href="javascript:void(0)">
|
||||
{{ $project['clientName'] }}
|
||||
</a>
|
||||
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $project['clientId'] }}" class="level-1 projectGroup {{ $parentState }}">
|
||||
@endif
|
||||
|
||||
<li class="projectLineItem hasSubtitle {{ session("currentProject") == $project['id'] ? "active" : '' }}" >
|
||||
@include('menu::partials.projectLink')
|
||||
<div class="clear"></div>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@@ -0,0 +1,5 @@
|
||||
<li class="fixedMenuPoint {{ $module == $settingsLink['module'] && $action == $settingsLink['action'] ? 'active' : '' }}">
|
||||
<a href="@if(isset($settingsLink['url'])) {{ $settingsLink['url'] }} @else {{ BASE_URL }}/{{ $settingsLink['module'] }}/{{ $settingsLink['action'] }} @endif">
|
||||
{!! __($settingsLink['label']) !!}
|
||||
</a>
|
||||
</li>
|
||||
@@ -0,0 +1,5 @@
|
||||
<li class="title">
|
||||
<a href="javascript:void(0);">
|
||||
<strong>{!! __($menuItem['title']) !!}</strong>
|
||||
</a>
|
||||
</li>
|
||||
25
app/Domain/Menu/Templates/partials/leftnav/item.blade.php
Normal file
25
app/Domain/Menu/Templates/partials/leftnav/item.blade.php
Normal file
@@ -0,0 +1,25 @@
|
||||
@if(!isset($menuItem['role']) || $login::userIsAtLeast($menuItem['role'] ?? 'editor'))
|
||||
|
||||
<li
|
||||
@if(
|
||||
$module == $menuItem['module']
|
||||
&& (!isset($menuItem['active']) || in_array($action, $menuItem['active']))
|
||||
)
|
||||
class='active'
|
||||
@endif
|
||||
>
|
||||
<a href="{{ BASE_URL . $menuItem['href'] }}"
|
||||
data-tippy-content="{{ strip_tags(__($menuItem['tooltip'])) }}"
|
||||
data-tippy-placement="right"
|
||||
preload="mouseover"
|
||||
@if(isset($menuItem['attributes']))
|
||||
@foreach($menuItem['attributes'] as $key => $value)
|
||||
{{ $key }}="{{ $value }}"
|
||||
@endforeach
|
||||
@endif
|
||||
>
|
||||
{!! $menuItem['title'] !!}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@endif
|
||||
@@ -0,0 +1 @@
|
||||
<li class="separator"></li>
|
||||
27
app/Domain/Menu/Templates/partials/leftnav/submenu.blade.php
Normal file
27
app/Domain/Menu/Templates/partials/leftnav/submenu.blade.php
Normal file
@@ -0,0 +1,27 @@
|
||||
@if(!isset($menuItem['role']) || $login::userIsAtLeast($menuItem['role'] ?? 'editor'))
|
||||
|
||||
<li class="submenuToggle">
|
||||
<a href="javascript:void(0);"
|
||||
@if ( $menuItem['visual'] !== 'always' )
|
||||
onclick="leantime.menuController.toggleSubmenu('{{ $menuItem['id'] }}')"
|
||||
@endif
|
||||
>
|
||||
<i class="submenuCaret fa fa-angle-{{ $menuItem['visual'] == 'closed' ? 'right' : 'down' }}"
|
||||
id="submenu-icon-{{ $menuItem['id'] }}"></i>
|
||||
<strong>{!! __($menuItem['title']) !!}</strong>
|
||||
</a>
|
||||
</li>
|
||||
<ul id="submenu-{{ $menuItem['id'] }}" class="submenu {{ $menuItem['visual'] == 'closed' ? 'closed' : 'open' }}">
|
||||
@foreach ($menuItem['submenu'] as $subkey => $submenuItem)
|
||||
@switch ($submenuItem['type'])
|
||||
@case('header')
|
||||
@include("menu::partials.leftnav.header", ["menuItem" => $submenuItem, "module" => $module, "action" => $action])
|
||||
@break
|
||||
@case('item')
|
||||
@include("menu::partials.leftnav.item", ["menuItem" => $submenuItem, "module" => $module, "action" => $action])
|
||||
@endswitch
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@endif
|
||||
|
||||
19
app/Domain/Menu/Templates/partials/noGroup.blade.php
Normal file
19
app/Domain/Menu/Templates/partials/noGroup.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<ul class="level-0 noGroup">
|
||||
@foreach($projects as $project)
|
||||
|
||||
@if(
|
||||
!session()->exists("usersettings.projectSelectFilter.client")
|
||||
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|
||||
|| session("usersettings.projectSelectFilter.client") == 0
|
||||
|| session("usersettings.projectSelectFilter.client") == ""
|
||||
)
|
||||
|
||||
<li class="projectLineItem hasSubtitle {{ session("currentProject") ?? 0 == $project['id'] ? "active" : '' }}" >
|
||||
@include('menu::partials.projectLink')
|
||||
<div class="clear"></div>
|
||||
</li>
|
||||
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
</ul>
|
||||
46
app/Domain/Menu/Templates/partials/projectGroup.blade.php
Normal file
46
app/Domain/Menu/Templates/partials/projectGroup.blade.php
Normal file
@@ -0,0 +1,46 @@
|
||||
@php
|
||||
$groupState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$parent, 'closed');
|
||||
@endphp
|
||||
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $parent }}" class="level-{{ $level }} projectGroup {{ $groupState }}">
|
||||
@foreach($projects as $project)
|
||||
|
||||
@if(
|
||||
!session()->exists("usersettings.projectSelectFilter.client")
|
||||
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|
||||
|| session("usersettings.projectSelectFilter.client") == 0
|
||||
|| session("usersettings.projectSelectFilter.client") == ""
|
||||
|| $project["clientId"] == ''
|
||||
)
|
||||
|
||||
<li class="projectLineItem hasSubtitle {{ session("currentProject") == $project['id'] ? "active" : '' }}" >
|
||||
@php
|
||||
$parentState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$project['id'], 'closed');
|
||||
@endphp
|
||||
|
||||
@if((empty($project['children']) || count($project['children']) ==0))
|
||||
<span class="toggler"></span>
|
||||
@endif
|
||||
|
||||
@if(!empty($project['children']) && count($project['children']) >0)
|
||||
<a href="javascript:void(0);" class="toggler {{ $parentState }}" id="{{ $prefix }}-toggler-{{ $project["id"] }}" onclick="leantime.menuController.toggleProjectDropDownList('{{ $project["id"] }}', '', '{{ $prefix }}')">
|
||||
@if($parentState == 'closed')
|
||||
<i class="fa fa-angle-right"></i>
|
||||
@else
|
||||
<i class="fa fa-angle-down"></i>
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
@include('menu::partials.projectLink')
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
{{-- Depth cap: the hierarchy is cycle-guarded server-side, but a runaway
|
||||
tree (e.g. mutated by a plugin filter) must never recurse unbounded --}}
|
||||
@if(!empty($project['children']) && count($project['children']) >0 && $level < 20)
|
||||
@include('menu::partials.projectGroup', ['projects' => $project['children'], 'parent' => $project['id'], 'level'=> $level+1, 'prefix' => $prefix, "currentProject"=>$currentProject])
|
||||
@endif
|
||||
</li>
|
||||
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
21
app/Domain/Menu/Templates/partials/projectLink.blade.php
Normal file
21
app/Domain/Menu/Templates/partials/projectLink.blade.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<a href='{{ BASE_URL }}/projects/changeCurrentProject/{{ $project["id"] }}'
|
||||
@if(strlen($project["name"]) > 25)
|
||||
data-tippy-content='{{ $project["name"] }}'
|
||||
@endif >
|
||||
<span class='projectAvatar'>
|
||||
@if(isset($projectTypeAvatars[$project["type"]]) && $projectTypeAvatars[$project["type"]] != "avatar")
|
||||
<span class="{{ $projectTypeAvatars[$project["type"]] }}"></span>
|
||||
@else
|
||||
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $project["id"] }}&v={{ format($project['modified'])->timestamp() }}' />
|
||||
@endif
|
||||
</span>
|
||||
<span class='projectName'>
|
||||
@if($project["clientName"] != '')
|
||||
<small>{{ $project["clientName"] }}</small><br />
|
||||
@else
|
||||
<small>{{ __('projectType.'.$project["type"] ?? 'project') }}</small><br />
|
||||
@endif
|
||||
|
||||
{{ $project["name"] }}
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="projectListFilter">
|
||||
|
||||
<form
|
||||
hx-target="#mainProjectSelector"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="change">
|
||||
<i class="fas fa-filter"></i>
|
||||
<select data-placeholder="" title=""
|
||||
hx-post="{{ BASE_URL }}/hx/menu/projectSelector/update-menu"
|
||||
hx-target="#mainProjectSelector"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator=".htmx-indicator, .htmx-loaded-content"
|
||||
name="client">
|
||||
<option value="" data-placeholder="true">All Clients</option>
|
||||
@foreach ($clients as $client)
|
||||
@if($client['id'] > 0)
|
||||
<option value='{{ $client['id'] }}'
|
||||
@if (isset($projectSelectFilter['client']) && $projectSelectFilter['client'] == $client['id'])
|
||||
selected='selected'
|
||||
@endif
|
||||
>{{ $client['name'] }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<i class="fa-solid fa-diagram-project"></i>
|
||||
<select data-placeholder="" name="groupBy"
|
||||
hx-post="{{ BASE_URL }}/hx/menu/projectSelector/update-menu"
|
||||
hx-target="#mainProjectSelector"
|
||||
hx-indicator=".htmx-indicator, .htmx-loaded-content"
|
||||
hx-swap="outerHTML">
|
||||
@foreach ($projectSelectGroupOptions as $key => $group)
|
||||
<option value='{{ $key }}'
|
||||
|
||||
{{ $projectSelectFilter["groupBy"] == $key ? " selected='selected' " : "" }}
|
||||
|
||||
>{{ $group }}</option>
|
||||
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="hidden" name="activeTab" value="" />
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="htmx-indicator tw-ml-m tw-mr-m tw-pt-l">
|
||||
<x-global::loadingText type="project" count="5" includeHeadline="false"/>
|
||||
</div>
|
||||
|
||||
126
app/Domain/Menu/Templates/partials/projectSelector.blade.php
Normal file
126
app/Domain/Menu/Templates/partials/projectSelector.blade.php
Normal file
@@ -0,0 +1,126 @@
|
||||
@props([
|
||||
'redirect' => 'dashboard/show',
|
||||
'currentProject'
|
||||
])
|
||||
|
||||
<div class="dropdown-menu projectselector" id="mainProjectSelector">
|
||||
|
||||
@if ($menuType == 'project' || $menuType == 'default')
|
||||
<div class="head">
|
||||
<span class="sub">{{ __("menu.current_project") }}</span><br />
|
||||
<span class="title">{{ session("currentProjectName") }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="projectSelectorFooter" style="border:none; border-bottom:1px solid var(--main-border-color)">
|
||||
<ul class="selectorList projectList">
|
||||
<li>
|
||||
<a href="{{ BASE_URL }}/projects/showMy"><strong><i class="fa-solid fa-house-flag"></i> Open Project Hub</strong></a>
|
||||
</li>
|
||||
|
||||
@if ($login::userIsAtLeast("manager"))
|
||||
@dispatchEvent('beforeProjectCreateLink')
|
||||
<li><a href="{{ $startSomethingUrl }}">
|
||||
<span class="fancyLink">
|
||||
{!! __('menu.create_something_new') !!}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@dispatchEvent('afterProjectCreateLink')
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="tabbedwidget tab-primary projectSelectorTabs">
|
||||
<ul class="tabs">
|
||||
<li><a href="#myProjects">{{ __('menu.projectselector.my_projects') }}</a></li>
|
||||
<li><a href="#favorites">{{ __('menu.projectselector.favorites') }}</a></li>
|
||||
<li><a href="#recentProjects">{{ __('menu.projectselector.recent') }}</a></li>
|
||||
<li><a href="#allProjects">{{ __('menu.projectselector.all_projects') }}</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="myProjects" class="scrollingTab">
|
||||
@include('menu::partials.projectListFilter', ['clients' => $clients, 'projectSelectFilter' => $projectSelectFilter])
|
||||
<ul class="selectorList projectList htmx-loaded-content">
|
||||
@if($projectSelectFilter["groupBy"] == "client")
|
||||
@include('menu::partials.clientGroup', ['projects' => $allAssignedProjects, 'parent' => 0, 'level'=> 0, "prefix" => "myClientProjects", "currentProject"=>$currentProject])
|
||||
@elseif($projectSelectFilter["groupBy"] == "structure")
|
||||
@include('menu::partials.projectGroup', ['projects' => $projectHierarchy, 'parent' => 0, 'level'=> 0, "prefix" => "myProjects", "currentProject"=>$currentProject])
|
||||
@else
|
||||
@include('menu::partials.noGroup', ['projects' => $allAssignedProjects, "currentProject"=>$currentProject])
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
<div id="allProjects" class="scrollingTab">
|
||||
@include('menu::partials.projectListFilter', ['clients' => $clients, 'projectSelectFilter' => $projectSelectFilter])
|
||||
<ul class="selectorList projectList htmx-loaded-content">
|
||||
@if($projectSelectFilter["groupBy"] == "client")
|
||||
@include('menu::partials.clientGroup', ['projects' => $allAvailableProjects, 'parent' => 0, 'level'=> 0, "prefix" => "allClientProjects", "currentProject"=>$currentProject])
|
||||
@elseif($projectSelectFilter["groupBy"] == "structure")
|
||||
@include('menu::partials.projectGroup', ['projects' => $allAvailableProjectsHierarchy, 'parent' => 0, 'level'=> 0, "prefix" => "allProjects", "currentProject"=>$currentProject])
|
||||
@else
|
||||
@include('menu::partials.noGroup', ['projects' => $allAvailableProjects, "currentProject"=>$currentProject])
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
<div id="recentProjects" class="scrollingTab">
|
||||
<ul class="selectorList projectList">
|
||||
@if(count($recentProjects) >= 1)
|
||||
@include('menu::partials.noGroup', ['projects' => $recentProjects])
|
||||
@else
|
||||
<li class='nav-header'></li>
|
||||
<li><span class='info'>
|
||||
{{ __("menu.you_dont_have_projects") }}
|
||||
</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
<div id="favorites" class="scrollingTab">
|
||||
<ul class="selectorList projectList">
|
||||
@if(count($favoriteProjects) >= 1)
|
||||
@include('menu::partials.noGroup', ['projects' => $favoriteProjects])
|
||||
@else
|
||||
<li><span class='info'>
|
||||
{{ __("text.you_have_not_favorited_any_projects") }}
|
||||
</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($menuType == 'project' || $menuType == 'default')
|
||||
<div class="projectSelectorFooter">
|
||||
<ul class="selectorList projectList">
|
||||
|
||||
@if ($login::userIsAtLeast("manager"))
|
||||
@dispatchEvent('beforeProjectCreateLink')
|
||||
<li><a href="{{ $startSomethingUrl }}">
|
||||
<span class="fancyLink">
|
||||
{!! __('menu.create_something_new') !!}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@dispatchEvent('afterProjectCreateLink')
|
||||
@endif
|
||||
|
||||
|
||||
<li>
|
||||
<a href="{{ BASE_URL }}/projects/showMy"><i class="fa-solid fa-circle-nodes"></i> Project Hub</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function () {
|
||||
leantime.menuController.initProjectSelector();
|
||||
});
|
||||
</script>
|
||||
21
app/Domain/Menu/Templates/projectSelector.blade.php
Normal file
21
app/Domain/Menu/Templates/projectSelector.blade.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<a href="{{ BASE_URL }}/projects/showMy"
|
||||
class="dropdown-toggle bigProjectSelector {{ $menuType == "project" ? "active" : "" }}"
|
||||
data-toggle="dropdown">
|
||||
|
||||
@if ($menuType == 'project' || $menuType == 'default')
|
||||
<span class="projectAvatar {{ $currentProjectType }}">
|
||||
@if(isset($projectTypeAvatars[$currentProjectType]) && $projectTypeAvatars[$currentProjectType] != "avatar")
|
||||
<span class="{{ $projectTypeAvatars[$currentProjectType] }}"></span>
|
||||
@else
|
||||
<img src="{{ BASE_URL }}/api/projects?projectAvatar={{ $currentProject['id'] ?? -1 }}&v={{ format($currentProject['modified'] ?? '')->timestamp() }}"/>
|
||||
@endif
|
||||
</span>
|
||||
{{ $currentProject['name'] ?? "" }}
|
||||
|
||||
@else
|
||||
{!! __('menu.projects') !!}
|
||||
@endif
|
||||
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
@include('menu::partials.projectSelector', [])
|
||||
Reference in New Issue
Block a user