Thanks, we've got this working now!
Sorry, I'm not quite following your meaning. I'm less concerned about the breadcrumbs and more so wondering if I could define a navigation item that behaves as a sort of "catch-all":
*app-routing.module.ts*
{
path: '/module1/page1/**', // Something to denote a wildcard/catchall
name: 'M1P1',
// etc,
isActive: (activatedRoute, itemConfig /* this node */) => isPathOrChild(itemConfig.path, activatedRoute); // Or something like this
}
And have this item selected for any of: /module1/page1 /module1/page1/list /module1/page1/details
We would like to avoid that as it's too granular for the menu and the detail page has a dynamic url (based on selection from the list page). Any options for this work as I described?
Ok, I think I understand. Our custom contributor and localizer will be responsible for caching the texts from the database and then we can do one of a few options: 1. We can create a custom resource class per lookup:
public class CompanyTypeResources {
}
...
Configure<AbpLocalizationOptions>(options => options.Resources.Add<CompanyTypeResources>());
2. We can create a single resource class instead (ex: LookupResources
) and have our localizer cache all text for this resource
3. Or we can just use the existing MyProjectResources
that's created as part of the default template
Does this sound right? I'd like to avoid creating a resource class per lookup, so I'm thinking option 2 or 3 would suit best. Any recommendations on best practices?
Hi, I've looked at the code you've sent me, but I'm still not sure how this should be used for our use case. These classes you've shared expect a LocalizationResourceBase
to be passed in via constructor/parameters and it's not clear to me what generates the LocalizationResourceBase
and how they get passed in. My suspicion is that these are generated based on code (possibly classes decorated with LocalizationResourceNameAttribute
?) and it's not clear how to tie this class that's dependent on something baked into the assembly to instead look for a dynamic list of texts from the database. Do we need to create an empty resource class for each of our lookups and have the custom contributor go to the database for these?
Ah, that makes complete sense. Thank you!
I wonder, though: is there any ability to highlight the menu item for child routes? As in my example, we usually have /list and /detail child routes. It would be nice to be able to set a menu item "Page 1" with a path of "/module1/page1" and have it be marked as active if we're at either "/module1/page1/list" or "/module1/page1/detail".
Hi Liang Shiwei,
I tried to email you, but it was rejected by the mail server. I've sent the file as a Google Drive share instead.
Thanks
Hi maliming,
The Language Management module is a pro module which we don't have access to the source code for; the best I can do is use a decompiler to view an obfuscated version of the code and try to understand it.
Reviewing the decompiled code, it looks as if these classes are dependent on a LocalizationResourceBase being passed in, but I'm not sure if this is appropriate to my use case. We have a structure whereas our lookup tables (ex: Company Type, Lead Source, Tax Class, etc) store localization values in the database (see a demo ERD); in our example, each CompanyType record has one corresponding TextResource record with multiple TextResourceTranslations (one per supported culture). This is one example from a large list of lookup tables that we support.
In our ABP application we have a series of pages for each lookup that supports the creation of the lookup entity, text resource, and text resource translations. It's now our desire to be able to use the built-in localization engine to pull the correct translation from our database table utilizing the group & key values of the Text Resource table. Continuing our example of Company Type, this would be something like {{ 'CompanyTypes:Software' | abpLocalization }}
.
I created a sample implementation of DynamicLocalizationResourceContributor
and DynamicResourceLocalizer
and walked through how it's called and it seems to only be called for classes in the code that have been marked as a resource file.
Can you provide any other guidance on implementing this requirement?
Sorry for the delay, I've been quite busy with competing priorities. I'll get you this test project as soon as I can, but wanted to check in and ensure this ticket doesn't get closed before I get a chance.
It looks like we're missing the forLazy() call from our module import, but it seems that's something we need to manually create on our modules and just looking at your link, I don't understand how to create this (in explanation, I don't understand what's happening in the forChild() call).
Here's an anonymized sample of how we've set things up (routes and module names changed to protect our client). Apologies if some of this is implemented strangely; our team is somewhat inexperienced with Angular.
Would this all work if we implemented and used forLazy() on all of our routing modules that do lazy loading?
/src/app/app-routing.module.ts
import { authGuard, permissionGuard } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
// ... Removed default ABP routes
// Custom routes
{
path: 'module1',
loadChildren: () => import('./modules/module1/module1.module').then(m => m.Module1),
},
{
path: 'module2',
loadChildren: () => import('./modules/module2/module2.module').then(m => m.Module2),
},
{
path: 'module3',
loadChildren: () => import('./modules/module3/module3.module').then(m => m.Module3),
},
{
path: 'module4',
loadChildren: () => import('./modules/module4/module4.module').then(m => m.Module4),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { bindToComponentInputs: true })],
exports: [RouterModule],
})
export class AppRoutingModule {}
/src/app/modules/module1/module1-routing.module.ts
import { NgModule } from '@angular/core';
import { permissionGuard } from '@abp/ng.core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
children: [
{
path: 'page1',
loadChildren: () => import('./module1/page1.module').then(m => m.Page1Module), // Has own routing that exposes child routes for CRUD operations (list, edit, create, update)
},
{
path: 'page2',
loadChildren: () => import('./module1/page2.module').then(m => m.Page2Module), // Has own routing that exposes child routes for CRUD operations (list, edit, create, update)
},
// ... snip ...
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class Module1RoutingModule { }
/src/app/route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/',
name: '::Menu:Home',
iconClass: 'fas fa-home',
order: 1,
layout: eLayoutType.application,
},
{
path: '/dashboard',
name: '::Menu:Dashboard',
iconClass: 'fas fa-chart-line',
order: 2,
layout: eLayoutType.application,
},
{
name: '::Menu:Module1',
order: 3,
iconClass: 'fa fa-gear',
layout: eLayoutType.application,
},
{
path: '/module1/page1',
name: 'M1P1',
parentName: '::Menu:Module1',
order: 1,
iconClass: 'fas fa-table',
layout: eLayoutType.application,
},
{
path: '/module1/page2',
name: 'M1P2',
parentName: '::Menu:Module1',
iconClass: 'fas fa-table',
order: 2,
layout: eLayoutType.application,
},
// ... snip ...
]);
};
}