OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View 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,
];
}
}

View 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,
];
}
}

View 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,
];
}
}

View 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 {}
}

View 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
};
})();

View 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
};
})();

View 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;
}
}

View 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,
];
}
}

View 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

View 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

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
<li class="title">
<a href="javascript:void(0);">
<strong>{!! __($menuItem['title']) !!}</strong>
</a>
</li>

View 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

View File

@@ -0,0 +1 @@
<li class="separator"></li>

View 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

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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'] ?? "" }}&nbsp;
@else
{!! __('menu.projects') !!}
@endif
<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
@include('menu::partials.projectSelector', [])