mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-09-04 10:14:13 +08:00
Add Navbar translations
- Make language facade loading dynamic to load given types of strings -- Add cache per type of language strings -- Add configuration per type of language strings -- Enable fetching modStrings, appStrings, appListStrings - Expose a stream for each type of strings - Make BaseMetadataResolver language loading dynamic based on router configuration -- Add navigation loading -- add string loading, depending on configuration -- Add default configuration: Loads SystemConfigs, Navigation and all Languages -- Add ability to override default configuration on router - Configure BaseMetadata resolver in router - Only use default language if it there is no active language - Update login component to cope with changes to language facade - Refactor: move existing facades to the facades folder - Re-write navigation facade to use reactive facades approach - Update navbar api to cope with with api contract -- Add modules entry -- Move moduleSubmenus to modules entry -- Disable collection query on graphql -- Update frontend to cope with these changes - Update navbar component -- Cleanup component -- Fetch navigation and languages reactively -- Update navbar menu item creation with new navigation structure -- Fix html for loading icons -- Fix html for regular module tab menu - Translate navbar items -- Add translations on navbar menu items creation - Add karma tests - Adjust navbar to properly render links -- Add extra query params to submenu in API -- Adjust LegacyHandler to retrieve the extra query params -- Adjust angular component
This commit is contained in:
parent
62b05e18fe
commit
a8235c444a
40 changed files with 4213 additions and 947 deletions
|
@ -25,6 +25,7 @@ services:
|
|||
$exposedSystemConfigs: '%legacy.exposed_system_configs%'
|
||||
$legacyModuleNameMap: '%legacy.module_name_map%'
|
||||
$legacyActionNameMap: '%legacy.action_name_map%'
|
||||
$menuItemMap: '%legacy.menu_item_map%'
|
||||
$legacyAssetPaths: '%legacy.asset_paths%'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
|
|
143
config/services/legacy/menu_item_map.yaml
Normal file
143
config/services/legacy/menu_item_map.yaml
Normal file
|
@ -0,0 +1,143 @@
|
|||
parameters:
|
||||
legacy.menu_item_map:
|
||||
default:
|
||||
List:
|
||||
icon: view
|
||||
View:
|
||||
icon: view
|
||||
Add:
|
||||
icon: plus
|
||||
Create:
|
||||
icon: plus
|
||||
Import:
|
||||
icon: download
|
||||
Security_Groups:
|
||||
icon: view
|
||||
Schedule_Call:
|
||||
icon: plus
|
||||
Schedule_Meetings:
|
||||
icon: plus
|
||||
Schedule_Meeting:
|
||||
icon: plus
|
||||
contacts:
|
||||
Create_Contact_Vcard:
|
||||
icon: plus
|
||||
calls:
|
||||
Calls:
|
||||
icon: plus
|
||||
calendar:
|
||||
Calendar:
|
||||
icon: plus
|
||||
Today:
|
||||
icon: calendar
|
||||
workflow:
|
||||
View_Process_Audit:
|
||||
icon: view
|
||||
workflow-processed:
|
||||
View_Process_Audit:
|
||||
icon: view
|
||||
mail-merge:
|
||||
Documents:
|
||||
icon: view
|
||||
email-templates:
|
||||
View_Email_Templates:
|
||||
icon: view
|
||||
View_Create_Email_Templates:
|
||||
icon: plus
|
||||
campaigns:
|
||||
View_Create_Email_Templates:
|
||||
icon: plus
|
||||
View_Email_Templates:
|
||||
icon: view
|
||||
Setup_Email:
|
||||
icon: email
|
||||
View_Diagnostics:
|
||||
icon: view
|
||||
Create_Person_Form:
|
||||
icon: person
|
||||
leads:
|
||||
Create_Lead_Vcard:
|
||||
icon: plus
|
||||
inbound-email:
|
||||
Setup_Email:
|
||||
icon: email
|
||||
schedulers:
|
||||
Schedulers:
|
||||
icon: view
|
||||
address-cache:
|
||||
Createjjwg_Address_Cache:
|
||||
icon: plus
|
||||
jjwg_Address_Cache:
|
||||
icon: view
|
||||
maps:
|
||||
List_Maps:
|
||||
icon: view
|
||||
Quick_Radius_Map:
|
||||
icon: view
|
||||
security-groups:
|
||||
Create_Security_Group:
|
||||
icon: plus
|
||||
Security_Groups:
|
||||
icon: view
|
||||
Security_Suite_Settings:
|
||||
icon: padlock
|
||||
Role_Management:
|
||||
icon: view
|
||||
acl-roles:
|
||||
Role_Management:
|
||||
icon: view
|
||||
connectors:
|
||||
icon_Connectors:
|
||||
icon: view
|
||||
icon_ConnectorConfig_16:
|
||||
icon: view
|
||||
icon_ConnectorEnable_16:
|
||||
icon: view
|
||||
icon_ConnectorMap_16:
|
||||
icon: view
|
||||
configurator:
|
||||
Administration:
|
||||
icon: view
|
||||
Leads:
|
||||
icon: view
|
||||
help:
|
||||
Administration:
|
||||
icon: view
|
||||
Accounts:
|
||||
icon: view
|
||||
Opportunities:
|
||||
icon: view
|
||||
Cases:
|
||||
icon: view
|
||||
Notes:
|
||||
icon: view
|
||||
Calls:
|
||||
icon: view
|
||||
Emails:
|
||||
icon: view
|
||||
Meetings:
|
||||
icon: view
|
||||
Tasks:
|
||||
icon: view
|
||||
project:
|
||||
Resource_Chart:
|
||||
icon: piechart
|
||||
View_Project_Tasks:
|
||||
icon: view
|
||||
project-task:
|
||||
View_Project_Tasks:
|
||||
icon: view
|
||||
roles:
|
||||
Role_Management:
|
||||
icon: view
|
||||
users:
|
||||
Create_Group_User:
|
||||
icon: plus
|
||||
Create_Security_Group:
|
||||
icon: plus
|
||||
Security_Groups:
|
||||
icon: view
|
||||
Role_Management:
|
||||
icon: view
|
||||
Security_Suite_Settings:
|
||||
icon: view
|
|
@ -1,7 +1,7 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
import {ClassicViewUiComponent} from '@components/classic-view/classic-view.component';
|
||||
import {ClassicViewResolver} from '@services/classic-view/classic-view.resolver';
|
||||
import {ClassicViewResolver} from "@services/classic-view/classic-view.resolver";
|
||||
import {BaseMetadataResolver} from '@services/metadata/base-metadata.resolver';
|
||||
import {AuthGuard} from '../services/auth/auth-guard.service';
|
||||
import {ListComponent} from '@views/list/list.component';
|
||||
|
@ -10,23 +10,37 @@ const routes: Routes = [
|
|||
{
|
||||
path: 'Listview',
|
||||
component: ListComponent,
|
||||
resolve: {view: BaseMetadataResolver}
|
||||
resolve: {
|
||||
metadata: BaseMetadataResolver
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'Login',
|
||||
loadChildren: () => import('../components/login/login.module').then(m => m.LoginUiModule),
|
||||
resolve: {view: BaseMetadataResolver}
|
||||
resolve: {metadata: BaseMetadataResolver},
|
||||
data: {
|
||||
load: {
|
||||
navigation: false,
|
||||
languageStrings: ['appStrings']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'Home',
|
||||
loadChildren: () => import('../components/home/home.module').then(m => m.HomeUiModule),
|
||||
resolve: {
|
||||
metadata: BaseMetadataResolver,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':module',
|
||||
component: ClassicViewUiComponent,
|
||||
canActivate: [AuthGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
resolve: {view: ClassicViewResolver},
|
||||
resolve: {
|
||||
metadata: BaseMetadataResolver,
|
||||
view: ClassicViewResolver
|
||||
},
|
||||
data: {
|
||||
reuseRoute: false
|
||||
}
|
||||
|
@ -36,7 +50,10 @@ const routes: Routes = [
|
|||
component: ClassicViewUiComponent,
|
||||
canActivate: [AuthGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
resolve: {view: ClassicViewResolver},
|
||||
resolve: {
|
||||
metadata: BaseMetadataResolver,
|
||||
view: ClassicViewResolver
|
||||
},
|
||||
data: {
|
||||
reuseRoute: false
|
||||
}
|
||||
|
@ -46,7 +63,10 @@ const routes: Routes = [
|
|||
component: ClassicViewUiComponent,
|
||||
canActivate: [AuthGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
resolve: {view: ClassicViewResolver},
|
||||
resolve: {
|
||||
metadata: BaseMetadataResolver,
|
||||
view: ClassicViewResolver
|
||||
},
|
||||
data: {
|
||||
reuseRoute: false
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Component, ViewChild, ViewContainerRef, OnInit} from '@angular/core';
|
||||
import {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router} from '@angular/router';
|
||||
import {AppState, AppStateFacade} from "@base/facades/app-state.facade";
|
||||
import {AppState, AppStateFacade} from "@base/facades/app-state/app-state.facade";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="form-group row" *ngIf="vm.showLanguages">
|
||||
<label class="col-sm-3 ml-2 col-form-label" for="languages">{{vm.appStrings['LBL_LANGUAGE']}}:</label>
|
||||
<div class="select col-sm-8 p-0 inner-addon left-addon">
|
||||
<select #languageSelect id="languages" (change)="languageFacade.updateLanguage(languageSelect.value)">
|
||||
<select #languageSelect id="languages" (change)="languageFacade.changeLanguage(languageSelect.value)">
|
||||
<option [value]="item.key"
|
||||
*ngFor="let item of vm.systemConfigs['languages'].items | keyvalue">{{item.value}}</option>
|
||||
</select>
|
||||
|
@ -67,10 +67,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- End of login form section -->
|
||||
<!-- End of login form section -->
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
|
|||
|
||||
import {LoginUiComponent} from './login.component';
|
||||
import {ApiService} from '../../services/api/api.service';
|
||||
import {LanguageFacade} from '@base/facades/language/language.facade';
|
||||
import {languageFacadeMock} from '@base/facades/language/language.facade.spec.mock';
|
||||
import {ApolloTestingModule} from 'apollo-angular/testing';
|
||||
import {systemConfigFacadeMock} from '@base/facades/system-config/system-config.facade.spec.mock';
|
||||
import {SystemConfigFacade} from '@base/facades/system-config/system-config.facade';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
|
@ -20,10 +24,13 @@ describe('LoginComponent', () => {
|
|||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ApolloTestingModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [LoginUiComponent]
|
||||
declarations: [LoginUiComponent],
|
||||
providers: [
|
||||
{provide: SystemConfigFacade, useValue: systemConfigFacadeMock},
|
||||
{provide: LanguageFacade, useValue: languageFacadeMock},
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
@ -34,7 +41,7 @@ describe('LoginComponent', () => {
|
|||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it(`should create`, async(inject([HttpTestingController],
|
||||
it('should create', async(inject([HttpTestingController],
|
||||
(router: RouterTestingModule, http: HttpTestingController, api: ApiService) => {
|
||||
expect(component).toBeTruthy();
|
||||
})));
|
||||
|
|
|
@ -6,10 +6,10 @@ import {LoginResponseModel} from '../../services/auth/login-response-model';
|
|||
import {MessageService} from '../../services/message/message.service';
|
||||
import {ApiService} from '../../services/api/api.service';
|
||||
|
||||
import {SystemConfigFacade, SystemConfigMap} from '@services/metadata/configs/system-config.facade';
|
||||
import {SystemConfigFacade, SystemConfigMap} from '@base/facades/system-config/system-config.facade';
|
||||
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {LanguageFacade, LanguageStringMap} from '@base/facades/language.facade';
|
||||
import {LanguageFacade, LanguageStringMap} from '@base/facades/language/language.facade';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {transition, trigger, useAnimation} from '@angular/animations';
|
||||
import {fadeIn} from 'ng-animate';
|
||||
|
@ -36,7 +36,7 @@ export class LoginUiComponent {
|
|||
cardState = 'front';
|
||||
|
||||
systemConfigs$: Observable<SystemConfigMap> = this.systemConfigFacade.configs$;
|
||||
appStrings$: Observable<LanguageStringMap> = this.languageFacade.languageStrings$;
|
||||
appStrings$: Observable<LanguageStringMap> = this.languageFacade.appStrings$;
|
||||
|
||||
vm$ = combineLatest([this.systemConfigs$, this.appStrings$]).pipe(
|
||||
map(([systemConfigs, appStrings]) => {
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
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 {ActionLinkModel} from './action-link-model';
|
||||
import {CurrentUserModel} from './current-user-model';
|
||||
import {AllMenuModel} from './all-menu-model';
|
||||
import {LogoModel} from '../logo/logo-model';
|
||||
import {NavbarModuleMap} from '@base/facades/navigation/navigation.facade';
|
||||
import {LanguageListStringMap, LanguageStringMap} from '@base/facades/language/language.facade';
|
||||
import {MenuItem} from '@components/navbar/navbar.abstract';
|
||||
|
||||
export interface NavbarModel {
|
||||
authenticated: boolean;
|
||||
logo: LogoModel;
|
||||
useGroupTabs: boolean;
|
||||
globalActions: Array<ActionLinkModel>;
|
||||
currentUser: CurrentUserModel;
|
||||
all: AllMenuModel;
|
||||
menu: any;
|
||||
buildMenu(items: any, menuItemThreshold: number): void;
|
||||
authenticated: boolean;
|
||||
logo: LogoModel;
|
||||
useGroupTabs: boolean;
|
||||
globalActions: ActionLinkModel[];
|
||||
currentUser: CurrentUserModel;
|
||||
all: AllMenuModel;
|
||||
menu: MenuItem[];
|
||||
|
||||
resetMenu(): void;
|
||||
|
||||
buildMenu(
|
||||
items: string[],
|
||||
modules: NavbarModuleMap,
|
||||
appStrings: LanguageStringMap,
|
||||
modStrings: LanguageListStringMap,
|
||||
appListStrings: LanguageListStringMap,
|
||||
menuItemThreshold: number): void;
|
||||
}
|
||||
|
|
|
@ -1,103 +1,171 @@
|
|||
import { NavbarModel } from './navbar-model';
|
||||
import { LogoAbstract } from '../logo/logo-abstract';
|
||||
import {NavbarModel} from './navbar-model';
|
||||
import {LogoAbstract} from '../logo/logo-abstract';
|
||||
import {NavbarModule, NavbarModuleMap} from "@base/facades/navigation/navigation.facade";
|
||||
import {LanguageListStringMap, LanguageStringMap} from '@base/facades/language/language.facade';
|
||||
import {CurrentUserModel} from './current-user-model';
|
||||
import {ActionLinkModel} from './action-link-model';
|
||||
|
||||
export interface RecentRecordsMenuItem {
|
||||
summary: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
link: {
|
||||
label: string;
|
||||
url: string;
|
||||
route?: string;
|
||||
params?: { [key: string]: string; }
|
||||
};
|
||||
icon: string;
|
||||
submenu: MenuItem[];
|
||||
recentRecords?: RecentRecordsMenuItem[];
|
||||
}
|
||||
|
||||
|
||||
const ROUTE_PREFIX = './#';
|
||||
|
||||
export class NavbarAbstract implements NavbarModel {
|
||||
authenticated = true;
|
||||
logo = new LogoAbstract();
|
||||
useGroupTabs = true;
|
||||
globalActions = [
|
||||
{
|
||||
link: {
|
||||
url: '',
|
||||
label: 'Employees'
|
||||
}
|
||||
},
|
||||
{
|
||||
link: {
|
||||
url: '',
|
||||
label: 'Admin'
|
||||
}
|
||||
},
|
||||
{
|
||||
link: {
|
||||
url: '',
|
||||
label: 'Support Forums'
|
||||
}
|
||||
},
|
||||
{
|
||||
link: {
|
||||
url: '',
|
||||
label: 'About'
|
||||
}
|
||||
}
|
||||
];
|
||||
currentUser = {
|
||||
id: '1',
|
||||
name: 'Will Rennie',
|
||||
};
|
||||
all = {
|
||||
modules: [],
|
||||
extra: [],
|
||||
};
|
||||
menu = [];
|
||||
|
||||
public buildMenu(items: {}, threshold: number): void {
|
||||
const navItems = [];
|
||||
const moreItems = [];
|
||||
|
||||
if (!items || Object.keys(items).length === 0) {
|
||||
this.menu = navItems;
|
||||
this.all.extra = moreItems;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
Object.keys(items).forEach(module => {
|
||||
|
||||
if (count <= threshold) {
|
||||
navItems.push(this.buildMenuItem(module, items[module]));
|
||||
} else {
|
||||
moreItems.push(this.buildMenuItem(module, items[module]));
|
||||
}
|
||||
|
||||
count++;
|
||||
});
|
||||
|
||||
this.menu = navItems;
|
||||
this.all.modules = moreItems;
|
||||
}
|
||||
|
||||
public buildMenuItem(module: string, label: string): any {
|
||||
|
||||
return {
|
||||
link: { label, url: `./#/${module}/index` }, icon: 'home_page',
|
||||
submenu:
|
||||
[
|
||||
{
|
||||
link: {
|
||||
label: `Create ${label}`,
|
||||
url: `./#/${module}/edit`
|
||||
},
|
||||
icon: 'plus',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
link: {
|
||||
label: `View ${label}`,
|
||||
url: `./#/${module}/list`
|
||||
},
|
||||
icon: 'view',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
link: {
|
||||
label: `Import ${label}`,
|
||||
url: `./#/${module}/import`
|
||||
},
|
||||
icon: 'upload',
|
||||
submenu: []
|
||||
},
|
||||
]
|
||||
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'
|
||||
}
|
||||
}
|
||||
];
|
||||
currentUser: CurrentUserModel = {
|
||||
id: '1',
|
||||
name: 'Will Rennie',
|
||||
};
|
||||
}
|
||||
all = {
|
||||
modules: [],
|
||||
extra: [],
|
||||
};
|
||||
menu: MenuItem[] = [];
|
||||
|
||||
public resetMenu() {
|
||||
this.menu = [];
|
||||
this.all.modules = [];
|
||||
}
|
||||
|
||||
public buildMenu(items: string[],
|
||||
modules: NavbarModuleMap,
|
||||
appStrings: LanguageStringMap,
|
||||
modStrings: LanguageListStringMap,
|
||||
appListStrings: LanguageListStringMap,
|
||||
threshold: number): void {
|
||||
|
||||
const navItems = [];
|
||||
const moreItems = [];
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
this.menu = navItems;
|
||||
this.all.extra = moreItems;
|
||||
return;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
items.forEach((module: string) => {
|
||||
|
||||
if (module === 'home') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count <= threshold) {
|
||||
navItems.push(this.buildMenuItem(module, modules[module], appStrings, modStrings, appListStrings));
|
||||
} else {
|
||||
moreItems.push(this.buildMenuItem(module, modules[module], appStrings, modStrings, appListStrings));
|
||||
}
|
||||
|
||||
count++;
|
||||
});
|
||||
|
||||
this.menu = navItems;
|
||||
this.all.modules = moreItems;
|
||||
this.all.extra = moreItems;
|
||||
}
|
||||
|
||||
public buildMenuItem(
|
||||
module: string,
|
||||
moduleInfo: NavbarModule,
|
||||
appStrings: LanguageStringMap,
|
||||
modStrings: LanguageListStringMap,
|
||||
appListStrings: LanguageListStringMap): MenuItem {
|
||||
|
||||
|
||||
let moduleUrl = moduleInfo.defaultRoute;
|
||||
let moduleRoute = null;
|
||||
if (moduleUrl.startsWith(ROUTE_PREFIX)) {
|
||||
moduleRoute = moduleUrl.replace(ROUTE_PREFIX, '');
|
||||
moduleUrl = null;
|
||||
}
|
||||
|
||||
const moduleLabel = moduleInfo.labelKey;
|
||||
const menuItem = {
|
||||
link: {
|
||||
label: (appListStrings && appListStrings.moduleList[moduleInfo.labelKey]) || moduleLabel,
|
||||
url: moduleUrl,
|
||||
route: moduleRoute,
|
||||
params: null
|
||||
},
|
||||
icon: '',
|
||||
submenu: []
|
||||
};
|
||||
|
||||
moduleInfo.menu.forEach((subMenu) => {
|
||||
let label = modStrings[module][subMenu.labelKey];
|
||||
|
||||
if (!label) {
|
||||
label = appStrings[subMenu.labelKey];
|
||||
}
|
||||
|
||||
let actionUrl = subMenu.url;
|
||||
let actionRoute = null;
|
||||
let actionParams = null;
|
||||
if (actionUrl.startsWith(ROUTE_PREFIX)) {
|
||||
actionRoute = actionUrl.replace(ROUTE_PREFIX, '');
|
||||
actionUrl = null;
|
||||
|
||||
if (subMenu.params) {
|
||||
actionParams = subMenu.params;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
menuItem.submenu.push({
|
||||
link: {
|
||||
label,
|
||||
url: actionUrl,
|
||||
route: actionRoute,
|
||||
params: actionParams
|
||||
},
|
||||
icon: subMenu.icon,
|
||||
submenu: []
|
||||
});
|
||||
});
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<!-- Start of main navbar section -->
|
||||
<div class="top-panel fixed-top">
|
||||
<div class="top-panel fixed-top" *ngIf="(vm$ | async) as vm">
|
||||
|
||||
<!-- Start of empty navbar section until data is loaded -->
|
||||
<!-- Start of empty navbar section until data is loaded -->
|
||||
|
||||
<ng-template [ngIf]="!loaded">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="navbar-collapse collapse order-4 order-md-0 collapsenav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="top-nav nav-item">
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template [ngIf]="!loaded">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="navbar-collapse collapse order-4 order-md-0 collapsenav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="top-nav nav-item">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</ng-template>
|
||||
|
@ -48,21 +48,21 @@
|
|||
</ng-template>
|
||||
<ng-template [ngIf]="mainNavLink">
|
||||
<li class="" *ngFor="let item of navbar.menu" ngbDropdownItem>
|
||||
<a class="mobile-nav-link">{{ vm.appStringsList[item.link.label] }}</a>
|
||||
<a class="mobile-nav-link">{{ item.link.label }}</a>
|
||||
<svg-icon class="sicon-xs mobile-nav-arrow" src="public/themes/suite8/images/arrow_right_filled.svg"
|
||||
(click)="changeSubNav($event, item)"></svg-icon>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="mobileSubNav">
|
||||
<li class="" *ngFor="let sub of submenu" ngbDropdownItem>
|
||||
<a class="mobile-nav-link action-link">
|
||||
{{ vm.appStringsList[sub.link.label] }}
|
||||
</a>
|
||||
<a class="mobile-nav-link action-link">
|
||||
{{ sub.link.label }}
|
||||
</a>
|
||||
<ul *ngIf="sub.submenu.length" class="">
|
||||
<li *ngFor="let subitem of sub.submenu" class="nav-item">
|
||||
<a class="mobile-nav-link action-link" href="{{ subitem.link.url }}">
|
||||
{{ vm.appStringsList[subitem.link.label] }}
|
||||
</a>
|
||||
<a class="mobile-nav-link action-link" href="{{ subitem.link.url }}">
|
||||
{{ subitem.link.label }}
|
||||
</a>
|
||||
</li>
|
||||
<ng-template [ngIf]="sub.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">RECENTLY VIEWED</h4>
|
||||
|
@ -130,88 +130,93 @@
|
|||
<li class="top-nav nav-item dropdown main-grouped" *ngFor="let item of navbar.menu">
|
||||
<span data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<a class="nav-link-grouped dropdown-toggle" data-toggle="dropdown" href="{{ item.link.url }}">
|
||||
{{ vm.appStringsList[item.link.label] }}
|
||||
{{ item.link.label }}
|
||||
</a>
|
||||
</span>
|
||||
<ul class="dropdown-menu main" aria-labelledby="navbarDropdownMenuLink" [ngbCollapse]="subNavCollapse">
|
||||
<li class="nav-item dropdown-submenu submenu" *ngFor="let sub of item.submenu">
|
||||
<a class="nav-link action-link" *ngIf="!sub.submenu.length" [href]="sub.link.url">
|
||||
{{ vm.appStringsList[sub.link.label] }}
|
||||
</a>
|
||||
<a class="nav-link action-link dropdown-item dropdown-toggle" *ngIf="sub.submenu.length"
|
||||
(click)="subItemCollapse = !subItemCollapse">
|
||||
{{ vm.appStringsList[sub.link.label] }}
|
||||
</a>
|
||||
<a class="nav-link action-link" *ngIf="!sub.submenu.length" [href]="sub.link.url">
|
||||
{{ sub.link.label }}
|
||||
</a>
|
||||
<a class="nav-link action-link dropdown-item dropdown-toggle" *ngIf="sub.submenu.length"
|
||||
(click)="subItemCollapse = !subItemCollapse">
|
||||
{{ sub.link.label }}
|
||||
</a>
|
||||
<ul *ngIf="sub.submenu.length" class="dropdown-menu submenu">
|
||||
<li *ngFor="let subitem of sub.submenu" class="nav-item">
|
||||
<a class="nav-link action-link" href="{{ subitem.link.url }}">
|
||||
<svg-icon src="{{ subitem.link.iconRef.resolved }}.svg">
|
||||
</svg-icon>
|
||||
{{ vm.appStringsList[subitem.link.label] }}
|
||||
</a>
|
||||
<a class="nav-link action-link" href="{{ subitem.link.url }}">
|
||||
<svg-icon *ngIf="subitem.icon" src="public/themes/suite8/images/{{ subitem.icon }}.svg">
|
||||
</svg-icon>
|
||||
{{ subitem.link.label }}
|
||||
</a>
|
||||
</li>
|
||||
<ng-template [ngIf]="sub.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">RECENTLY VIEWED</h4>
|
||||
<li class="nav-item" *ngFor="let rec of sub.recentRecords">
|
||||
<a class="nav-link action-link"
|
||||
href="#/{{ rec.moduleName }}/{{ rec.itemId }}">{{ rec.itemSummary }}</a>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="sub.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">RECENTLY VIEWED</h4>
|
||||
<li class="nav-item" *ngFor="let rec of sub.recentRecords">
|
||||
<a class="nav-link action-link"
|
||||
href="{{ rec.url }}">{{ rec.summary }}</a>
|
||||
</li>
|
||||
</ng-template>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template [ngIf]="item.recentRecords && item.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">RECENTLY VIEWED</h4>
|
||||
<li *ngFor="let recentRecord of item.recentRecords" class="nav-item">
|
||||
<a class="nav-link action-link" href="#">{{ recentRecord.itemSummary }}</a>
|
||||
</li>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<!-- Navbar with non-grouped tabs -->
|
||||
<!-- Navbar with non-grouped tabs -->
|
||||
|
||||
<ul *ngIf="!navbar.useGroupTabs" class="navbar-nav">
|
||||
<li class="top-nav nav-item dropdown non-grouped" *ngFor="let item of navbar.all.modules">
|
||||
<ul *ngIf="!navbar.useGroupTabs" class="navbar-nav">
|
||||
<li class="top-nav nav-item dropdown non-grouped" *ngFor="let item of navbar.menu">
|
||||
|
||||
<!-- TODO: Implement a cleaner solution than hard coded ngIf -->
|
||||
<!-- TODO: Implement a cleaner solution than hard coded ngIf -->
|
||||
|
||||
<ng-template [ngIf]="item.link.label !== 'Home'">
|
||||
<span data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<a class="nav-link-nongrouped dropdown-toggle"
|
||||
href="{{ item.link.url }}">{{ item.link.label }}
|
||||
</a>
|
||||
</span>
|
||||
<div aria-labelledby="navbarDropdownMenuLink" class="dropdown-menu submenu">
|
||||
<li class="nav-item" *ngFor="let sub of item.submenu">
|
||||
<a class="nav-link action-link" href="{{ sub.link.url }}">
|
||||
<svg-icon src="{{ sub.link.iconRef.resolved }}.svg"></svg-icon>
|
||||
{{ vm.appStringsList[sub.link.label] }}
|
||||
</a>
|
||||
</li>
|
||||
<ng-template [ngIf]="item.recentRecords && item.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">RECENTLY VIEWED</h4>
|
||||
<li *ngFor="let recentRecord of item.recentRecords" class="nav-item">
|
||||
<a class="nav-link action-link" href="#">{{ recentRecord.itemSummary }}</a>
|
||||
</li>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<ul *ngIf="!navbar.useGroupTabs" class="navbar-nav">
|
||||
<li class="top-nav nav-item dropdown non-grouped">
|
||||
<a class="nav-link-nongrouped dropdown-toggle">More</a>
|
||||
<div aria-labelledby="navbarDropdownMenuLink" class="dropdown-menu more-menu submenu">
|
||||
<li class="nav-item" *ngFor="let item of navbar.all.modules">
|
||||
<a class="nav-link action-link" href="{{ item.link.url }}">{{ vm.appStringsList[item.link.label] }}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngFor="let item of navbar.all.extra">
|
||||
<a class="nav-link action-link" href="{{ item.link.url }}">{{ vm.appStringsList[item.link.label] }}</a>
|
||||
</li>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template [ngIf]="item.link.label !== 'Home'">
|
||||
<span data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<a class="nav-link-nongrouped dropdown-toggle"
|
||||
[href]="item.link.url"
|
||||
[routerLink]="item.link.route"
|
||||
[queryParams]="item.link.params"
|
||||
routerLinkActive="active">
|
||||
{{ item.link.label }}
|
||||
</a>
|
||||
</span>
|
||||
<div aria-labelledby="navbarDropdownMenuLink" class="dropdown-menu submenu">
|
||||
<div class="nav-item" *ngFor="let sub of item.submenu">
|
||||
<a class="nav-link action-link"
|
||||
[href]="sub.link.url"
|
||||
[routerLink]="sub.link.route"
|
||||
[queryParams]="sub.link.params">
|
||||
<svg-icon *ngIf="sub.icon"
|
||||
src="public/themes/suite8/images/{{ sub.icon }}.svg"></svg-icon>
|
||||
{{ sub.link.label }}
|
||||
</a>
|
||||
</div>
|
||||
<ng-template [ngIf]="item.recentRecords && item.recentRecords.length">
|
||||
<h4 class="recently-viewed-header">{{vm.appStrings['LBL_LAST_VIEWED']}}</h4>
|
||||
<div *ngFor="let recentRecord of item.recentRecords" class="nav-item">
|
||||
<a class="nav-link action-link" href="#">{{ recentRecord.summary }}</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<ul *ngIf="!navbar.useGroupTabs && navbar.all.modules && navbar.all.modules.length > 0"
|
||||
class="navbar-nav">
|
||||
<li class="top-nav nav-item dropdown non-grouped">
|
||||
<a class="nav-link-nongrouped dropdown-toggle">More</a>
|
||||
<div aria-labelledby="navbarDropdownMenuLink" class="dropdown-menu more-menu submenu">
|
||||
<div class="nav-item" *ngFor="let item of navbar.all.modules">
|
||||
<a class="nav-link action-link" [href]="item.link.url"
|
||||
[routerLink]="item.link.route">{{ item.link.label }}</a>
|
||||
</div>
|
||||
<div class="nav-item" *ngFor="let item of navbar.all.extra">
|
||||
<a class="nav-link action-link" [href]="item.link.url"
|
||||
[routerLink]="item.link.route">{{ item.link.label }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="global-links" ngbDropdown>
|
||||
<ul class="navbar-nav">
|
||||
|
|
|
@ -6,77 +6,54 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
|
|||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import {NavbarUiComponent} from './navbar.component';
|
||||
import {of} from 'rxjs';
|
||||
import {Metadata} from '@services/metadata/metadata.service';
|
||||
import {NavigationFacade} from '@base/facades/navigation/navigation.facade';
|
||||
import {LanguageFacade} from '@base/facades/language/language.facade';
|
||||
import {navigationMock} from '@base/facades/navigation/navigation.facade.spec.mock';
|
||||
import {languageFacadeMock} from '@base/facades/language/language.facade.spec.mock';
|
||||
|
||||
describe('NavbarUiComponent', () => {
|
||||
|
||||
|
||||
|
||||
describe('Test with mock service', () => {
|
||||
let component: NavbarUiComponent;
|
||||
let fixture: ComponentFixture<NavbarUiComponent>;
|
||||
let service: Metadata;
|
||||
|
||||
const navigationMockData = {
|
||||
navbar: {
|
||||
NonGroupedTabs: {
|
||||
contacts: 'Contacts',
|
||||
},
|
||||
groupedTabs: {
|
||||
Sales: {
|
||||
modules: {
|
||||
Accounts: 'Accounts',
|
||||
}
|
||||
},
|
||||
},
|
||||
userActionMenu: [
|
||||
{
|
||||
label: 'Profile',
|
||||
url: 'index.php?module=Users&action=EditView&record=1',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
label: 'Employees',
|
||||
url: 'index.php?module=Employees&action=index',
|
||||
submenu: []
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
// Create a fake TwainService object with a 'getQuote()' spy
|
||||
const metadata = jasmine.createSpyObj('Metadata', ['getNavigation']);
|
||||
// Make the spy return a synchronous Observable with the test data
|
||||
const getMetadata = metadata.getNavigation.and.returnValue(of(navigationMockData.navbar));
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
imports: [RouterTestingModule, HttpClientTestingModule, NgbModule],
|
||||
providers: [{provide: Metadata, useValue: metadata}],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
NgbModule
|
||||
],
|
||||
providers: [
|
||||
{provide: NavigationFacade, useValue: navigationMock},
|
||||
{provide: LanguageFacade, useValue: languageFacadeMock},
|
||||
],
|
||||
declarations: [NavbarUiComponent]
|
||||
}).compileComponents();
|
||||
service = TestBed.get(Metadata);
|
||||
fixture = TestBed.createComponent(NavbarUiComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
fixture.detectChanges(); // onInit()
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
/*
|
||||
it('should get metadata', async (() => {
|
||||
fixture.detectChanges(); // onInit()
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.navigationMetadata).toEqual(jasmine.objectContaining(navigationMockData.navbar));
|
||||
expect(component.navbar).toEqual(jasmine.objectContaining(navigationMockData.navbar));
|
||||
});
|
||||
|
||||
component.ngOnInit();
|
||||
}));
|
||||
|
||||
*/
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,98 +1,124 @@
|
|||
import {Component, OnInit, HostListener} from '@angular/core';
|
||||
import {Router, NavigationEnd} from '@angular/router';
|
||||
import {ApiService} from '../../services/api/api.service';
|
||||
import {NavbarModel} from './navbar-model';
|
||||
import {NavbarAbstract} from './navbar.abstract';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
|
||||
import { Metadata } from '../../services/metadata/metadata.service';
|
||||
import {NavbarModuleMap, NavigationFacade} from '@base/facades/navigation/navigation.facade';
|
||||
import {LanguageFacade, LanguageListStringMap, LanguageStringMap} from '@base/facades/language/language.facade';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-navbar-ui',
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrls: []
|
||||
selector: 'scrm-navbar-ui',
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrls: []
|
||||
})
|
||||
export class NavbarUiComponent implements OnInit {
|
||||
private navbarSubscription: Subscription;
|
||||
public navigationMetadata: any;
|
||||
|
||||
constructor(protected metadata: Metadata, protected api: ApiService, protected router: Router) {
|
||||
NavbarUiComponent.instances.push(this);
|
||||
}
|
||||
protected static instances: NavbarUiComponent[] = [];
|
||||
|
||||
protected static instances: NavbarUiComponent[] = [];
|
||||
loaded = true;
|
||||
|
||||
loaded = true;
|
||||
mainNavCollapse = true;
|
||||
subItemCollapse = true;
|
||||
subNavCollapse = true;
|
||||
mobileNavbar = false;
|
||||
mobileSubNav = false;
|
||||
backLink = false;
|
||||
mainNavLink = true;
|
||||
parentNavLink = '';
|
||||
submenu: any = [];
|
||||
|
||||
mainNavCollapse = true;
|
||||
subItemCollapse = true;
|
||||
subNavCollapse = true;
|
||||
mobileNavbar = false;
|
||||
mobileSubNav = false;
|
||||
backLink = false;
|
||||
mainNavLink = true;
|
||||
parentNavLink: string = '';
|
||||
submenu: any = [];
|
||||
navbar: NavbarModel = new NavbarAbstract();
|
||||
|
||||
navbar: NavbarModel = new NavbarAbstract();
|
||||
tabs$: Observable<string[]> = this.navigationFacade.tabs$;
|
||||
modules$: Observable<NavbarModuleMap> = this.navigationFacade.modules$;
|
||||
appStrings$: Observable<LanguageStringMap> = this.languageFacade.appStrings$;
|
||||
modStrings$: Observable<LanguageListStringMap> = this.languageFacade.modStrings$;
|
||||
appListStrings$: Observable<LanguageListStringMap> = this.languageFacade.appListStrings$;
|
||||
|
||||
public changeSubNav(event: Event, parentNavItem) {
|
||||
this.mobileSubNav = !this.mobileSubNav;
|
||||
this.backLink = !this.backLink;
|
||||
this.mainNavLink = !this.mainNavLink;
|
||||
this.submenu = parentNavItem.submenu;
|
||||
}
|
||||
vm$ = combineLatest([this.tabs$, this.modules$, this.appStrings$, this.appListStrings$, this.modStrings$]).pipe(
|
||||
map((
|
||||
[
|
||||
tabs,
|
||||
modules,
|
||||
appStrings,
|
||||
appListStrings,
|
||||
modStrings
|
||||
]) => {
|
||||
|
||||
public navBackLink(event: Event) {
|
||||
this.mobileSubNav = !this.mobileSubNav;
|
||||
this.backLink = !this.backLink;
|
||||
this.mainNavLink = !this.mainNavLink;
|
||||
}
|
||||
if (tabs && tabs.length > 0 &&
|
||||
modules && Object.keys(modules).length > 0 &&
|
||||
appStrings && Object.keys(appStrings).length > 0 &&
|
||||
modStrings && Object.keys(modStrings).length > 0 &&
|
||||
appListStrings && Object.keys(appListStrings).length > 0) {
|
||||
this.navbar.resetMenu();
|
||||
this.navbar.buildMenu(tabs, modules, appStrings, modStrings, appListStrings, this.menuItemThreshold);
|
||||
}
|
||||
|
||||
@HostListener("window:resize", ["$event"])
|
||||
onResize(event: any) {
|
||||
event.target.innerWidth;
|
||||
if (innerWidth <= 768) {
|
||||
this.mobileNavbar = true;
|
||||
} else {
|
||||
this.mobileNavbar = false;
|
||||
return {
|
||||
tabs,
|
||||
modules,
|
||||
appStrings,
|
||||
appListStrings,
|
||||
modStrings
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
protected menuItemThreshold = 5;
|
||||
|
||||
constructor(protected navigationFacade: NavigationFacade,
|
||||
protected languageFacade: LanguageFacade,
|
||||
protected api: ApiService) {
|
||||
const navbar = new NavbarAbstract();
|
||||
this.setNavbar(navbar);
|
||||
|
||||
NavbarUiComponent.instances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
static reset() {
|
||||
NavbarUiComponent.instances.forEach((navbarComponent: NavbarUiComponent) => {
|
||||
navbarComponent.loaded = false;
|
||||
navbarComponent.navbar = new NavbarAbstract();
|
||||
});
|
||||
}
|
||||
|
||||
protected setNavbar(navbar: NavbarModel) {
|
||||
this.navbar = navbar;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
protected isLoaded() {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
const menuItemThreshold = 5;
|
||||
/*
|
||||
* TODO : this call should be moved elsewhere once
|
||||
* we have the final structure using cache
|
||||
*/
|
||||
this.navbarSubscription = this.metadata
|
||||
.getNavigation()
|
||||
.subscribe((data) => {
|
||||
this.navigationMetadata = data;
|
||||
if (data && data.NonGroupedTabs) {
|
||||
this.navbar.buildMenu(data.NonGroupedTabs, menuItemThreshold);
|
||||
}
|
||||
static reset() {
|
||||
NavbarUiComponent.instances.forEach((navbarComponent: NavbarUiComponent) => {
|
||||
navbarComponent.loaded = false;
|
||||
navbarComponent.navbar = new NavbarAbstract();
|
||||
});
|
||||
}
|
||||
|
||||
const navbar = new NavbarAbstract();
|
||||
this.setNavbar(navbar);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
public changeSubNav(event: Event, parentNavItem) {
|
||||
this.mobileSubNav = !this.mobileSubNav;
|
||||
this.backLink = !this.backLink;
|
||||
this.mainNavLink = !this.mainNavLink;
|
||||
this.submenu = parentNavItem.submenu;
|
||||
}
|
||||
|
||||
public navBackLink(event: Event) {
|
||||
this.mobileSubNav = !this.mobileSubNav;
|
||||
this.backLink = !this.backLink;
|
||||
this.mainNavLink = !this.mainNavLink;
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event: any) {
|
||||
const innerWidth = event.target.innerWidth;
|
||||
if (innerWidth <= 768) {
|
||||
this.mobileNavbar = true;
|
||||
} else {
|
||||
this.mobileNavbar = false;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const navbar = new NavbarAbstract();
|
||||
this.setNavbar(navbar);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
protected setNavbar(navbar: NavbarModel) {
|
||||
this.navbar = navbar;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
protected isLoaded() {
|
||||
return this.loaded;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import {LogoutUiModule} from '../logout/logout.module';
|
|||
import {ActionBarUiModule} from '../action-bar/action-bar.module';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {AngularSvgIconModule} from 'angular-svg-icon';
|
||||
import {RouterModule} from '@angular/router';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [NavbarUiComponent],
|
||||
|
@ -20,7 +22,8 @@ import {AngularSvgIconModule} from 'angular-svg-icon';
|
|||
LogoutUiModule,
|
||||
ActionBarUiModule,
|
||||
NgbModule,
|
||||
AngularSvgIconModule
|
||||
AngularSvgIconModule,
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class NavbarUiModule {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import {AppStateFacade} from '@base/facades/app-state/app-state.facade';
|
||||
|
||||
|
||||
export const appStateFacadeMock = new AppStateFacade();
|
24
core/app/src/facades/app-state/app-state.facade.spec.ts
Normal file
24
core/app/src/facades/app-state/app-state.facade.spec.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||
import {AppStateFacade} from '@base/facades/app-state/app-state.facade';
|
||||
import {appStateFacadeMock} from '@base/facades/app-state/app-state.facade.spec.mock';
|
||||
|
||||
describe('AppState Facade', () => {
|
||||
let injector: TestBed;
|
||||
const service: AppStateFacade = appStateFacadeMock;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
injector = getTestBed();
|
||||
});
|
||||
|
||||
it('#updateLoading',
|
||||
(done: DoneFn) => {
|
||||
service.updateLoading(true);
|
||||
service.loading$.subscribe(loading => {
|
||||
expect(loading).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {map, distinctUntilChanged, tap, shareReplay, first} from 'rxjs/operators';
|
||||
import {RecordGQL} from '@services/api/graphql-api/api.record.get';
|
||||
import {AppStateFacade} from "@base/facades/app-state.facade";
|
||||
|
||||
export interface LanguageStringMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadedLanguageStringMap {
|
||||
[key: string]: LanguageStringMap;
|
||||
}
|
||||
|
||||
export interface LanguageState {
|
||||
languageStrings: LanguageStringMap;
|
||||
languageType: string;
|
||||
loaded?: LoadedLanguageStringMap;
|
||||
}
|
||||
|
||||
let internalState: LanguageState = {
|
||||
languageStrings: {},
|
||||
languageType: 'en_us',
|
||||
loaded: {}
|
||||
};
|
||||
|
||||
const cache: { [key: string]: Observable<any> } = {};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LanguageFacade {
|
||||
protected store = new BehaviorSubject<LanguageState>(internalState);
|
||||
protected state$ = this.store.asObservable();
|
||||
protected resourceName = 'appStrings';
|
||||
protected fieldsMetadata = {
|
||||
fields: [
|
||||
'id',
|
||||
'_id',
|
||||
'items'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Public long-lived observable streams
|
||||
*/
|
||||
languageStrings$ = this.state$.pipe(map(state => state.languageStrings), distinctUntilChanged());
|
||||
languageType$ = this.state$.pipe(map(state => state.languageType), distinctUntilChanged());
|
||||
|
||||
constructor(private recordGQL: RecordGQL, private appStateFacade: AppStateFacade) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update the AppStrings to the given language
|
||||
*
|
||||
* @param languageType
|
||||
*/
|
||||
public updateLanguage(languageType: string): void {
|
||||
this.loadAppStrings(languageType).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial AppStrings Load for given if not cached and update state.
|
||||
* Returns observable to be used in resolver if needed
|
||||
*
|
||||
* @param languageType
|
||||
* @returns Observable
|
||||
*/
|
||||
public loadAppStrings(languageType: string): Observable<{}> {
|
||||
|
||||
return this.getAppStrings(languageType).pipe(
|
||||
first(),
|
||||
tap((languageStrings: LanguageStringMap) => {
|
||||
this.updateState({...internalState, languageStrings, languageType});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal API
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Update internal state cache and emit from store...
|
||||
*
|
||||
* @param state
|
||||
*/
|
||||
protected updateState(state: LanguageState): void {
|
||||
this.store.next(internalState = state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AppStrings cached Observable or call the backend
|
||||
*
|
||||
* @param language
|
||||
* @returns Observable<any>
|
||||
*/
|
||||
protected getAppStrings(language: string): Observable<{}> {
|
||||
|
||||
if (cache[language]) {
|
||||
return cache[language];
|
||||
}
|
||||
|
||||
this.appStateFacade.updateLoading(true);
|
||||
|
||||
cache[language] = this.fetchAppStrings(language).pipe(
|
||||
shareReplay(1),
|
||||
tap(() => this.appStateFacade.updateLoading(false))
|
||||
);
|
||||
|
||||
return cache[language];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the App strings from the backend
|
||||
*
|
||||
* @param language
|
||||
* @returns Observable<{}>
|
||||
*/
|
||||
protected fetchAppStrings(language: string): Observable<{}> {
|
||||
return this.recordGQL.fetch(this.resourceName, `/api/app-strings/${language}`, this.fieldsMetadata)
|
||||
.pipe(
|
||||
map(({data}) => {
|
||||
let items = {};
|
||||
|
||||
if (data.appStrings) {
|
||||
items = data.appStrings.items;
|
||||
}
|
||||
|
||||
return items;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
109
core/app/src/facades/language/language.facade.spec.mock.ts
Normal file
109
core/app/src/facades/language/language.facade.spec.mock.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {RecordGQL} from '@services/api/graphql-api/api.record.get';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {shareReplay} from 'rxjs/operators';
|
||||
import {LanguageFacade} from '@base/facades/language/language.facade';
|
||||
import {appStateFacadeMock} from '@base/facades/app-state/app-state.facade.spec.mock';
|
||||
|
||||
export const languageMockData = {
|
||||
appStrings: {
|
||||
LBL_SEARCH_REAULTS_TITLE: 'Results',
|
||||
ERR_SEARCH_INVALID_QUERY: 'An error has occurred while performing the search. Your query syntax might not be valid.',
|
||||
ERR_SEARCH_NO_RESULTS: 'No results matching your search criteria. Try broadening your search.',
|
||||
LBL_SEARCH_PERFORMED_IN: 'Search performed in',
|
||||
LBL_EMAIL_CODE: 'Email Code:',
|
||||
LBL_SEND: 'Send',
|
||||
LBL_LOGOUT: 'Logout',
|
||||
LBL_TOUR_NEXT: 'Next',
|
||||
},
|
||||
appListStrings: {
|
||||
language_pack_name: 'US English',
|
||||
moduleList: {
|
||||
Home: 'Home',
|
||||
Accounts: 'Accounts',
|
||||
}
|
||||
},
|
||||
modStrings: {
|
||||
home: {
|
||||
LBL_MODULE_NAME: 'Home',
|
||||
LBL_NEW_FORM_TITLE: 'New Contact',
|
||||
LBL_FIRST_NAME: 'First Name:',
|
||||
LBL_LAST_NAME: 'Last Name:',
|
||||
LBL_LIST_LAST_NAME: 'Last Name',
|
||||
LBL_PHONE: 'Phone:',
|
||||
LBL_EMAIL_ADDRESS: 'Email Address:',
|
||||
LBL_MY_PIPELINE_FORM_TITLE: 'My Pipeline',
|
||||
LBL_PIPELINE_FORM_TITLE: 'Pipeline By Sales Stage',
|
||||
LBL_RGraph_PIPELINE_FORM_TITLE: 'Pipeline By Sales Stage',
|
||||
LNK_NEW_CONTACT: 'Create Contact',
|
||||
LNK_NEW_ACCOUNT: 'Create Account',
|
||||
LNK_NEW_OPPORTUNITY: 'Create Opportunity',
|
||||
LNK_NEW_LEAD: 'Create Lead',
|
||||
LNK_NEW_CASE: 'Create Case',
|
||||
LNK_NEW_NOTE: 'Create Note or Attachment',
|
||||
LNK_NEW_CALL: 'Log Call',
|
||||
LNK_NEW_EMAIL: 'Archive Email',
|
||||
LNK_NEW_MEETING: 'Schedule Meeting',
|
||||
LNK_NEW_TASK: 'Create Task',
|
||||
LNK_NEW_BUG: 'Report Bug',
|
||||
LNK_NEW_SEND_EMAIL: 'Compose Email',
|
||||
LBL_NO_ACCESS: 'You do not have access'
|
||||
},
|
||||
accounts: {
|
||||
LBL_ID: 'ID',
|
||||
LBL_NAME: 'Name:',
|
||||
LBL_LIST_NAME: 'Name',
|
||||
LNK_ACCOUNT_LIST: 'View Accounts',
|
||||
LNK_NEW_ACCOUNT: 'Create Account',
|
||||
LBL_MODULE_NAME: 'Accounts',
|
||||
LBL_MODULE_TITLE: 'Accounts: Home',
|
||||
LBL_MODULE_ID: 'Accounts',
|
||||
LBL_NEW_FORM_TITLE: 'New Account',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class LanguageRecordGQLSpy extends RecordGQL {
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public fetch(module: string, id: string, metadata: { fields: string[] }): Observable<any> {
|
||||
if (module === 'appStrings') {
|
||||
|
||||
return of({
|
||||
data: {
|
||||
appStrings: {
|
||||
_id: 'en_us',
|
||||
items: languageMockData.appStrings
|
||||
}
|
||||
}
|
||||
}).pipe(shareReplay());
|
||||
}
|
||||
|
||||
if (module === 'appListStrings') {
|
||||
return of({
|
||||
data: {
|
||||
appListStrings: {
|
||||
_id: 'en_us',
|
||||
items: languageMockData.appListStrings
|
||||
}
|
||||
}
|
||||
}).pipe(shareReplay());
|
||||
}
|
||||
|
||||
if (module === 'modStrings') {
|
||||
return of({
|
||||
data: {
|
||||
modStrings: {
|
||||
_id: 'en_us',
|
||||
items: languageMockData.modStrings
|
||||
}
|
||||
}
|
||||
}).pipe(shareReplay());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const languageFacadeMock = new LanguageFacade(new LanguageRecordGQLSpy(), appStateFacadeMock);
|
23
core/app/src/facades/language/language.facade.spec.ts
Normal file
23
core/app/src/facades/language/language.facade.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||
import {LanguageFacade} from "@base/facades/language/language.facade";
|
||||
import {languageFacadeMock, languageMockData} from "@base/facades/language/language.facade.spec.mock";
|
||||
|
||||
describe('Language Facade', () => {
|
||||
let injector: TestBed;
|
||||
const service: LanguageFacade = languageFacadeMock;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
injector = getTestBed();
|
||||
});
|
||||
|
||||
it('#load',
|
||||
(done: DoneFn) => {
|
||||
service.load('en_us', languageFacadeMock.getAvailableStringsTypes()).subscribe(data => {
|
||||
expect(data).toEqual(jasmine.objectContaining(languageMockData));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
292
core/app/src/facades/language/language.facade.ts
Normal file
292
core/app/src/facades/language/language.facade.ts
Normal file
|
@ -0,0 +1,292 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {BehaviorSubject, forkJoin, Observable} from 'rxjs';
|
||||
import {map, distinctUntilChanged, tap, shareReplay, first} from 'rxjs/operators';
|
||||
import {RecordGQL} from '@services/api/graphql-api/api.record.get';
|
||||
import {AppStateFacade} from '@base/facades/app-state/app-state.facade';
|
||||
|
||||
export interface LanguageStringMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface LanguageListStringMap {
|
||||
[key: string]: string | LanguageStringMap;
|
||||
}
|
||||
|
||||
export interface LoadedLanguageStringMap {
|
||||
[key: string]: LanguageStringMap;
|
||||
}
|
||||
|
||||
export interface LanguageState {
|
||||
appStrings: LanguageStringMap;
|
||||
appListStrings: LanguageListStringMap;
|
||||
modStrings: LanguageListStringMap;
|
||||
languageKey: string;
|
||||
loaded?: LoadedLanguageStringMap;
|
||||
hasChanged: boolean;
|
||||
}
|
||||
|
||||
export interface LanguageCache {
|
||||
[key: string]: {
|
||||
[key: string]: Observable<any>;
|
||||
};
|
||||
}
|
||||
|
||||
let internalState: LanguageState = {
|
||||
appStrings: {},
|
||||
appListStrings: {},
|
||||
modStrings: {},
|
||||
languageKey: 'en_us',
|
||||
loaded: {},
|
||||
hasChanged: false
|
||||
};
|
||||
|
||||
const loadedLanguages = {};
|
||||
const cache: LanguageCache = {
|
||||
appStrings: {},
|
||||
appListStrings: {},
|
||||
modStrings: {},
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LanguageFacade {
|
||||
protected store = new BehaviorSubject<LanguageState>(internalState);
|
||||
protected state$ = this.store.asObservable();
|
||||
|
||||
protected config = {
|
||||
appStrings: {
|
||||
fetch: 'fetchAppStrings',
|
||||
resourceName: 'appStrings',
|
||||
metadata: {
|
||||
fields: [
|
||||
'id',
|
||||
'_id',
|
||||
'items'
|
||||
]
|
||||
}
|
||||
},
|
||||
appListStrings: {
|
||||
fetch: 'fetchAppListStrings',
|
||||
resourceName: 'appListStrings',
|
||||
metadata: {
|
||||
fields: [
|
||||
'id',
|
||||
'_id',
|
||||
'items'
|
||||
]
|
||||
}
|
||||
},
|
||||
modStrings: {
|
||||
fetch: 'fetchModStrings',
|
||||
resourceName: 'modStrings',
|
||||
metadata: {
|
||||
fields: [
|
||||
'id',
|
||||
'_id',
|
||||
'items'
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Public long-lived observable streams
|
||||
*/
|
||||
appStrings$ = this.state$.pipe(map(state => state.appStrings), distinctUntilChanged());
|
||||
appListStrings$ = this.state$.pipe(map(state => state.appListStrings), distinctUntilChanged());
|
||||
modStrings$ = this.state$.pipe(map(state => state.modStrings), distinctUntilChanged());
|
||||
languageKey$ = this.state$.pipe(map(state => state.languageKey), distinctUntilChanged());
|
||||
|
||||
constructor(private recordGQL: RecordGQL, private appStateFacade: AppStateFacade) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update the language strings toe the given language
|
||||
*
|
||||
* @param languageKey
|
||||
*/
|
||||
public changeLanguage(languageKey: string): void {
|
||||
const types = [];
|
||||
|
||||
Object.keys(loadedLanguages).forEach(type => loadedLanguages[type] && types.push(type));
|
||||
|
||||
internalState.hasChanged = true;
|
||||
|
||||
this.appStateFacade.updateLoading(true);
|
||||
|
||||
this.load(languageKey, types).pipe(
|
||||
tap(() => this.appStateFacade.updateLoading(false))
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available string types
|
||||
*
|
||||
* @returns Observable
|
||||
*/
|
||||
public getAvailableStringsTypes(): string[] {
|
||||
return Object.keys(this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the language has changed manually
|
||||
*
|
||||
* @returns bool
|
||||
*/
|
||||
public hasLanguageChanged(): boolean {
|
||||
return internalState.hasChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active language
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
public getCurrentLanguage(): string {
|
||||
return internalState.languageKey;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initial Language Strings Load for given language and types if not cached and update state.
|
||||
* Returns observable to be used in resolver if needed
|
||||
*
|
||||
* @param languageKey
|
||||
* @param types
|
||||
* @returns Observable
|
||||
*/
|
||||
public load(languageKey: string, types: string[]): Observable<{}> {
|
||||
|
||||
const streams$ = {};
|
||||
|
||||
types.forEach(type => streams$[type] = this.getStrings(languageKey, type));
|
||||
|
||||
return forkJoin(streams$).pipe(
|
||||
first(),
|
||||
tap(result => {
|
||||
const stateUpdate = {...internalState, languageKey};
|
||||
|
||||
types.forEach(type => {
|
||||
stateUpdate[type] = result[type];
|
||||
loadedLanguages[type] = true;
|
||||
});
|
||||
|
||||
this.updateState(stateUpdate);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal API
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Update internal state cache and emit from store...
|
||||
*
|
||||
* @param state
|
||||
*/
|
||||
protected updateState(state: LanguageState): void {
|
||||
this.store.next(internalState = state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get given $type of strings Observable from cache or call the backend
|
||||
*
|
||||
* @param language
|
||||
* @param type
|
||||
* @returns Observable<any>
|
||||
*/
|
||||
protected getStrings(language: string, type: string): Observable<{}> {
|
||||
|
||||
const stringsCache = cache[type];
|
||||
const fetchMethod = this.config[type].fetch;
|
||||
|
||||
if (stringsCache[language]) {
|
||||
return stringsCache[language];
|
||||
}
|
||||
|
||||
stringsCache[language] = this[fetchMethod](language).pipe(
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
return stringsCache[language];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the App strings from the backend
|
||||
*
|
||||
* @param language
|
||||
* @returns Observable<{}>
|
||||
*/
|
||||
protected fetchAppStrings(language: string): Observable<{}> {
|
||||
const resourceName = this.config.appStrings.resourceName;
|
||||
const fields = this.config.appStrings.metadata;
|
||||
return this.recordGQL.fetch(resourceName, `/api/app-strings/${language}`, fields)
|
||||
.pipe(
|
||||
map(({data}) => {
|
||||
let items = {};
|
||||
|
||||
if (data.appStrings) {
|
||||
items = data.appStrings.items;
|
||||
}
|
||||
|
||||
return items;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the App list strings from the backend
|
||||
*
|
||||
* @param language
|
||||
* @returns Observable<{}>
|
||||
*/
|
||||
protected fetchAppListStrings(language: string): Observable<{}> {
|
||||
const resourceName = this.config.appListStrings.resourceName;
|
||||
const fields = this.config.appListStrings.metadata;
|
||||
|
||||
return this.recordGQL.fetch(resourceName, `/api/app-list-strings/${language}`, fields)
|
||||
.pipe(
|
||||
map(({data}) => {
|
||||
let items = {};
|
||||
|
||||
if (data.appListStrings) {
|
||||
items = data.appListStrings.items;
|
||||
}
|
||||
|
||||
return items;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the Mod strings from the backend
|
||||
*
|
||||
* @param language
|
||||
* @returns Observable<{}>
|
||||
*/
|
||||
protected fetchModStrings(language: string): Observable<{}> {
|
||||
const resourceName = this.config.modStrings.resourceName;
|
||||
const fields = this.config.modStrings.metadata;
|
||||
return this.recordGQL.fetch(resourceName, `/api/mod-strings/${language}`, fields)
|
||||
.pipe(
|
||||
map(({data}) => {
|
||||
let items = {};
|
||||
|
||||
if (data.modStrings) {
|
||||
items = data.modStrings.items;
|
||||
}
|
||||
|
||||
return items;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
214
core/app/src/facades/navigation/navigation.facade.spec.mock.ts
Normal file
214
core/app/src/facades/navigation/navigation.facade.spec.mock.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
import {NavigationFacade} from '@base/facades/navigation/navigation.facade';
|
||||
import {RecordGQL} from '@services/api/graphql-api/api.record.get';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {shareReplay} from 'rxjs/operators';
|
||||
|
||||
export const navigationMockData = {
|
||||
navbar: {
|
||||
groupedTabs: [
|
||||
{
|
||||
name: 'LBL_TABGROUP_SALES',
|
||||
labelKey: 'LBL_TABGROUP_SALES',
|
||||
modules: [
|
||||
'accounts',
|
||||
'home',
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'LBL_TABGROUP_MARKETING',
|
||||
labelKey: 'LBL_TABGROUP_MARKETING',
|
||||
modules: [
|
||||
'accounts',
|
||||
'home',
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'LBL_TABGROUP_SUPPORT',
|
||||
labelKey: 'LBL_TABGROUP_SUPPORT',
|
||||
modules: [
|
||||
'accounts',
|
||||
'home'
|
||||
]
|
||||
},
|
||||
],
|
||||
tabs: [
|
||||
'home',
|
||||
'accounts',
|
||||
],
|
||||
userActionMenu: [
|
||||
{
|
||||
name: 'profile',
|
||||
labelKey: 'LBL_PROFILE',
|
||||
url: 'index.php?module=Users&action=EditView&record=1',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
name: 'employees',
|
||||
labelKey: 'LBL_EMPLOYEES',
|
||||
url: 'index.php?module=Employees&action=index',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
name: 'training',
|
||||
labelKey: 'LBL_TRAINING',
|
||||
url: 'https://community.suitecrm.com',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
labelKey: 'LNK_ABOUT',
|
||||
url: 'index.php?module=Home&action=About',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
labelKey: 'LBL_LOGOUT',
|
||||
url: 'index.php?module=Users&action=Logout',
|
||||
icon: ''
|
||||
}
|
||||
],
|
||||
modules: {
|
||||
home: {
|
||||
path: 'home',
|
||||
defaultRoute: './#/home/index',
|
||||
name: 'home',
|
||||
labelKey: 'Home',
|
||||
menu: []
|
||||
},
|
||||
accounts: {
|
||||
path: 'accounts',
|
||||
defaultRoute: './#/accounts/index',
|
||||
name: 'accounts',
|
||||
labelKey: 'Accounts',
|
||||
menu: [
|
||||
{
|
||||
name: 'Create',
|
||||
labelKey: 'LNK_NEW_ACCOUNT',
|
||||
url: './#/accounts/edit',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
labelKey: 'LNK_ACCOUNT_LIST',
|
||||
url: './#/accounts/index',
|
||||
icon: 'view'
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
labelKey: 'LNK_IMPORT_ACCOUNTS',
|
||||
url: './#/import/step1',
|
||||
icon: 'download'
|
||||
}
|
||||
]
|
||||
},
|
||||
contacts: {
|
||||
path: 'contacts',
|
||||
defaultRoute: './#/contacts/index',
|
||||
name: 'contacts',
|
||||
labelKey: 'Contacts',
|
||||
menu: [
|
||||
{
|
||||
name: 'Create',
|
||||
labelKey: 'LNK_NEW_CONTACT',
|
||||
url: './#/contacts/edit',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'Create_Contact_Vcard',
|
||||
labelKey: 'LNK_IMPORT_VCARD',
|
||||
url: './#/contacts/importvcard',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
labelKey: 'LNK_CONTACT_LIST',
|
||||
url: './#/contacts/index',
|
||||
icon: 'view'
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
labelKey: 'LNK_IMPORT_CONTACTS',
|
||||
url: './#/import/step1',
|
||||
icon: 'download'
|
||||
}
|
||||
]
|
||||
},
|
||||
opportunities: {
|
||||
path: 'opportunities',
|
||||
defaultRoute: './#/opportunities/index',
|
||||
name: 'opportunities',
|
||||
labelKey: 'Opportunities',
|
||||
menu: [
|
||||
{
|
||||
name: 'Create',
|
||||
labelKey: 'LNK_NEW_OPPORTUNITY',
|
||||
url: './#/opportunities/edit',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
labelKey: 'LNK_OPPORTUNITY_LIST',
|
||||
url: './#/opportunities/index',
|
||||
icon: 'view'
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
labelKey: 'LNK_IMPORT_OPPORTUNITIES',
|
||||
url: './#/import/step1',
|
||||
icon: 'download'
|
||||
}
|
||||
]
|
||||
},
|
||||
leads: {
|
||||
path: 'leads',
|
||||
defaultRoute: './#/leads/index',
|
||||
name: 'leads',
|
||||
labelKey: 'Leads',
|
||||
menu: [
|
||||
{
|
||||
name: 'Create',
|
||||
labelKey: 'LNK_NEW_LEAD',
|
||||
url: './#/leads/edit',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'Create_Lead_Vcard',
|
||||
labelKey: 'LNK_IMPORT_VCARD',
|
||||
url: './#/leads/importvcard',
|
||||
icon: 'plus'
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
labelKey: 'LNK_LEAD_LIST',
|
||||
url: './#/leads/index',
|
||||
icon: 'view'
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
labelKey: 'LNK_IMPORT_LEADS',
|
||||
url: './#/import/step1',
|
||||
icon: 'download'
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class NavigationRecordGQLSpy extends RecordGQL {
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public fetch(module: string, id: string, metadata: { fields: string[] }): Observable<any> {
|
||||
|
||||
return of({
|
||||
data: {
|
||||
navbar: navigationMockData.navbar
|
||||
}
|
||||
}).pipe(shareReplay());
|
||||
}
|
||||
}
|
||||
|
||||
export const navigationMock = new NavigationFacade(new NavigationRecordGQLSpy());
|
24
core/app/src/facades/navigation/navigation.facade.spec.ts
Normal file
24
core/app/src/facades/navigation/navigation.facade.spec.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||
import {NavigationFacade} from './navigation.facade';
|
||||
import {navigationMock, navigationMockData} from './navigation.facade.spec.mock';
|
||||
|
||||
describe('Navigation Facade', () => {
|
||||
let injector: TestBed;
|
||||
const service: NavigationFacade = navigationMock;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
injector = getTestBed();
|
||||
});
|
||||
|
||||
it('#load',
|
||||
(done: DoneFn) => {
|
||||
|
||||
service.load().subscribe(data => {
|
||||
expect(data).toEqual(jasmine.objectContaining(navigationMockData.navbar));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
170
core/app/src/facades/navigation/navigation.facade.ts
Normal file
170
core/app/src/facades/navigation/navigation.facade.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {map, distinctUntilChanged, tap, shareReplay} from 'rxjs/operators';
|
||||
|
||||
import {RecordGQL} from '@services/api/graphql-api/api.record.get';
|
||||
|
||||
export interface Navigation {
|
||||
tabs: string[];
|
||||
groupedTabs: GroupedTab[];
|
||||
modules: NavbarModuleMap;
|
||||
userActionMenu: UserActionMenu[];
|
||||
}
|
||||
|
||||
export interface NavbarModuleMap {
|
||||
[key: string]: NavbarModule;
|
||||
}
|
||||
|
||||
export interface NavbarModule {
|
||||
name: string;
|
||||
path: string;
|
||||
defaultRoute: string;
|
||||
labelKey: string;
|
||||
menu: ModuleSubMenu[];
|
||||
}
|
||||
|
||||
export interface GroupedTab {
|
||||
name: string;
|
||||
labelKey: string;
|
||||
modules: string[];
|
||||
}
|
||||
|
||||
export interface UserActionMenu {
|
||||
name: string;
|
||||
labelKey: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface ModuleSubMenu {
|
||||
name: string;
|
||||
labelKey: string;
|
||||
label?: string;
|
||||
url: string;
|
||||
params?: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
let internalState: Navigation = {
|
||||
tabs: [],
|
||||
groupedTabs: [],
|
||||
modules: {},
|
||||
userActionMenu: []
|
||||
};
|
||||
|
||||
let cache$: Observable<any> = null;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NavigationFacade {
|
||||
|
||||
protected store = new BehaviorSubject<Navigation>(internalState);
|
||||
protected state$ = this.store.asObservable();
|
||||
protected resourceName = 'navbar';
|
||||
protected fieldsMetadata = {
|
||||
fields: [
|
||||
'tabs',
|
||||
'groupedTabs',
|
||||
'modules',
|
||||
'userActionMenu'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Public long-lived observable streams
|
||||
*/
|
||||
tabs$ = this.state$.pipe(map(state => state.tabs), distinctUntilChanged());
|
||||
groupedTabs$ = this.state$.pipe(map(state => state.groupedTabs), distinctUntilChanged());
|
||||
modules$ = this.state$.pipe(map(state => state.modules), distinctUntilChanged());
|
||||
userActionMenu$ = this.state$.pipe(map(state => state.userActionMenu), distinctUntilChanged());
|
||||
|
||||
constructor(private recordGQL: RecordGQL) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Public Api
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Initial Navigation load if not cached and update state.
|
||||
* Returns observable to be used in resolver if needed
|
||||
*
|
||||
* @returns Observable<any>
|
||||
*/
|
||||
public load(): Observable<any> {
|
||||
|
||||
return this.getNavigation().pipe(
|
||||
tap(navigation => {
|
||||
this.updateState({
|
||||
...internalState,
|
||||
tabs: navigation.tabs,
|
||||
groupedTabs: navigation.groupedTabs,
|
||||
userActionMenu: navigation.userActionMenu,
|
||||
modules: navigation.modules
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update the state
|
||||
*
|
||||
* @param state
|
||||
*/
|
||||
protected updateState(state: Navigation) {
|
||||
this.store.next(internalState = state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Navigation cached Observable or call the backend
|
||||
*
|
||||
* @return Observable<any>
|
||||
*/
|
||||
protected getNavigation(): Observable<any> {
|
||||
|
||||
const user = '1';
|
||||
|
||||
if (cache$ == null) {
|
||||
cache$ = this.fetch(user).pipe(
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
return cache$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the Navigation from the backend
|
||||
*
|
||||
* @returns Observable<any>
|
||||
*/
|
||||
protected fetch(userId: string): Observable<any> {
|
||||
|
||||
return this.recordGQL
|
||||
.fetch(this.resourceName, `/api/navbars/${userId}`, this.fieldsMetadata)
|
||||
.pipe(
|
||||
map(({data}) => {
|
||||
let navigation: Navigation = null;
|
||||
|
||||
if (data && data.navbar) {
|
||||
navigation = {
|
||||
tabs: data.navbar.tabs,
|
||||
groupedTabs: data.navbar.groupedTabs,
|
||||
userActionMenu: data.navbar.userActionMenu,
|
||||
modules: data.navbar.modules
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return navigation;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import {Observable, of} from 'rxjs';
|
||||
import {shareReplay} from 'rxjs/operators';
|
||||
import {SystemConfigFacade} from '@base/facades/system-config/system-config.facade';
|
||||
import {CollectionGQL} from '@services/api/graphql-api/api.collection.get';
|
||||
|
||||
export const systemConfigMockData = {
|
||||
systemConfigs: {
|
||||
default_language: {
|
||||
id: '/docroot/api/system-configs/default_language',
|
||||
_id: 'default_language',
|
||||
value: 'en_us',
|
||||
items: []
|
||||
},
|
||||
passwordsetting: {
|
||||
id: '/docroot/api/system-configs/passwordsetting',
|
||||
_id: 'passwordsetting',
|
||||
value: null,
|
||||
items: {
|
||||
forgotpasswordON: false
|
||||
}
|
||||
},
|
||||
languages: {
|
||||
id: '/docroot/api/system-configs/languages',
|
||||
_id: 'languages',
|
||||
value: null,
|
||||
items: {
|
||||
en_us: 'English (US)',
|
||||
pt_PT: 'Português (Portugal) - pt-PT'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class SystemConfigRecordGQLSpy extends CollectionGQL {
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public fetchAll(module: string, metadata: { fields: string[] }): Observable<any> {
|
||||
const data = {
|
||||
data: {
|
||||
systemConfigs: {
|
||||
edges: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(systemConfigMockData.systemConfigs).forEach(key => {
|
||||
data.data.systemConfigs.edges.push({
|
||||
node: systemConfigMockData.systemConfigs[key]
|
||||
});
|
||||
})
|
||||
|
||||
return of(data).pipe(shareReplay());
|
||||
}
|
||||
}
|
||||
|
||||
export const systemConfigFacadeMock = new SystemConfigFacade(new SystemConfigRecordGQLSpy());
|
|
@ -0,0 +1,24 @@
|
|||
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||
import {SystemConfigFacade} from '@base/facades/system-config/system-config.facade';
|
||||
import {systemConfigFacadeMock, systemConfigMockData} from '@base/facades/system-config/system-config.facade.spec.mock';
|
||||
|
||||
describe('SystemConfig Facade', () => {
|
||||
let injector: TestBed;
|
||||
const service: SystemConfigFacade = systemConfigFacadeMock;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
injector = getTestBed();
|
||||
});
|
||||
|
||||
it('#load',
|
||||
(done: DoneFn) => {
|
||||
|
||||
service.load().subscribe(data => {
|
||||
expect(data).toEqual(jasmine.objectContaining(systemConfigMockData.systemConfigs));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
|
|||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {map, distinctUntilChanged, tap, shareReplay} from 'rxjs/operators';
|
||||
|
||||
import {CollectionGQL} from '../../api/graphql-api/api.collection.get';
|
||||
import {CollectionGQL} from '@services/api/graphql-api/api.collection.get';
|
||||
|
||||
export interface SystemConfig {
|
||||
id: string;
|
|
@ -5,20 +5,127 @@ import {
|
|||
RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
|
||||
import {SystemConfigFacade} from '@services/metadata/configs/system-config.facade';
|
||||
import {LanguageFacade} from '@base/facades/language.facade';
|
||||
import {flatMap} from 'rxjs/operators';
|
||||
import {SystemConfigFacade} from '@base/facades/system-config/system-config.facade';
|
||||
import {LanguageFacade} from '@base/facades/language/language.facade';
|
||||
import {concatAll, first, flatMap, map, mergeMap, tap, toArray} from 'rxjs/operators';
|
||||
import {forkJoin, Observable} from 'rxjs';
|
||||
import {NavigationFacade} from '@base/facades/navigation/navigation.facade';
|
||||
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class BaseMetadataResolver implements Resolve<any> {
|
||||
|
||||
constructor(private systemConfigFacade: SystemConfigFacade, private languageFacade: LanguageFacade) {
|
||||
constructor(private systemConfigFacade: SystemConfigFacade,
|
||||
private languageFacade: LanguageFacade,
|
||||
private navigationFacade: NavigationFacade) {
|
||||
}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
return this.systemConfigFacade.load().pipe(
|
||||
flatMap(configs => this.languageFacade.loadAppStrings(configs['default_language'].value))
|
||||
);
|
||||
const streams$: { [key: string]: Observable<any> } = {};
|
||||
|
||||
if (this.isToLoadNavigation(route)) {
|
||||
streams$.navigation = this.navigationFacade.load();
|
||||
}
|
||||
|
||||
if (this.isToLoadConfigs(route)) {
|
||||
|
||||
let configs$ = this.systemConfigFacade.load();
|
||||
|
||||
if (this.isToLoadLanguageStrings(route)) {
|
||||
const langStrings = this.getLanguagesToLoad(route);
|
||||
|
||||
configs$ = configs$.pipe(
|
||||
map(
|
||||
configs => {
|
||||
|
||||
let language = configs.default_language.value;
|
||||
|
||||
if (this.languageFacade.hasLanguageChanged()) {
|
||||
language = this.languageFacade.getCurrentLanguage();
|
||||
}
|
||||
|
||||
return this.languageFacade.load(language, langStrings);
|
||||
},
|
||||
),
|
||||
concatAll(),
|
||||
toArray()
|
||||
);
|
||||
}
|
||||
|
||||
streams$.configs = configs$;
|
||||
}
|
||||
|
||||
|
||||
return forkJoin(streams$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Languages to Load
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
protected getLanguagesToLoad(route: ActivatedRouteSnapshot): string[] {
|
||||
let langStrings: string[] = this.languageFacade.getAvailableStringsTypes();
|
||||
|
||||
if (this.isToLoadNavigation(route)) {
|
||||
return langStrings;
|
||||
}
|
||||
|
||||
if (!route.data || !route.data.load) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(route.data.load.languageStrings)) {
|
||||
langStrings = route.data.load.languageStrings;
|
||||
}
|
||||
|
||||
return langStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should load language strings. True if navigation is to load
|
||||
*
|
||||
* @param route
|
||||
* @returns boolean
|
||||
*/
|
||||
protected isToLoadLanguageStrings(route: ActivatedRouteSnapshot): boolean {
|
||||
|
||||
if (this.isToLoadNavigation(route)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!route.data || !route.data.load) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Array.isArray(route.data.load.languageStrings) || route.data.load.languageStrings === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should load navigation. If not set defaults to true
|
||||
*
|
||||
* @param route
|
||||
* @returns boolean
|
||||
*/
|
||||
protected isToLoadConfigs(route: ActivatedRouteSnapshot): boolean {
|
||||
if (!route.data || !route.data.load) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return route.data.load.configs !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should load navigation, If not set defaults to true
|
||||
*
|
||||
* @param route
|
||||
* @returns boolean
|
||||
*/
|
||||
protected isToLoadNavigation(route: ActivatedRouteSnapshot): boolean {
|
||||
if (!route.data || !route.data.load) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return route.data.load.navigation !== false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
|
||||
import { NavigationMetadata } from './navigation/navigation.metadata.service';
|
||||
|
||||
|
||||
/**
|
||||
* Facade like service to retrieve metadata
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Metadata {
|
||||
constructor(private navigationMetadata: NavigationMetadata) {}
|
||||
|
||||
public getNavigation() {
|
||||
return this.navigationMetadata.fetch();
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import {async, ComponentFixture, getTestBed, inject, TestBed} from '@angular/core/testing';
|
||||
import {Metadata} from '@services/metadata/metadata.service';
|
||||
import {NavigationMetadata} from '@services/metadata/navigation/navigation.metadata.service';
|
||||
import {of} from 'rxjs';
|
||||
|
||||
describe('Metadata Service', () => {
|
||||
let injector: TestBed;
|
||||
let service: Metadata;
|
||||
|
||||
describe('Navigation Metadata', () => {
|
||||
|
||||
const navigationMockData = {
|
||||
navbar: {
|
||||
NonGroupedTabs: {
|
||||
Contacts: 'Contacts',
|
||||
Accounts: 'Accounts'
|
||||
},
|
||||
groupedTabs: {
|
||||
Sales: {
|
||||
modules: {
|
||||
Accounts: 'Accounts',
|
||||
Contacts: 'Contacts',
|
||||
}
|
||||
},
|
||||
},
|
||||
userActionMenu: [
|
||||
{
|
||||
label: 'Profile',
|
||||
url: 'index.php?module=Users&action=EditView&record=1',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
label: 'Employees',
|
||||
url: 'index.php?module=Employees&action=index',
|
||||
submenu: []
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
const navigationMetadata = ({
|
||||
fetch() {
|
||||
return of(navigationMockData.navbar);
|
||||
}
|
||||
} as any) as NavigationMetadata;
|
||||
*/
|
||||
|
||||
// Create a fake TwainService object with a `getQuote()` spy
|
||||
const navigationMetadata = jasmine.createSpyObj('NavigationMetadata', ['fetch']);
|
||||
// Make the spy return a synchronous Observable with the test data
|
||||
navigationMetadata.fetch.and.returnValue( of(navigationMockData.navbar) );
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: NavigationMetadata, useValue: navigationMetadata}],
|
||||
});
|
||||
|
||||
injector = getTestBed();
|
||||
service = injector.get(Metadata);
|
||||
});
|
||||
|
||||
|
||||
it('#getNavigation', (done: DoneFn) => {
|
||||
service.getNavigation().subscribe(data => {
|
||||
expect(data).toEqual(jasmine.objectContaining(navigationMockData.navbar));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import { RecordGQL } from '../../api/graphql-api/api.record.get';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NavigationMetadata {
|
||||
protected resourceName = 'navbar';
|
||||
protected fieldsMetadata = {
|
||||
fields: [
|
||||
'NonGroupedTabs',
|
||||
'groupedTabs',
|
||||
'userActionMenu'
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private recordGQL: RecordGQL) {}
|
||||
|
||||
public fetch(): Observable<any> {
|
||||
const id = '/api/navbars/1';
|
||||
|
||||
return this.recordGQL
|
||||
.fetch(this.resourceName, id, this.fieldsMetadata)
|
||||
.pipe(map(({data}) => data.navbar));
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
import {getTestBed, TestBed} from '@angular/core/testing';
|
||||
import {
|
||||
ApolloTestingModule,
|
||||
ApolloTestingController,
|
||||
} from 'apollo-angular/testing';
|
||||
import gql from 'graphql-tag';
|
||||
import {NavigationMetadata} from '@services/metadata/navigation/navigation.metadata.service';
|
||||
|
||||
describe('Navigation Metadata Service', () => {
|
||||
let injector: TestBed;
|
||||
let service: NavigationMetadata;
|
||||
let controller: ApolloTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ApolloTestingModule],
|
||||
});
|
||||
|
||||
injector = getTestBed();
|
||||
controller = injector.get(ApolloTestingController);
|
||||
service = injector.get(NavigationMetadata);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
controller.verify();
|
||||
});
|
||||
|
||||
it('#fetch',
|
||||
(done: DoneFn) => {
|
||||
|
||||
const navigationMockData = {
|
||||
navbar: {
|
||||
NonGroupedTabs: {
|
||||
Contacts: 'Contacts',
|
||||
Accounts: 'Accounts'
|
||||
},
|
||||
groupedTabs: {
|
||||
Sales: {
|
||||
modules: {
|
||||
Accounts: 'Accounts',
|
||||
Contacts: 'Contacts',
|
||||
}
|
||||
},
|
||||
},
|
||||
userActionMenu: [
|
||||
{
|
||||
label: 'Profile',
|
||||
url: 'index.php?module=Users&action=EditView&record=1',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
label: 'Employees',
|
||||
url: 'index.php?module=Employees&action=index',
|
||||
submenu: []
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
service.fetch().subscribe(data => {
|
||||
expect(data).toEqual(jasmine.objectContaining(navigationMockData.navbar));
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
const op = controller.expectOne(gql`
|
||||
query navbar($id: ID!) {
|
||||
navbar(id: $id) {
|
||||
NonGroupedTabs
|
||||
groupedTabs
|
||||
userActionMenu
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
// Assert that one of variables is Mr Apollo.
|
||||
expect(op.operation.variables.id).toEqual('/api/navbars/1');
|
||||
|
||||
// Respond with mock data, causing Observable to resolve.
|
||||
op.flush({
|
||||
data: navigationMockData
|
||||
});
|
||||
|
||||
// Finally, assert that there are no outstanding operations.
|
||||
controller.verify();
|
||||
});
|
||||
});
|
||||
|
|
@ -18,12 +18,12 @@ class AppListStringsHandler extends LegacyHandler
|
|||
*/
|
||||
public function getAppListStrings(string $language): ?AppListStrings
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (empty($language)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->init();
|
||||
|
||||
$enabledLanguages = get_languages();
|
||||
|
||||
if (empty($enabledLanguages) || !array_key_exists($language, $enabledLanguages)) {
|
||||
|
|
|
@ -34,12 +34,12 @@ class AppStringsHandler extends LegacyHandler
|
|||
*/
|
||||
public function getAppStrings(string $language): ?AppStrings
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (empty($language)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->init();
|
||||
|
||||
$enabledLanguages = get_languages();
|
||||
|
||||
if (empty($enabledLanguages) || !array_key_exists($language, $enabledLanguages)) {
|
||||
|
|
|
@ -135,7 +135,7 @@ class LegacyHandler
|
|||
/**
|
||||
* Disable legacy suite translations
|
||||
*/
|
||||
protected function disableTranslations()
|
||||
protected function disableTranslations(): void
|
||||
{
|
||||
global $sugar_config, $app_strings;
|
||||
|
||||
|
|
|
@ -43,12 +43,12 @@ class ModStringsHandler extends LegacyHandler
|
|||
*/
|
||||
public function getModStrings(string $language): ?ModStrings
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (empty($language)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->init();
|
||||
|
||||
$enabledLanguages = get_languages();
|
||||
|
||||
if (empty($enabledLanguages) || !array_key_exists($language, $enabledLanguages)) {
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
namespace SuiteCRM\Core\Legacy;
|
||||
|
||||
use App\Entity\Navbar;
|
||||
use App\Service\ModuleNameMapper;
|
||||
use App\Service\RouteConverter;
|
||||
use SugarView;
|
||||
use TabController;
|
||||
use GroupedTabStructure;
|
||||
use TabGroupHelper;
|
||||
|
@ -12,6 +15,46 @@ use TabGroupHelper;
|
|||
*/
|
||||
class NavbarHandler extends LegacyHandler
|
||||
{
|
||||
/**
|
||||
* @var ModuleNameMapper
|
||||
*/
|
||||
private $moduleNameMapper;
|
||||
|
||||
/**
|
||||
* @var RouteConverter
|
||||
*/
|
||||
private $routeConverter;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $menuItemMap;
|
||||
|
||||
/**
|
||||
* SystemConfigHandler constructor.
|
||||
* @param string $projectDir
|
||||
* @param string $legacyDir
|
||||
* @param string $legacySessionName
|
||||
* @param string $defaultSessionName
|
||||
* @param array $menuItemMap
|
||||
* @param ModuleNameMapper $moduleNameMapper
|
||||
* @param RouteConverter $routeConverter
|
||||
*/
|
||||
public function __construct(
|
||||
string $projectDir,
|
||||
string $legacyDir,
|
||||
string $legacySessionName,
|
||||
string $defaultSessionName,
|
||||
array $menuItemMap,
|
||||
ModuleNameMapper $moduleNameMapper,
|
||||
RouteConverter $routeConverter
|
||||
) {
|
||||
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName);
|
||||
$this->moduleNameMapper = $moduleNameMapper;
|
||||
$this->routeConverter = $routeConverter;
|
||||
$this->menuItemMap = $menuItemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Navbar using legacy information
|
||||
* @return Navbar
|
||||
|
@ -20,12 +63,19 @@ class NavbarHandler extends LegacyHandler
|
|||
{
|
||||
$this->init();
|
||||
|
||||
$this->disableTranslations();
|
||||
|
||||
$navbar = new Navbar();
|
||||
|
||||
$navbar->NonGroupedTabs = $this->fetchNonGroupedNavTabs();
|
||||
$sugarView = new SugarView();
|
||||
|
||||
$legacyTabNames = $this->fetchNavTabs();
|
||||
$nameMap = $this->createFrontendNameMap($legacyTabNames);
|
||||
|
||||
$navbar->tabs = array_values($nameMap);
|
||||
$navbar->modules = $this->buildModuleInfo($sugarView, $nameMap);
|
||||
$navbar->groupedTabs = $this->fetchGroupedNavTabs();
|
||||
$navbar->userActionMenu = $this->fetchUserActionMenu();
|
||||
$navbar->moduleSubmenus = $this->fetchModuleSubMenus();
|
||||
|
||||
$this->close();
|
||||
|
||||
|
@ -36,26 +86,11 @@ class NavbarHandler extends LegacyHandler
|
|||
* Fetch module navigation tabs
|
||||
* @return array
|
||||
*/
|
||||
protected function fetchNonGroupedNavTabs(): array
|
||||
protected function fetchNavTabs(): array
|
||||
{
|
||||
require_once 'modules/MySettings/TabController.php';
|
||||
$tabArray = (new TabController())->get_user_tabs($GLOBALS['current_user']);
|
||||
$tabArray = array_map('strtolower', $tabArray);
|
||||
|
||||
return $tabArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the module submenus
|
||||
* @return array
|
||||
*/
|
||||
protected function fetchModuleSubMenus(): array
|
||||
{
|
||||
ob_start();
|
||||
require_once 'modules/Home/sitemap.php';
|
||||
ob_end_clean();
|
||||
|
||||
return sm_build_array();
|
||||
return (new TabController())->get_user_tabs($GLOBALS['current_user']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,25 +106,26 @@ class NavbarHandler extends LegacyHandler
|
|||
require_once 'modules/Studio/TabGroups/TabGroupHelper.php';
|
||||
|
||||
$tg = new TabGroupHelper();
|
||||
$selectedAppLanguages = return_application_language($current_language);
|
||||
|
||||
$availableModules = $tg->getAvailableModules($current_language);
|
||||
$modList = array_keys($availableModules);
|
||||
$modList = array_combine($modList, $modList);
|
||||
$groupedTabStructure = (new GroupedTabStructure())->get_tab_structure($modList, '', true, true);
|
||||
|
||||
$moduleNameMap = $this->createFrontendNameMap($modList);
|
||||
|
||||
foreach ($groupedTabStructure as $mainTab => $subModules) {
|
||||
$groupedTabStructure[$mainTab]['label'] = $mainTab;
|
||||
$groupedTabStructure[$mainTab]['labelValue'] = strtolower($selectedAppLanguages[$mainTab]);
|
||||
$submoduleArray = [];
|
||||
|
||||
foreach ($subModules['modules'] as $submodule) {
|
||||
$submoduleArray[] = strtolower($submodule);
|
||||
foreach ($subModules['modules'] as $submodule => $submoduleLabel) {
|
||||
$submoduleArray[] = $moduleNameMap[$submodule];
|
||||
}
|
||||
|
||||
sort($submoduleArray);
|
||||
|
||||
$output[] = [
|
||||
|
||||
'name' => $groupedTabStructure[$mainTab]['labelValue'],
|
||||
'name' => $mainTab,
|
||||
'labelKey' => $mainTab,
|
||||
'modules' => array_values($submoduleArray)
|
||||
|
||||
|
@ -116,20 +152,21 @@ class NavbarHandler extends LegacyHandler
|
|||
|
||||
require 'include/globalControlLinks.php';
|
||||
|
||||
$labelKeys = [
|
||||
'employees' => 'LBL_EMPLOYEES',
|
||||
'training' => 'LBL_TRAINING',
|
||||
'about' => 'LNK_ABOUT',
|
||||
'users' => 'LBL_LOGOUT',
|
||||
$actionLabelMap = [
|
||||
'LBL_EMPLOYEES' => 'employees',
|
||||
'LBL_TRAINING' => 'training',
|
||||
'LNK_ABOUT' => 'about',
|
||||
'LBL_LOGOUT' => 'logout'
|
||||
];
|
||||
|
||||
|
||||
foreach ($global_control_links as $key => $value) {
|
||||
foreach ($value as $linkAttribute => $attributeValue) {
|
||||
// get the main link info
|
||||
if ($linkAttribute === 'linkinfo') {
|
||||
$userActionMenu[] = [
|
||||
'name' => strtolower(key($attributeValue)),
|
||||
'labelKey' => $labelKeys[$key],
|
||||
'name' => $actionLabelMap[key($attributeValue)],
|
||||
'labelKey' => key($attributeValue),
|
||||
'url' => current($attributeValue),
|
||||
'icon' => '',
|
||||
];
|
||||
|
@ -143,4 +180,109 @@ class NavbarHandler extends LegacyHandler
|
|||
|
||||
return array_values($userActionMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SugarView $sugarView
|
||||
* @param array $legacyNameMap
|
||||
* @return array
|
||||
*/
|
||||
protected function buildModuleInfo(SugarView $sugarView, array $legacyNameMap): array
|
||||
{
|
||||
$modules = [];
|
||||
|
||||
foreach ($legacyNameMap as $legacyName => $frontendName) {
|
||||
$menu = $this->buildSubModule($sugarView, $legacyName, $frontendName);
|
||||
$modules[$frontendName] = [
|
||||
'path' => $frontendName,
|
||||
'defaultRoute' => "./#/$frontendName/index",
|
||||
'name' => $frontendName,
|
||||
'labelKey' => $legacyName,
|
||||
'menu' => $menu
|
||||
];
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve legacy menu information and build suite 8 entry
|
||||
* @param SugarView $sugarView
|
||||
* @param string $legacyModule
|
||||
* @param string $frontendModule
|
||||
* @return array
|
||||
*/
|
||||
protected function buildSubModule(SugarView $sugarView, string $legacyModule, string $frontendModule): array
|
||||
{
|
||||
$subMenu = [];
|
||||
$legacyMenuItems = $sugarView->getMenu($legacyModule);
|
||||
|
||||
foreach ($legacyMenuItems as $legacyMenuItem) {
|
||||
|
||||
[$url, $label, $action] = $legacyMenuItem;
|
||||
|
||||
$routeInfo = $this->routeConverter->parseUri($url);
|
||||
|
||||
$subMenu[] = [
|
||||
'name' => $action,
|
||||
'labelKey' => $this->mapEntry($frontendModule, $action, 'labelKey', $label),
|
||||
'url' => $this->mapEntry($frontendModule, $action, 'url', $routeInfo['route']),
|
||||
'params' => $routeInfo['params'],
|
||||
'icon' => $this->mapEntry($frontendModule, $action, 'icon', '')
|
||||
];
|
||||
}
|
||||
|
||||
return $subMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map entry if defined on the configuration
|
||||
* @param string $moduleName
|
||||
* @param string $action
|
||||
* @param string $entry
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
protected function mapEntry(string $moduleName, string $action, string $entry, string $default): string
|
||||
{
|
||||
$module = $moduleName;
|
||||
if ($this->isEntryMapped($action, $entry, $module)) {
|
||||
$module = 'default';
|
||||
}
|
||||
|
||||
if ($this->isEntryMapped($action, $entry, $module)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->menuItemMap[$module][$action][$entry];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an entry mapped for the given module
|
||||
* @param string $action
|
||||
* @param string $entry
|
||||
* @param string $module
|
||||
* @return bool
|
||||
*/
|
||||
protected function isEntryMapped(string $action, string $entry, string $module): bool
|
||||
{
|
||||
return empty($this->menuItemMap[$module]) ||
|
||||
empty($this->menuItemMap[$module][$action]) ||
|
||||
empty($this->menuItemMap[$module][$action][$entry]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map legacy names to front end names
|
||||
* @param array $legacyTabNames
|
||||
* @return array
|
||||
*/
|
||||
protected function createFrontendNameMap(array $legacyTabNames): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($legacyTabNames as $legacyTabName) {
|
||||
$map[$legacyTabName] = $this->moduleNameMapper->toFrontEnd($legacyTabName);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ use ApiPlatform\Core\Annotation\ApiProperty;
|
|||
* "get"
|
||||
* },
|
||||
* collectionOperations={
|
||||
* },
|
||||
* graphql={
|
||||
* "item_query"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
@ -26,7 +29,7 @@ final class Navbar
|
|||
* @var array
|
||||
* @ApiProperty
|
||||
*/
|
||||
public $NonGroupedTabs;
|
||||
public $tabs;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
|
@ -44,5 +47,5 @@ final class Navbar
|
|||
* @var array
|
||||
* @ApiProperty
|
||||
*/
|
||||
public $moduleSubmenus;
|
||||
public $modules;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ class RouteConverter
|
|||
*/
|
||||
public function isLegacyRoute(Request $request): bool
|
||||
{
|
||||
if (!empty($request->getPathInfo()) && $request->getPathInfo() !== '/'){
|
||||
if (!empty($request->getPathInfo()) && $request->getPathInfo() !== '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ class RouteConverter
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!$this->moduleNameMapper->isValidModule($module)){
|
||||
if (!$this->moduleNameMapper->isValidModule($module)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -83,15 +83,52 @@ class RouteConverter
|
|||
$route = $this->buildRoute($module, $action, $record);
|
||||
|
||||
if (null !== $queryString = $request->getQueryString()) {
|
||||
$queryString = $this->removeParameter($queryString, 'module', $module);
|
||||
$queryString = $this->removeParameter($queryString, 'action', $action);
|
||||
$queryString = $this->removeParameter($queryString, 'record', $record);
|
||||
$queryString = '?' . Request::normalizeQueryString($queryString);
|
||||
$queryParams = $request->query->all();
|
||||
$queryString = '?' . $this->buildQueryString($queryParams, ['module', 'action', 'record']);
|
||||
}
|
||||
|
||||
return $route . $queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert given $uri route
|
||||
*
|
||||
* @param string $uri
|
||||
* @return string
|
||||
*/
|
||||
public function convertUri(string $uri): string
|
||||
{
|
||||
$request = Request::create($uri);
|
||||
|
||||
return $this->convert($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse given $uri route
|
||||
*
|
||||
* @param string $uri
|
||||
* @return array
|
||||
*/
|
||||
public function parseUri(string $uri): array
|
||||
{
|
||||
$request = Request::create($uri);
|
||||
|
||||
$module = $request->query->get('module');
|
||||
$action = $request->query->get('action');
|
||||
$record = $request->query->get('record');
|
||||
|
||||
if (empty($module)) {
|
||||
throw new InvalidArgumentException('No module defined');
|
||||
}
|
||||
|
||||
$route = $this->buildRoute($module, $action, $record);
|
||||
|
||||
return [
|
||||
'route' => $route,
|
||||
'params' => $this->excludeParams($request->query->all(), ['module', 'action', 'record'])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given $action is valid
|
||||
* @param string|null $action
|
||||
|
@ -114,7 +151,8 @@ class RouteConverter
|
|||
* Get map of valid action
|
||||
* @return array
|
||||
*/
|
||||
protected function getValidActions(): array {
|
||||
protected function getValidActions(): array
|
||||
{
|
||||
return $this->map;
|
||||
}
|
||||
|
||||
|
@ -170,17 +208,35 @@ class RouteConverter
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string|null $queryString
|
||||
* @param string|null $param
|
||||
* @param string|null $value
|
||||
* @return string|string[]|null
|
||||
* Build query string
|
||||
* @param array $queryParams
|
||||
* @param array $exclude
|
||||
* @return string
|
||||
*/
|
||||
protected function removeParameter(?string $queryString, ?string $param, ?string $value)
|
||||
protected function buildQueryString(array $queryParams, array $exclude): string
|
||||
{
|
||||
if (empty($value) || empty($param) || empty($queryString)) {
|
||||
return $queryString;
|
||||
$validParams = $this->excludeParams($queryParams, $exclude);
|
||||
|
||||
return Request::normalizeQueryString(http_build_query($validParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build new array where list of query params are excluded
|
||||
* @param array $queryParams
|
||||
* @param array $exclude
|
||||
* @return array
|
||||
*/
|
||||
protected function excludeParams(array $queryParams, array $exclude): array
|
||||
{
|
||||
$validParams = [];
|
||||
|
||||
foreach ($queryParams as $name => $value) {
|
||||
if (in_array($name, $exclude)) {
|
||||
continue;
|
||||
}
|
||||
$validParams[$name] = $value;
|
||||
}
|
||||
|
||||
return str_replace("$param=$value", '', $queryString);;
|
||||
return $validParams;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use ApiPlatform\Core\Exception\ItemNotFoundException;
|
||||
use App\Entity\ModStrings;
|
||||
use App\Service\ModuleNameMapper;
|
||||
use Codeception\Test\Unit;
|
||||
use SuiteCRM\Core\Legacy\ModStringsHandler;
|
||||
|
||||
|
@ -19,11 +20,345 @@ class ModStringsHandlerTest extends Unit
|
|||
|
||||
protected function _before()
|
||||
{
|
||||
$legacyModuleNameMap = [
|
||||
'Home' => [
|
||||
'frontend' => 'home',
|
||||
'core' => 'Home',
|
||||
],
|
||||
'Calendar' => [
|
||||
'frontend' => 'calendar',
|
||||
'core' => 'Calendar',
|
||||
],
|
||||
'Calls' => [
|
||||
'frontend' => 'calls',
|
||||
'core' => 'Calls',
|
||||
],
|
||||
'Calls_Reschedule' => [
|
||||
'frontend' => 'calls-reschedule',
|
||||
'core' => 'CallsReschedule',
|
||||
],
|
||||
'Meetings' => [
|
||||
'frontend' => 'meetings',
|
||||
'core' => 'Meetings',
|
||||
],
|
||||
'Tasks' => [
|
||||
'frontend' => 'tasks',
|
||||
'core' => 'Tasks',
|
||||
],
|
||||
'Notes' => [
|
||||
'frontend' => 'notes',
|
||||
'core' => 'Notes',
|
||||
],
|
||||
'Leads' => [
|
||||
'frontend' => 'leads',
|
||||
'core' => 'Leads',
|
||||
],
|
||||
'Contacts' => [
|
||||
'frontend' => 'contacts',
|
||||
'core' => 'Contacts',
|
||||
],
|
||||
'Accounts' => [
|
||||
'frontend' => 'accounts',
|
||||
'core' => 'Accounts',
|
||||
],
|
||||
'Opportunities' => [
|
||||
'frontend' => 'opportunities',
|
||||
'core' => 'Opportunities',
|
||||
],
|
||||
'Import' => [
|
||||
'frontend' => 'import',
|
||||
'core' => 'Import',
|
||||
],
|
||||
'Emails' => [
|
||||
'frontend' => 'emails',
|
||||
'core' => 'Emails',
|
||||
],
|
||||
'EmailTemplates' => [
|
||||
'frontend' => 'email-templates',
|
||||
'core' => 'EmailTemplates',
|
||||
],
|
||||
'Campaigns' => [
|
||||
'frontend' => 'campaigns',
|
||||
'core' => 'Campaigns',
|
||||
],
|
||||
'Targets' => [
|
||||
'frontend' => 'targets',
|
||||
'core' => 'Targets',
|
||||
],
|
||||
'Targets - Lists' => [
|
||||
'frontend' => 'prospect-lists',
|
||||
'core' => 'ProspectLists',
|
||||
],
|
||||
'Prospects' => [
|
||||
'frontend' => 'prospects',
|
||||
'core' => 'Prospects',
|
||||
],
|
||||
'ProspectLists' => [
|
||||
'frontend' => 'prospect-lists',
|
||||
'core' => 'ProspectLists',
|
||||
],
|
||||
'Documents' => [
|
||||
'frontend' => 'documents',
|
||||
'core' => 'Documents',
|
||||
],
|
||||
'Cases' => [
|
||||
'frontend' => 'cases',
|
||||
'core' => 'Cases',
|
||||
],
|
||||
'Project' => [
|
||||
'frontend' => 'project',
|
||||
'core' => 'Project',
|
||||
],
|
||||
'ProjectTask' => [
|
||||
'frontend' => 'project-task',
|
||||
'core' => 'ProjectTask',
|
||||
],
|
||||
'Bugs' => [
|
||||
'frontend' => 'bugs',
|
||||
'core' => 'Bugs',
|
||||
],
|
||||
'ResourceCalendar' => [
|
||||
'frontend' => 'resource-calendar',
|
||||
'core' => 'ResourceCalendar',
|
||||
],
|
||||
'AOBH_BusinessHours' => [
|
||||
'frontend' => 'business-hours',
|
||||
'core' => 'BusinessHours',
|
||||
],
|
||||
'Spots' => [
|
||||
'frontend' => 'spots',
|
||||
'core' => 'Spots',
|
||||
],
|
||||
'SecurityGroups' => [
|
||||
'frontend' => 'security-groups',
|
||||
'core' => 'SecurityGroups',
|
||||
],
|
||||
'ACL' => [
|
||||
'frontend' => 'acl',
|
||||
'core' => 'ACL',
|
||||
],
|
||||
'ACLRoles' => [
|
||||
'frontend' => 'acl-roles',
|
||||
'core' => 'ACLRoles',
|
||||
],
|
||||
'Configurator' => [
|
||||
'frontend' => 'configurator',
|
||||
'core' => 'Configurator',
|
||||
],
|
||||
'UserPreferences' => [
|
||||
'frontend' => 'user-preferences',
|
||||
'core' => 'UserPreferences',
|
||||
],
|
||||
'SavedSearch' => [
|
||||
'frontend' => 'saved-search',
|
||||
'core' => 'SavedSearch',
|
||||
],
|
||||
'Studio' => [
|
||||
'frontend' => 'studio',
|
||||
'core' => 'Studio',
|
||||
],
|
||||
'Connectors' => [
|
||||
'frontend' => 'connectors',
|
||||
'core' => 'Connectors',
|
||||
],
|
||||
'SugarFeed' => [
|
||||
'frontend' => 'sugar-feed',
|
||||
'core' => 'SugarFeed',
|
||||
],
|
||||
'EAPM' => [
|
||||
'frontend' => 'eapm',
|
||||
'core' => 'EAPM',
|
||||
],
|
||||
'OutboundEmailAccounts' => [
|
||||
'frontend' => 'outbound-email-accounts',
|
||||
'core' => 'OutboundEmailAccounts',
|
||||
],
|
||||
'TemplateSectionLine' => [
|
||||
'frontend' => 'template-section-line',
|
||||
'core' => 'TemplateSectionLine',
|
||||
],
|
||||
'OAuthKeys' => [
|
||||
'frontend' => 'oauth-keys',
|
||||
'core' => 'OAuthKeys',
|
||||
],
|
||||
'OAuthTokens' => [
|
||||
'frontend' => 'oauth-tokens',
|
||||
'core' => 'OAuthTokens',
|
||||
],
|
||||
'OAuth2Tokens' => [
|
||||
'frontend' => 'oauth2-tokens',
|
||||
'core' => 'OAuth2Tokens',
|
||||
],
|
||||
'OAuth2Clients' => [
|
||||
'frontend' => 'oauth2-clients',
|
||||
'core' => 'OAuth2Clients',
|
||||
],
|
||||
'Surveys' => [
|
||||
'frontend' => 'surveys',
|
||||
'core' => 'Surveys',
|
||||
],
|
||||
'SurveyResponses' => [
|
||||
'frontend' => 'survey-responses',
|
||||
'core' => 'SurveyResponses',
|
||||
],
|
||||
'SurveyQuestionResponses' => [
|
||||
'frontend' => 'survey-question-responses',
|
||||
'core' => 'SurveyQuestionResponses',
|
||||
],
|
||||
'SurveyQuestions' => [
|
||||
'frontend' => 'survey-questions',
|
||||
'core' => 'SurveyQuestions',
|
||||
],
|
||||
'SurveyQuestionOptions' => [
|
||||
'frontend' => 'survey-question-options',
|
||||
'core' => 'SurveyQuestionOptions',
|
||||
],
|
||||
'Reminders' => [
|
||||
'frontend' => 'reminders',
|
||||
'core' => 'Reminders',
|
||||
],
|
||||
'Reminders_Invitees' => [
|
||||
'frontend' => 'reminders-invitees',
|
||||
'core' => 'RemindersInvitees',
|
||||
],
|
||||
'AM_ProjectTemplates' => [
|
||||
'frontend' => 'project-templates',
|
||||
'core' => 'ProjectTemplates',
|
||||
],
|
||||
'AM_TaskTemplates' => [
|
||||
'frontend' => 'task-templates',
|
||||
'core' => 'TaskTemplates',
|
||||
],
|
||||
'AOK_Knowledge_Base_Categories' => [
|
||||
'frontend' => 'knowledge-base-categories',
|
||||
'core' => 'KnowledgeBaseCategories',
|
||||
],
|
||||
'AOK_KnowledgeBase' => [
|
||||
'frontend' => 'knowledge-base',
|
||||
'core' => 'KnowledgeBase',
|
||||
],
|
||||
'FP_events' => [
|
||||
'frontend' => 'events',
|
||||
'core' => 'Events',
|
||||
],
|
||||
'FP_Event_Locations' => [
|
||||
'frontend' => 'event-locations',
|
||||
'core' => 'EventLocations',
|
||||
],
|
||||
'AOS_Contracts' => [
|
||||
'frontend' => 'contracts',
|
||||
'core' => 'Contracts',
|
||||
],
|
||||
'AOS_Invoices' => [
|
||||
'frontend' => 'invoices',
|
||||
'core' => 'Invoices',
|
||||
],
|
||||
'AOS_PDF_Templates' => [
|
||||
'frontend' => 'pdf-templates',
|
||||
'core' => 'PDFTemplates',
|
||||
],
|
||||
'AOS_Product_Categories' => [
|
||||
'frontend' => 'product-categories',
|
||||
'core' => 'ProductCategories',
|
||||
],
|
||||
'AOS_Products' => [
|
||||
'frontend' => 'products',
|
||||
'core' => 'Products',
|
||||
],
|
||||
'AOS_Quotes' => [
|
||||
'frontend' => 'quotes',
|
||||
'core' => 'Quotes',
|
||||
],
|
||||
'AOS_Products_Quotes' => [
|
||||
'frontend' => 'products-quotes',
|
||||
'core' => 'ProductsQuotes',
|
||||
],
|
||||
'AOS_Line_Item_Groups' => [
|
||||
'frontend' => 'line-item-groups',
|
||||
'core' => 'LineItemGroups',
|
||||
],
|
||||
'jjwg_Maps' => [
|
||||
'frontend' => 'maps',
|
||||
'core' => 'Maps',
|
||||
],
|
||||
'jjwg_Markers' => [
|
||||
'frontend' => 'markers',
|
||||
'core' => 'Markers',
|
||||
],
|
||||
'jjwg_Areas' => [
|
||||
'frontend' => 'areas',
|
||||
'core' => 'Areas',
|
||||
],
|
||||
'jjwg_Address_Cache' => [
|
||||
'frontend' => 'address-cache',
|
||||
'core' => 'AddressCache',
|
||||
],
|
||||
'AOD_IndexEvent' => [
|
||||
'frontend' => 'index-event',
|
||||
'core' => 'IndexEvent',
|
||||
],
|
||||
'AOD_Index' => [
|
||||
'frontend' => 'index',
|
||||
'core' => 'index',
|
||||
],
|
||||
'AOP_Case_Events' => [
|
||||
'frontend' => 'case-events',
|
||||
'core' => 'CaseEvents',
|
||||
],
|
||||
'AOP_Case_Updates' => [
|
||||
'frontend' => 'case-updates',
|
||||
'core' => 'CaseUpdates',
|
||||
],
|
||||
'AOR_Reports' => [
|
||||
'frontend' => 'reports',
|
||||
'core' => 'Reports',
|
||||
],
|
||||
'AOR_Scheduled_Reports' => [
|
||||
'frontend' => 'scheduled-reports',
|
||||
'core' => 'ScheduledReports',
|
||||
],
|
||||
'AOR_Fields' => [
|
||||
'frontend' => 'report-fields',
|
||||
'core' => 'ReportFields',
|
||||
],
|
||||
'AOR_Charts' => [
|
||||
'frontend' => 'report-charts',
|
||||
'core' => 'ReportCharts',
|
||||
],
|
||||
'AOR_Conditions' => [
|
||||
'frontend' => 'report-conditions',
|
||||
'core' => 'ReportConditions',
|
||||
],
|
||||
'AOW_WorkFlow' => [
|
||||
'frontend' => 'workFlow',
|
||||
'core' => 'WorkFlow',
|
||||
],
|
||||
'AOW_Actions' => [
|
||||
'frontend' => 'workflow-actions',
|
||||
'core' => 'WorkflowActions',
|
||||
],
|
||||
'AOW_Processed' => [
|
||||
'frontend' => 'workflow-processed',
|
||||
'core' => 'WorflowProcessed',
|
||||
],
|
||||
'AOW_Conditions' => [
|
||||
'frontend' => 'workflow-conditions',
|
||||
'core' => 'WorkflowConditions',
|
||||
],
|
||||
];
|
||||
$moduleNameMapper = new ModuleNameMapper($legacyModuleNameMap);
|
||||
|
||||
$projectDir = codecept_root_dir();
|
||||
$legacyDir = $projectDir . '/legacy';
|
||||
$legacySessionName = 'LEGACYSESSID';
|
||||
$defaultSessionName = 'PHPSESSID';
|
||||
$this->handler = new ModStringsHandler($projectDir, $legacyDir, $legacySessionName, $defaultSessionName);
|
||||
$this->handler = new ModStringsHandler(
|
||||
$projectDir,
|
||||
$legacyDir,
|
||||
$legacySessionName,
|
||||
$defaultSessionName,
|
||||
$moduleNameMapper
|
||||
);
|
||||
}
|
||||
|
||||
// tests
|
||||
|
@ -46,9 +381,9 @@ class ModStringsHandlerTest extends Unit
|
|||
static::assertNotNull($modStrings);
|
||||
static::assertEquals('en_us', $modStrings->getId());
|
||||
static::assertIsArray($modStrings->getItems());
|
||||
$this->assertLanguageKey('Home', 'LBL_MODULE_NAME', $modStrings);
|
||||
$this->assertLanguageKey('Accounts', 'LNK_ACCOUNT_LIST', $modStrings);
|
||||
$this->assertLanguageKey('Accounts', 'LNK_NEW_ACCOUNT', $modStrings);
|
||||
$this->assertLanguageKey('home', 'LBL_MODULE_NAME', $modStrings);
|
||||
$this->assertLanguageKey('accounts', 'LNK_ACCOUNT_LIST', $modStrings);
|
||||
$this->assertLanguageKey('accounts', 'LNK_NEW_ACCOUNT', $modStrings);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue