Implement user action menu

Signed-off-by: Dillon-Brown <dillon.brown@salesagility.com>
This commit is contained in:
Dillon-Brown 2020-04-24 04:16:13 +01:00
parent f488ae2c8c
commit aa242f970c
15 changed files with 190 additions and 76 deletions

View file

@ -53,3 +53,4 @@ dunglas_angular_csrf:
- { path: ^/(_(profiler|wdt))/ }
- { path: ^/api }
- { path: ^/session-status$ }
- { path: ^/current-user$ }

View file

@ -1,4 +1,5 @@
export interface CurrentUserModel {
id: string;
name: string;
firstName: string;
lastName: string;
}

View file

@ -2,7 +2,7 @@ import {ActionLinkModel} from './action-link-model';
import {CurrentUserModel} from './current-user-model';
import {AllMenuModel} from './all-menu-model';
import {LogoModel} from '../logo/logo-model';
import {GroupedTab, NavbarModuleMap} from '@base/facades/navigation/navigation.facade';
import {GroupedTab, NavbarModuleMap, UserActionMenu} from '@base/facades/navigation/navigation.facade';
import {LanguageListStringMap, LanguageStringMap} from '@base/facades/language/language.facade';
import {MenuItem} from '@components/navbar/navbar.abstract';
import {UserPreferenceMap} from '@base/facades/user-preference/user-preference.facade';
@ -26,7 +26,9 @@ export interface NavbarModel {
appListStrings: LanguageListStringMap,
menuItemThreshold: number,
groupedTabs: GroupedTab[],
userPreferences: UserPreferenceMap
userPreferences: UserPreferenceMap,
userActionMenu: UserActionMenu[],
currentUser: CurrentUserModel
): void;
buildGroupTabMenu(
@ -46,4 +48,10 @@ export interface NavbarModel {
modStrings: LanguageListStringMap,
appListStrings: LanguageListStringMap,
menuItemThreshold: number): void;
buildUserActionMenu(
appStrings: LanguageStringMap,
userActionMenu: UserActionMenu[],
currentUser: CurrentUserModel
): void;
}

View file

@ -1,6 +1,6 @@
import {NavbarModel} from './navbar-model';
import {LogoAbstract} from '../logo/logo-abstract';
import {GroupedTab, NavbarModuleMap} from '@base/facades/navigation/navigation.facade';
import {GroupedTab, NavbarModuleMap, UserActionMenu} from '@base/facades/navigation/navigation.facade';
import {LanguageListStringMap, LanguageStringMap} from '@base/facades/language/language.facade';
import {CurrentUserModel} from './current-user-model';
@ -31,35 +31,11 @@ export class NavbarAbstract implements NavbarModel {
authenticated = true;
logo = new LogoAbstract();
useGroupTabs = false;
globalActions: ActionLinkModel[] = [
{
link: {
url: '',
label: 'Employees'
}
},
{
link: {
url: '',
label: 'Admin'
}
},
{
link: {
url: '',
label: 'Support Forums'
}
},
{
link: {
url: '',
label: 'About'
}
}
];
globalActions: ActionLinkModel[] = [];
currentUser: CurrentUserModel = {
id: '1',
name: 'Will Rennie',
id: '',
firstName: '',
lastName: '',
};
all = {
modules: [],
@ -72,10 +48,48 @@ export class NavbarAbstract implements NavbarModel {
*/
public resetMenu(): void {
this.menu = [];
this.globalActions = [];
this.all.modules = [];
this.all.extra = [];
}
public buildUserActionMenu(
appStrings: LanguageStringMap,
userActionMenu: UserActionMenu[],
currentUser: CurrentUserModel
): void {
this.currentUser.id = currentUser.id;
this.currentUser.firstName = currentUser.firstName;
this.currentUser.lastName = currentUser.lastName;
if (userActionMenu) {
userActionMenu.forEach((subMenu) => {
let name = subMenu.name;
let url = subMenu.url;
let urlParams;
if (name == 'logout') {
return;
}
if (name !== 'training') {
urlParams = this.getModuleFromUrlParams(url);
url = ROUTE_PREFIX + '/' + (urlParams.module).toLowerCase() + '/' + (urlParams.action).toLowerCase();
}
let label = appStrings[subMenu.labelKey];
this.globalActions.push({
link: {
url: url,
label: label,
},
});
});
}
return;
}
/**
* Build navbar
* @param tabs
@ -86,6 +100,8 @@ export class NavbarAbstract implements NavbarModel {
* @param menuItemThreshold
* @param groupedTabs
* @param userPreferences
* @param userActionMenu
* @param currentUser
*/
public build(
tabs: string[],
@ -95,7 +111,9 @@ export class NavbarAbstract implements NavbarModel {
appListStrings: LanguageListStringMap,
menuItemThreshold: number,
groupedTabs: GroupedTab[],
userPreferences: UserPreferenceMap
userPreferences: UserPreferenceMap,
userActionMenu: UserActionMenu[],
currentUser: CurrentUserModel,
): void {
this.resetMenu();
@ -104,6 +122,8 @@ export class NavbarAbstract implements NavbarModel {
return;
}
this.buildUserActionMenu(appStrings, userActionMenu, currentUser);
const navigationParadigm = userPreferences.navigation_paradigm;
if (navigationParadigm.toString() === 'm') {
@ -238,7 +258,7 @@ export class NavbarAbstract implements NavbarModel {
moduleUrl = null;
}
const menuItem = {
return {
link: {
label: (appStrings && appStrings[moduleLabel]) || moduleLabel,
url: moduleUrl,
@ -248,8 +268,6 @@ export class NavbarAbstract implements NavbarModel {
icon: '',
submenu: this.buildGroupedMenu(groupedModules, modules, appStrings, modStrings, appListStrings)
};
return menuItem;
}
/**
@ -355,4 +373,17 @@ export class NavbarAbstract implements NavbarModel {
return menuItem;
}
/**
* @param search
*/
private getModuleFromUrlParams(search) {
const hashes = search.slice(search.indexOf('?') + 1).split('&')
const params = {}
hashes.map(hash => {
const [key, val] = hash.split('=')
params[key] = decodeURIComponent(val)
})
return params
}
}

View file

@ -90,12 +90,10 @@
<li class="global-link-item">
<a class="nav-link primary-global-link dropdown-toggle" ngbDropdownToggle>
<scrm-image class="global-action-icon sicon-2x" image="user"></scrm-image>
{{ navbar.currentUser.name }}
{{ navbar.currentUser.firstName }} {{navbar.currentUser.lastName}}
</a>
<div aria-labelledby="navbarDropdownMenuLink"
class="dropdown-menu global-links-dropdown" ngbDropdownMenu>
<a class="dropdown-item global-links-sublink" ngbDropdownItem
href="#/Users/EditView/{{ navbar.currentUser.id }}">Profile</a>
<ng-template [ngIf]="navbar.globalActions">
<a class="dropdown-item global-links-sublink" ngbDropdownItem
*ngFor="let globalAction of navbar.globalActions"
@ -258,12 +256,10 @@
<li class="global-link-item">
<a class="nav-link primary-global-link dropdown-toggle" ngbDropdownToggle>
<scrm-image class="global-action-icon sicon-2x" image="user"></scrm-image>
{{ navbar.currentUser.name }}
{{ navbar.currentUser.firstName }} {{navbar.currentUser.lastName}}
</a>
<div aria-labelledby="navbarDropdownMenuLink"
class="dropdown-menu global-links-dropdown dropdown-menu-right" ngbDropdownMenu>
<a class="dropdown-item global-links-sublink" ngbDropdownItem
href="#/Users/EditView/{{ navbar.currentUser.id }}">Profile</a>
<ng-template [ngIf]="navbar.globalActions">
<a class="dropdown-item global-links-sublink" ngbDropdownItem
*ngFor="let globalAction of navbar.globalActions"

View file

@ -1,5 +1,5 @@
import {Component, HostListener, OnInit} from '@angular/core';
import {ApiService} from '../../services/api/api.service';
import {ApiService} from '@services/api/api.service';
import {NavbarModel} from './navbar-model';
import {NavbarAbstract} from './navbar.abstract';
import {combineLatest, Observable} from 'rxjs';
@ -43,6 +43,8 @@ export class NavbarUiComponent implements OnInit {
appListStrings$: Observable<LanguageListStringMap> = this.languageFacade.appListStrings$;
userPreferences$: Observable<UserPreferenceMap> = this.userPreferenceFacade.userPreferences$;
groupedTabs$: Observable<any> = this.navigationFacade.groupedTabs$;
userActionMenu$: Observable<any> = this.navigationFacade.userActionMenu$;
currentUser$: Observable<any> = this.authService.currentUser$;
vm$ = combineLatest([
this.tabs$,
@ -51,9 +53,11 @@ export class NavbarUiComponent implements OnInit {
this.appListStrings$,
this.modStrings$,
this.userPreferences$,
this.groupedTabs$
this.groupedTabs$,
this.userActionMenu$,
this.currentUser$
]).pipe(
map(([tabs, modules, appStrings, appListStrings, modStrings, userPreferences, groupedTabs]) => {
map(([tabs, modules, appStrings, appListStrings, modStrings, userPreferences, groupedTabs, userActionMenu, currentUser]) => {
this.navbar.build(
tabs,
@ -63,7 +67,9 @@ export class NavbarUiComponent implements OnInit {
appListStrings,
this.menuItemThreshold,
groupedTabs,
userPreferences
userPreferences,
userActionMenu,
currentUser
)
return {
@ -109,11 +115,7 @@ export class NavbarUiComponent implements OnInit {
@HostListener('window:resize', ['$event'])
onResize(event: any) {
const innerWidth = event.target.innerWidth;
if (innerWidth <= 768) {
this.mobileNavbar = true;
} else {
this.mobileNavbar = false;
}
this.mobileNavbar = innerWidth <= 768;
}
ngOnInit(): void {

View file

@ -35,6 +35,7 @@ export class AuthGuard implements CanActivate {
take(1),
map((user: any) => {
if (user && user.active === true) {
this.authService.setCurrentUser(user);
return true;
}
// Re-direct to login

View file

@ -1,8 +1,8 @@
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http';
import {BehaviorSubject, Observable, throwError} from 'rxjs';
import {catchError, finalize, take} from 'rxjs/operators';
import {BehaviorSubject, throwError} from 'rxjs';
import {catchError, distinctUntilChanged, finalize, take} from 'rxjs/operators';
import {LoginUiComponent} from '@components/login/login.component';
import {User} from '@services/user/user';
import {MessageService} from '@services/message/message.service';
@ -15,10 +15,10 @@ import {SystemConfigFacade} from "@base/facades/system-config/system-config.faca
providedIn: 'root'
})
export class AuthService {
public currentUser$: Observable<User>;
private currentUserSubject: BehaviorSubject<User>;
defaultTimeout: string = '3600';
private currentUserSubject = new BehaviorSubject<User>({} as User);
public currentUser$ = this.currentUserSubject.asObservable().pipe(distinctUntilChanged());
public isUserLoggedIn: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
defaultTimeout: string = '3600';
constructor(
private http: HttpClient,
@ -29,8 +29,14 @@ export class AuthService {
private bnIdle: BnNgIdleService,
protected systemConfigFacade: SystemConfigFacade
) {
this.currentUserSubject = new BehaviorSubject<User>(null);
this.currentUser$ = this.currentUserSubject.asObservable();
}
getCurrentUser(): User {
return this.currentUserSubject.value;
}
setCurrentUser(data) {
this.currentUserSubject.next(data);
}
doLogin(
@ -56,6 +62,7 @@ export class AuthService {
).subscribe((response: any) => {
onSuccess(caller, response);
this.isUserLoggedIn.next(true);
this.setCurrentUser(response);
let duration = response.duration;
@ -76,7 +83,7 @@ export class AuthService {
})
}, (error: HttpErrorResponse) => {
onError(caller, error);
});
})
}
/**

View file

@ -1,8 +1,5 @@
export class User {
id: string;
username: string;
password: string;
salutation?: string;
status: string;
isAdmin: boolean;
firstName: string;
lastName: string;
}

View file

@ -164,14 +164,16 @@ class NavbarHandler extends LegacyHandler implements NavigationProviderInterface
*/
protected function fetchUserActionMenu(): array
{
global $current_user;
$actions['LBL_PROFILE'] = [
'name' => 'profile',
'labelKey' => 'LBL_PROFILE',
'url' => 'index.php?module=Users&action=EditView&record=1',
'url' => 'index.php?module=Users&action=EditView&record=' . $current_user->id,
'icon' => '',
];
//order matters
// Order matters
$actionLabelMap = [
'LBL_PROFILE' => 'profile',
'LBL_EMPLOYEES' => 'employees',
@ -209,7 +211,9 @@ class NavbarHandler extends LegacyHandler implements NavigationProviderInterface
$userActionMenu = [];
foreach ($actionKeys as $key) {
$userActionMenu[] = $actions[$key];
if (isset($actions[$key])) {
$userActionMenu[] = $actions[$key];
}
}
return array_values($userActionMenu);

View file

@ -8,6 +8,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
/**
@ -43,10 +44,24 @@ class SecurityController extends AbstractController
/**
* @Route("/session-status", name="app_session_status", methods={"GET"})
* @throws Exception
* @param Security $security
* @return JsonResponse
*/
public function sessionStatus(): JsonResponse
public function sessionStatus(Security $security): JsonResponse
{
return new JsonResponse(['active' => true], Response::HTTP_OK);
$user = $security->getUser();
$id = $user->getId();
$firstName = $user->getFirstName();
$lastName = $user->getLastName();
$data =
[
'active' => true,
'id' => $id,
'firstName' => $firstName,
'lastName' => $lastName
];
return new JsonResponse($data, Response::HTTP_OK);
}
}

View file

@ -6,6 +6,7 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Service\NavigationProviderInterface;
use App\Entity\Navbar;
use Symfony\Component\Security\Core\Security;
final class NavbarItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
@ -14,13 +15,20 @@ final class NavbarItemDataProvider implements ItemDataProviderInterface, Restric
*/
private $navigationService;
/**
* @var Security
*/
private $security;
/**
* NavbarItemDataProvider constructor.
* @param NavigationProviderInterface $navigationService
* @param Security $security
*/
public function __construct(NavigationProviderInterface $navigationService)
public function __construct(NavigationProviderInterface $navigationService, Security $security)
{
$this->navigationService = $navigationService;
$this->security = $security;
}
/**
@ -46,8 +54,8 @@ final class NavbarItemDataProvider implements ItemDataProviderInterface, Restric
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?Navbar
{
$navbar = $this->navigationService->getNavbar();
// This should be updated once we have authentication.
$navbar->userID = 1;
$user = $this->security->getUser();
$navbar->userID = $user->getId();
return $navbar;
}

View file

@ -2,16 +2,30 @@
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ApiResource(
* attributes={"security"="is_granted('ROLE_USER')"},
* itemOperations={
* "get"
* },
* collectionOperations={
* },
* graphql={
* "item_query",
* },
* )
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Table(name="users")
*/
class User implements UserInterface
{
/**
* @ApiProperty(identifier=true)
* @ORM\Id()
* @ORM\Column(type="string")
*/
@ -52,12 +66,14 @@ class User implements UserInterface
private $sugar_login;
/**
* @ApiProperty
* @var string
* @ORM\Column(type="string")
*/
private $first_name;
/**
* @ApiProperty
* @var string
* @ORM\Column(type="string")
*/
@ -261,7 +277,10 @@ class User implements UserInterface
return $this->is_admin;
}
public function getId(): ?int
/**
* @return string|int
*/
public function getId(): ?string
{
return $this->id;
}
@ -308,6 +327,22 @@ class User implements UserInterface
return $this;
}
/**
* @return string
*/
public function getFirstName(): string
{
return $this->first_name;
}
/**
* @return string
*/
public function getLastName(): string
{
return $this->last_name;
}
/**
* @see UserInterface
*/

View file

@ -176,9 +176,17 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements P
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$user = $token->getUser();
$id = $user->getId();
$firstName = $user->getFirstName();
$lastName = $user->getLastName();
$data = [
'status' => 'success',
'duration' => $this->session->getMetadataBag()->getLifetime()
'duration' => $this->session->getMetadataBag()->getLifetime(),
'id' => $id,
'firstName' => $firstName,
'lastName' => $lastName
];
return new JsonResponse($data, Response::HTTP_OK);

View file

@ -213,7 +213,7 @@ final class NavbarTest extends Unit
[
'name' => 'profile',
'labelKey' => 'LBL_PROFILE',
'url' => 'index.php?module=Users&action=EditView&record=1',
'url' => 'index.php?module=Users&action=EditView&record=',
'icon' => '',
],
[