Browse Source

feat(admin-ui): Implement Role list/detail routes

Michael Bromley 7 years ago
parent
commit
1217da4fe8

+ 5 - 1
admin-ui/src/app/administrator/administrator.module.ts

@@ -5,9 +5,13 @@ import { SharedModule } from '../shared/shared.module';
 
 import { administratorRoutes } from './administrator.routes';
 import { AdministratorListComponent } from './components/administrator-list/administrator-list.component';
+import { RoleDetailComponent } from './components/role-detail/role-detail.component';
+import { RoleListComponent } from './components/role-list/role-list.component';
+import { RoleResolver } from './providers/routing/role-resolver';
 
 @NgModule({
     imports: [SharedModule, RouterModule.forChild(administratorRoutes)],
-    declarations: [AdministratorListComponent],
+    declarations: [AdministratorListComponent, RoleListComponent, RoleDetailComponent],
+    providers: [RoleResolver],
 })
 export class AdministratorModule {}

+ 29 - 0
admin-ui/src/app/administrator/administrator.routes.ts

@@ -1,8 +1,14 @@
 import { Route } from '@angular/router';
+import { Role } from 'shared/generated-types';
 
+import { createResolveData } from '../common/base-entity-resolver';
+import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
 
 import { AdministratorListComponent } from './components/administrator-list/administrator-list.component';
+import { RoleDetailComponent } from './components/role-detail/role-detail.component';
+import { RoleListComponent } from './components/role-list/role-list.component';
+import { RoleResolver } from './providers/routing/role-resolver';
 
 export const administratorRoutes: Route[] = [
     {
@@ -12,4 +18,27 @@ export const administratorRoutes: Route[] = [
             breadcrumb: _('breadcrumb.administrators'),
         },
     },
+    {
+        path: 'roles',
+        component: RoleListComponent,
+        data: {
+            breadcrumb: _('breadcrumb.roles'),
+        },
+    },
+    {
+        path: 'roles/:id',
+        component: RoleDetailComponent,
+        resolve: createResolveData(RoleResolver),
+        data: { breadcrumb: roleBreadcrumb },
+    },
 ];
+
+export function roleBreadcrumb(data: any, params: any) {
+    return detailBreadcrumb<Role>({
+        entity: data.entity,
+        id: params.id,
+        breadcrumbKey: 'breadcrumb.roles',
+        getName: product => product.description,
+        route: 'roles',
+    });
+}

+ 5 - 5
admin-ui/src/app/administrator/components/administrator-list/administrator-list.component.html

@@ -2,7 +2,7 @@
     <vdr-ab-right>
         <a class="btn btn-primary" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
-            {{ 'catalog.create-new-administrator' | translate }}
+            {{ 'admin.create-new-administrator' | translate }}
         </a>
     </vdr-ab-right>
 </vdr-action-bar>
@@ -13,10 +13,10 @@
                 [currentPage]="currentPage$ | async"
                 (pageChange)="setPageNumber($event)"
                 (itemsPerPageChange)="setItemsPerPage($event)">
-    <vdr-dt-column>{{ 'administrator.ID' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'administrator.first-name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'administrator.last-name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'administrator.email-address' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.ID' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.first-name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.last-name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.email-address' | translate }}</vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <ng-template let-administrator="item">
         <td class="left">{{ administrator.id }}</td>

+ 66 - 0
admin-ui/src/app/administrator/components/role-detail/role-detail.component.html

@@ -0,0 +1,66 @@
+<vdr-action-bar>
+    <vdr-ab-left></vdr-ab-left>
+    <vdr-ab-right>
+        <button class="btn btn-primary"
+                *ngIf="isNew$ | async; else updateButton"
+                (click)="create()"
+                [disabled]="roleForm.invalid || roleForm.pristine">{{ 'common.create' | translate }}</button>
+        <ng-template #updateButton>
+            <button class="btn btn-primary"
+                    (click)="save()"
+                    [disabled]="(roleForm.invalid || roleForm.pristine) && !permissionsChanged">{{ 'common.update' | translate }}</button>
+        </ng-template>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<form class="form" [formGroup]="roleForm" >
+    <section class="form-block">
+        <label>{{ 'admin.role' | translate }}</label>
+        <vdr-form-field [label]="'admin.description' | translate" for="description">
+            <input id="description" type="text" formControlName="description" (input)="updateCode($event.target.value)">
+        </vdr-form-field>
+        <vdr-form-field [label]="'admin.code' | translate" for="code" [readOnlyToggle]="true">
+            <input id="code" type="text" formControlName="code">
+        </vdr-form-field>
+    </section>
+
+    <table class="table">
+        <tr>
+            <th>{{ 'admin.section' | translate }}</th>
+            <th>{{ 'admin.create' | translate }}</th>
+            <th>{{ 'admin.read' | translate }}</th>
+            <th>{{ 'admin.update' | translate }}</th>
+            <th>{{ 'admin.delete' | translate }}</th>
+        </tr>
+        <tbody>
+        <tr>
+            <td>{{ 'admin.catalog' | translate }}</td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['CreateCatalog']" (selectedChange)="setPermission('CreateCatalog', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['ReadCatalog']" (selectedChange)="setPermission('ReadCatalog', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['UpdateCatalog']" (selectedChange)="setPermission('UpdateCatalog', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['DeleteCatalog']" (selectedChange)="setPermission('DeleteCatalog', $event)"></vdr-select-toggle></td>
+        </tr>
+        <tr>
+            <td>{{ 'admin.customer' | translate }}</td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['CreateCustomer']" (selectedChange)="setPermission('CreateCustomer', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['ReadCustomer']" (selectedChange)="setPermission('ReadCustomer', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['UpdateCustomer']" (selectedChange)="setPermission('UpdateCustomer', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['DeleteCustomer']" (selectedChange)="setPermission('DeleteCustomer', $event)"></vdr-select-toggle></td>
+        </tr>
+        <tr>
+            <td>{{ 'admin.order' | translate }}</td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['CreateOrder']" (selectedChange)="setPermission('CreateOrder', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['ReadOrder']" (selectedChange)="setPermission('ReadOrder', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['UpdateOrder']" (selectedChange)="setPermission('UpdateOrder', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['DeleteOrder']" (selectedChange)="setPermission('DeleteOrder', $event)"></vdr-select-toggle></td>
+        </tr>
+        <tr>
+            <td>{{ 'admin.administrator' | translate }}</td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['CreateAdministrator']" (selectedChange)="setPermission('CreateAdministrator', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['ReadAdministrator']" (selectedChange)="setPermission('ReadAdministrator', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['UpdateAdministrator']" (selectedChange)="setPermission('UpdateAdministrator', $event)"></vdr-select-toggle></td>
+            <td><vdr-select-toggle size="small" [selected]="permissions['DeleteAdministrator']" (selectedChange)="setPermission('DeleteAdministrator', $event)"></vdr-select-toggle></td>
+        </tr>
+        </tbody>
+    </table>
+</form>

+ 0 - 0
admin-ui/src/app/administrator/components/role-detail/role-detail.component.scss


+ 127 - 0
admin-ui/src/app/administrator/components/role-detail/role-detail.component.ts

@@ -0,0 +1,127 @@
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Observable } from 'rxjs';
+import { mergeMap, take } from 'rxjs/operators';
+import { CreateRoleInput, LanguageCode, Role, UpdateRoleInput } from 'shared/generated-types';
+import { Permission } from 'shared/generated-types';
+
+import { BaseDetailComponent } from '../../../common/base-detail.component';
+import { normalizeString } from '../../../common/utilities/normalize-string';
+import { _ } from '../../../core/providers/i18n/mark-for-extraction';
+import { NotificationService } from '../../../core/providers/notification/notification.service';
+import { DataService } from '../../../data/providers/data.service';
+
+@Component({
+    selector: 'vdr-role-detail',
+    templateUrl: './role-detail.component.html',
+    styleUrls: ['./role-detail.component.scss'],
+})
+export class RoleDetailComponent extends BaseDetailComponent<Role> implements OnInit, OnDestroy {
+    role$: Observable<Role>;
+    roleForm: FormGroup;
+    permissions: { [K in Permission]: boolean };
+    permissionsChanged = false;
+    constructor(
+        router: Router,
+        route: ActivatedRoute,
+        private changeDetector: ChangeDetectorRef,
+        private dataService: DataService,
+        private formBuilder: FormBuilder,
+        private notificationService: NotificationService,
+    ) {
+        super(route, router);
+        this.permissions = Object.keys(Permission).reduce(
+            (result, key) => ({ ...result, [key]: false }),
+            {} as { [K in Permission]: boolean },
+        );
+        this.roleForm = this.formBuilder.group({
+            code: ['', Validators.required],
+            description: ['', Validators.required],
+        });
+    }
+
+    ngOnInit() {
+        this.init();
+        this.role$ = this.entity$;
+    }
+
+    ngOnDestroy(): void {
+        this.destroy();
+    }
+
+    updateCode(nameValue: string) {
+        const codeControl = this.roleForm.get(['code']);
+        if (codeControl && codeControl.pristine) {
+            codeControl.setValue(normalizeString(nameValue, '-'));
+        }
+    }
+
+    setPermission(key: Permission, value: boolean) {
+        this.permissions[key] = value;
+        this.permissionsChanged = true;
+    }
+
+    create() {
+        const formValue = this.roleForm.value;
+        const role: CreateRoleInput = {
+            code: formValue.code,
+            description: formValue.description,
+            permissions: this.getSelectedPermissions(),
+        };
+        this.dataService.administrator.createRole(role).subscribe(
+            data => {
+                this.notificationService.success(_('admin.notify-create-role-success'));
+                this.roleForm.markAsPristine();
+                this.changeDetector.markForCheck();
+                this.permissionsChanged = false;
+                this.router.navigate(['../', data.createRole.id], { relativeTo: this.route });
+            },
+            err => {
+                this.notificationService.error(_('admin.notify-create-role-error'));
+            },
+        );
+    }
+
+    save() {
+        this.role$
+            .pipe(
+                take(1),
+                mergeMap(({ id }) => {
+                    const formValue = this.roleForm.value;
+                    const role: UpdateRoleInput = {
+                        id,
+                        code: formValue.code,
+                        description: formValue.description,
+                        permissions: this.getSelectedPermissions(),
+                    };
+                    return this.dataService.administrator.updateRole(role);
+                }),
+            )
+            .subscribe(
+                data => {
+                    this.notificationService.success(_('admin.notify-update-role-success'));
+                    this.roleForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                    this.permissionsChanged = false;
+                },
+                err => {
+                    this.notificationService.error(_('admin.notify-update-role-error'));
+                },
+            );
+    }
+
+    protected setFormValues(role: Role, languageCode: LanguageCode): void {
+        this.roleForm.patchValue({
+            description: role.description,
+            code: role.code,
+        });
+        for (const permission of Object.keys(this.permissions)) {
+            this.permissions[permission] = role.permissions.includes(permission as Permission);
+        }
+    }
+
+    private getSelectedPermissions(): Permission[] {
+        return Object.keys(this.permissions).filter(p => this.permissions[p]) as Permission[];
+    }
+}

+ 35 - 0
admin-ui/src/app/administrator/components/role-list/role-list.component.html

@@ -0,0 +1,35 @@
+<vdr-action-bar>
+    <vdr-ab-right>
+        <a class="btn btn-primary" [routerLink]="['./create']">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'admin.create-new-role' | translate }}
+        </a>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<vdr-data-table [items]="items$ | async"
+                [itemsPerPage]="itemsPerPage$ | async"
+                [totalItems]="totalItems$ | async"
+                [currentPage]="currentPage$ | async"
+                (pageChange)="setPageNumber($event)"
+                (itemsPerPageChange)="setItemsPerPage($event)">
+    <vdr-dt-column>{{ 'admin.ID' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.code' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.description' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'admin.permissions' | translate }}</vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <ng-template let-role="item">
+        <td class="left">{{ role.id }}</td>
+        <td class="left">{{ role.code }}</td>
+        <td class="left">{{ role.description }}</td>
+        <td class="left">
+            <vdr-chip *ngFor="let permission of role.permissions">{{ permission }}</vdr-chip>
+        </td>
+        <td class="right">
+            <vdr-table-row-action iconShape="edit"
+                                  [label]="'common.edit' | translate"
+                                  [linkTo]="['./', role.id]">
+            </vdr-table-row-action>
+        </td>
+    </ng-template>
+</vdr-data-table>

+ 0 - 0
admin-ui/src/app/administrator/components/role-list/role-list.component.scss


+ 21 - 0
admin-ui/src/app/administrator/components/role-list/role-list.component.ts

@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { GetRoles, GetRoles_roles_items } from 'shared/generated-types';
+
+import { BaseListComponent } from '../../../common/base-list.component';
+import { DataService } from '../../../data/providers/data.service';
+
+@Component({
+    selector: 'vdr-role-list',
+    templateUrl: './role-list.component.html',
+    styleUrls: ['./role-list.component.scss'],
+})
+export class RoleListComponent extends BaseListComponent<GetRoles, GetRoles_roles_items> {
+    constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.administrator.getRoles(...args),
+            data => data.roles,
+        );
+    }
+}

+ 26 - 0
admin-ui/src/app/administrator/providers/routing/role-resolver.ts

@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { Role } from 'shared/generated-types';
+
+import { BaseEntityResolver } from '../../../common/base-entity-resolver';
+import { DataService } from '../../../data/providers/data.service';
+
+/**
+ * Resolves the id from the path into a Customer entity.
+ */
+
+@Injectable()
+export class RoleResolver extends BaseEntityResolver<Role> {
+    constructor(private dataService: DataService) {
+        super(
+            {
+                __typename: 'Role' as 'Role',
+                id: '',
+                code: '',
+                description: '',
+                permissions: [],
+                channels: [],
+            },
+            id => this.dataService.administrator.getRole(id).mapStream(data => data.role),
+        );
+    }
+}

+ 1 - 1
admin-ui/src/app/common/detail-breadcrumb.ts

@@ -24,7 +24,7 @@ export function detailBreadcrumb<T>(options: {
             }
             return [
                 {
-                    label: _('breadcrumb.products'),
+                    label: options.breadcrumbKey,
                     link: ['../', options.route],
                 },
                 {

+ 7 - 0
admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -46,6 +46,13 @@
                         <clr-icon shape="administrator" size="20"></clr-icon>{{ 'nav.administrators' | translate }}
                     </a>
                 </li>
+                <li>
+                    <a class="nav-link"
+                       [routerLink]="['/admin', 'roles']"
+                       routerLinkActive="active">
+                        <clr-icon shape="users" size="20"></clr-icon>{{ 'nav.roles' | translate }}
+                    </a>
+                </li>
             </ul>
         </section>
     </section>

+ 4 - 2
admin-ui/src/app/data/data.module.ts

@@ -50,8 +50,10 @@ export function createApollo(
                     return {};
                 } else {
                     return {
-                        headers: { Authorization: `Bearer ${authToken}` },
-                        uri: `${API_URL}/${API_PATH}?token=${channelToken}`,
+                        headers: {
+                            Authorization: `Bearer ${authToken}`,
+                            'vendure-token': channelToken,
+                        },
                     };
                 }
             }),

+ 71 - 0
admin-ui/src/app/data/definitions/administrator-definitions.ts

@@ -19,6 +19,20 @@ export const ADMINISTRATOR_FRAGMENT = gql`
     }
 `;
 
+export const ROLE_FRAGMENT = gql`
+    fragment Role on Role {
+        id
+        code
+        description
+        permissions
+        channels {
+            id
+            code
+            token
+        }
+    }
+`;
+
 export const GET_ADMINISTRATORS = gql`
     query GetAdministrators($options: AdministratorListOptions) {
         administrators(options: $options) {
@@ -30,3 +44,60 @@ export const GET_ADMINISTRATORS = gql`
     }
     ${ADMINISTRATOR_FRAGMENT}
 `;
+
+export const CREATE_ADMINISTRATOR = gql`
+    mutation CreateAdministrator($input: CreateAdministratorInput!) {
+        createAdministrator(input: $input) {
+            ...Administrator
+        }
+    }
+    ${ADMINISTRATOR_FRAGMENT}
+`;
+
+export const GET_ROLES = gql`
+    query GetRoles($options: RoleListOptions) {
+        roles(options: $options) {
+            items {
+                ...Role
+            }
+            totalItems
+        }
+    }
+    ${ROLE_FRAGMENT}
+`;
+
+export const GET_ROLE = gql`
+    query GetRole($id: ID!) {
+        role(id: $id) {
+            ...Role
+        }
+    }
+    ${ROLE_FRAGMENT}
+`;
+
+export const CREATE_ROLE = gql`
+    mutation CreateRole($input: CreateRoleInput!) {
+        createRole(input: $input) {
+            ...Role
+        }
+    }
+    ${ROLE_FRAGMENT}
+`;
+
+export const UPDATE_ROLE = gql`
+    mutation UpdateRole($input: UpdateRoleInput!) {
+        updateRole(input: $input) {
+            ...Role
+        }
+    }
+    ${ROLE_FRAGMENT}
+`;
+
+export const ASSIGN_ROLE_TO_ADMINISTRATOR = gql`
+    mutation AssignRoleToAdministrator($administratorId: ID!, $roleId: ID!) {
+        assignRoleToAdministrator(administratorId: $administratorId, roleId: $roleId) {
+            ...Administrator
+        }
+    }
+    ${ADMINISTRATOR_FRAGMENT}
+`;

+ 43 - 2
admin-ui/src/app/data/providers/administrator-data.service.ts

@@ -1,7 +1,27 @@
-import { GetAdministrators, GetAdministratorsVariables } from 'shared/generated-types';
+import { Observable } from 'rxjs';
+import {
+    CreateRole,
+    CreateRoleInput,
+    CreateRoleVariables,
+    GetAdministrators,
+    GetAdministratorsVariables,
+    GetRole,
+    GetRoles,
+    GetRolesVariables,
+    GetRoleVariables,
+    UpdateRole,
+    UpdateRoleInput,
+    UpdateRoleVariables,
+} from 'shared/generated-types';
 
 import { getDefaultLanguage } from '../../common/utilities/get-default-language';
-import { GET_ADMINISTRATORS } from '../definitions/administrator-definitions';
+import {
+    CREATE_ROLE,
+    GET_ADMINISTRATORS,
+    GET_ROLE,
+    GET_ROLES,
+    UPDATE_ROLE,
+} from '../definitions/administrator-definitions';
 import { QueryResult } from '../query-result';
 
 import { BaseDataService } from './base-data.service';
@@ -20,4 +40,25 @@ export class AdministratorDataService {
             },
         });
     }
+
+    getRoles(take: number = 10, skip: number = 0): QueryResult<GetRoles, GetRolesVariables> {
+        return this.baseDataService.query<GetRoles, GetRolesVariables>(GET_ROLES, {
+            options: {
+                take,
+                skip,
+            },
+        });
+    }
+
+    getRole(id: string): QueryResult<GetRole, GetRoleVariables> {
+        return this.baseDataService.query<GetRole, GetRoleVariables>(GET_ROLE, { id });
+    }
+
+    createRole(input: CreateRoleInput): Observable<CreateRole> {
+        return this.baseDataService.mutate<CreateRole, CreateRoleVariables>(CREATE_ROLE, { input });
+    }
+
+    updateRole(input: UpdateRoleInput): Observable<UpdateRole> {
+        return this.baseDataService.mutate<UpdateRole, UpdateRoleVariables>(UPDATE_ROLE, { input });
+    }
 }

+ 4 - 0
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -32,6 +32,10 @@ export function spyObservable(name: string, returnValue: any = {}): jasmine.Spy
 export class MockDataService implements DataServiceMock {
     administrator = {
         getAdministrators: spyQueryResult('getAdministrators'),
+        getRoles: spyQueryResult('getRoles'),
+        getRole: spyQueryResult('getRole'),
+        createRole: spyObservable('createRole'),
+        updateRole: spyObservable('updateRole'),
     };
     client = {
         startRequest: spyObservable('startRequest'),

+ 31 - 1
admin-ui/src/i18n-messages/en.json

@@ -1,8 +1,35 @@
 {
+  "admin": {
+    "ID": "ID",
+    "administrator": "Administrator",
+    "catalog": "Catalog",
+    "code": "Code",
+    "create": "Create",
+    "create-new-administrator": "Create new administrator",
+    "create-new-role": "Create new role",
+    "customer": "Read",
+    "delete": "Update",
+    "description": "Delete",
+    "email-address": "Email address",
+    "first-name": "First name",
+    "last-name": "Last name",
+    "notify-create-role-error": "An error occurred, could not create role",
+    "notify-create-role-success": "Created new role",
+    "notify-update-role-error": "An error occurred, could not create role",
+    "notify-update-role-success": "Updated role",
+    "order": "Order",
+    "permissions": "Permissions",
+    "read": "Read",
+    "role": "Role",
+    "section": "Section",
+    "update": "Update"
+  },
   "breadcrumb": {
+    "administrators": "Administrators",
     "dashboard": "Dashboard",
     "facets": "Facets",
-    "products": "Products"
+    "products": "Products",
+    "roles": "Roles"
   },
   "catalog": {
     "ID": "ID",
@@ -72,11 +99,14 @@
     "product-variant-form-values-do-not-match": "The number of variants in the product form does not match the actual number of variants"
   },
   "nav": {
+    "administrator": "Admin",
+    "administrators": "Administrators",
     "catalog": "Catalog",
     "categories": "Categories",
     "facets": "Facets",
     "orders": "Orders",
     "products": "Products",
+    "roles": "Roles",
     "sales": "Sales"
   }
 }