How can one remove the input and switch option on the login page and instead have a dropdown list of tenants? I have tried https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml but this seems to only override the tenant switch modal.
5 Answer(s)
-
0
Hi,
It's located in in the Account layout, and it's coming from the theme itself. It seems you're useing LeptonX theme, you can override the following page:
- Create the following file in your project in the exact same folder structure:
Themes/LeptonX/Layouts/Account/Default.cshtml
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Options @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX @using Volo.Abp.LeptonX.Shared.Localization; @using Volo.Abp.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Bundling @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles @using Volo.Abp.Ui.Branding @using Volo.Abp.AspNetCore.Mvc.AntiForgery @using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization @using Volo.Abp.AspNetCore.MultiTenancy @using Volo.Abp.MultiTenancy @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.Common.PageAlerts @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.SideMenu.Toolbar.LanguageSwitch @using Microsoft.AspNetCore.Http.Extensions @using Volo.Abp.Ui.LayoutHooks @using Volo.Abp.LeptonX.Shared @inject IAbpAntiForgeryManager AbpAntiForgeryManager @inject IBrandingProvider BrandingProvider @inject IOptions<LeptonXThemeOptions> LeptonXThemeOptions @inject IOptions<LeptonXThemeMvcOptions> LeptonXThemeMvcOptions @inject LeptonXStyleProvider LeptonXStyleProvider @inject IStringLocalizer<AbpUiMultiTenancyResource> MultiTenancyStringLocalizer @inject IStringLocalizer<LeptonXResource> L @inject ITenantResolveResultAccessor TenantResolveResultAccessor @inject IOptions<AbpMultiTenancyOptions> MultiTenancyOptions @inject ICurrentTenant CurrentTenant @inject ThemeLanguageInfoProvider ThemeLanguageInfoProvider @inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout @inject IOptions<AbpThemingOptions> ThemingOptions @{ AbpAntiForgeryManager.SetCookie(); var langDir = CultureHelper.IsRtl ? "rtl" : string.Empty; var title = $"{ViewBag.Title ?? PageLayout.Content.Title} | {BrandingProvider.AppName}".Trim('|', ' '); var languageInfo = await ThemeLanguageInfoProvider.GetLanguageSwitchViewComponentModel(); var returnUrl = System.Net.WebUtility.UrlEncode(Context.Request.GetEncodedPathAndQuery()); var logoUrl = BrandingProvider.LogoUrl == null ? null : "--lpx-logo: url(" + Url.Content(BrandingProvider.LogoUrl.EnsureStartsWith('/').EnsureStartsWith('~')) + ");"; var logoReverseUrl = BrandingProvider.LogoReverseUrl == null ? null : "--lpx-logo: url(" + Url.Content(BrandingProvider.LogoReverseUrl.EnsureStartsWith('/').EnsureStartsWith('~')) + ");"; var selectedStyle = await LeptonXStyleProvider.GetSelectedStyleAsync(); var selectedStyleFileName = CultureHelper.IsRtl ? selectedStyle + ".rtl" : selectedStyle; var accountLayoutBackgroundStyle = LeptonXThemeMvcOptions.Value.AccountLayoutBackgroundStyle ?? $"background-image: url('{Url.Content($"~/LeptonX/images/login-pages/login-bg-img-{selectedStyle}.svg")}') !important;"; } <!DOCTYPE html> <html lang="@CultureInfo.CurrentCulture.Name" dir="@langDir"> <head> @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.First, StandardLayouts.Account) @if (!ThemingOptions.Value.BaseUrl.IsNullOrWhiteSpace()) { <base href="@ThemingOptions.Value.BaseUrl" /> } <title>@title</title> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta charset="UTF-8" /> <meta name="description" content="@ViewBag.MetaDescription"> @if (LeptonXThemeOptions.Value.Favicon != null) { <link rel="icon" href="@Url.Content(LeptonXThemeOptions.Value.Favicon.Url)" type="@LeptonXThemeOptions.Value.Favicon.Type"> } <abp-style-bundle name="@LeptonXThemeBundles.Styles.Global" /> <link href="~/Themes/LeptonX/Global/side-menu/css/bootstrap-@(selectedStyleFileName).css" type="text/css" rel="stylesheet" id="lpx-theme-bootstrap-@selectedStyle" /> <link href="~/Themes/LeptonX/Global/side-menu/css/@(selectedStyleFileName).css" type="text/css" rel="stylesheet" id="lpx-theme-color-@selectedStyle" /> @await Component.InvokeAsync(typeof(WidgetStylesViewComponent)) @await RenderSectionAsync("styles", false) @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.Last, StandardLayouts.Account) <style> .lpx-login-bg { @Html.Raw(accountLayoutBackgroundStyle) } :root .lpx-theme-light { @logoUrl } :root .lpx-theme-dark { @logoReverseUrl } :root .lpx-theme-dim { @logoReverseUrl } </style> </head> <body class="abp-account-layout lpx-theme-@selectedStyle"> @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.First, StandardLayouts.Account) <div class="container-fluid p-0" style="overflow-x: hidden"> @await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.First, StandardLayouts.Account) <div class="lpx-login-area"> <div class="lpx-login-bg"> <div class="d-flex flex-column justify-content-center min-vh-100"> <div class="row"> <div class="col-xxl-5 col-lg-7 col-md-8 col-11 mx-auto position-relative py-4"> @if (BrandingProvider.LogoUrl.IsNullOrEmpty()) { <div class="lpx-logo-container lpx-login-brand-text"> <div class="lpx-brand-logo lpx-login-logo mx-auto"></div> <div class="lpx-brand-name lpx-login-name mx-auto">@BrandingProvider.AppName</div> </div> } else { <div class="lpx-brand-logo lpx-login-logo mb-3 mx-auto"></div> } <div class="card mx-auto" style="max-width: 30rem;"> <div class="card-body p-3 p-sm-4"> @if (languageInfo.Languages.Count > 1) { <div class="align-items-start d-flex justify-content-between mb-2"> <h2 class="lpx-main-title lpx-login-title m-0 me-auto"> @PageLayout.Content.Title @* TODO: Find a better text here. *@</h2> <div class="dropdown btn-group ms-auto" aria-labelledby="languageDropdown"> <button class="btn btn-sm btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <i class="bi bi-translate me-1"></i> @languageInfo.CurrentLanguage.DisplayName </button> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuButton1" style="max-height: 60vh; overflow-y: auto;"> @foreach (var language in languageInfo.Languages) { var twoLetterLanguageName = new CultureInfo(language.CultureName).TwoLetterISOLanguageName.ToUpperInvariant(); var url = Url.Content($"~/Abp/Languages/Switch?culture={language.CultureName}&uiCulture={language.UiCultureName}&returnUrl={returnUrl}"); <li> <a href="@url" class="dropdown-item" data-lpx-language-option="@twoLetterLanguageName">@language.DisplayName / @twoLetterLanguageName</a> </li> } </ul> </div> </div> <hr/> } @await Component.InvokeAsync(typeof(PageAlertsViewComponent)) @if (MultiTenancyOptions.Value.IsEnabled && (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true || TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(QueryStringTenantResolveContributor.ContributorName) == true)) { <div> <div class="row"> <div class="col"> <span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/> <h6 class="m-0 d-inline-block"> @if (CurrentTenant.Id == null) { <span> @MultiTenancyStringLocalizer["NotSelected"] </span> } else { <strong> @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) </strong> } </h6> </div> <div class="col-auto"> <a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a> </div> </div> </div> <hr/> } @RenderBody() </div> @* @await Html.PartialAsync("~/Themes/LeptonX/Layouts/Account/_Footer.cshtml") *@ </div> </div> </div> </div> </div> </div> @await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.Last, StandardLayouts.Account) </div> <abp-script-bundle name="@LeptonXThemeBundles.Scripts.Global" /> <script src="~/Abp/ApplicationLocalizationScript?cultureName=@CultureInfo.CurrentUICulture.Name"></script> <script type="text/javascript" src="~/Abp/ApplicationConfigurationScript"></script> <script type="text/javascript" src="~/Abp/ServiceProxyScript"></script> @await Component.InvokeAsync(typeof(WidgetScriptsViewComponent)) @await RenderSectionAsync("scripts", false) @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Account) </body> </html>
This was the original content of the Account layout in v4.1
You should find the following section:
<div class="row"> <div class="col"> <span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/> <h6 class="m-0 d-inline-block"> @if (CurrentTenant.Id == null) { <span> @MultiTenancyStringLocalizer["NotSelected"] </span> } else { <strong> @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) </strong> } </h6> </div> <div class="col-auto"> <a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a> </div> </div>
And update it with the following code:
<div class="row"> <form method="post" asp-page="/Abp/MultiTenancy/TenantSwitchModal"> <select name="Input.Name" class="auto-complete-select" data-autocomplete-api-url="/api/app/tenant-lookup" data-autocomplete-display-property="name" data-autocomplete-value-property="name" data-autocomplete-items-property="items" data-autocomplete-filter-param-name="filter" data-autocomplete-allow-clear="true" > @if (CurrentTenant.Id != null) { @* Tenant resolvers works with name, not id. So value must be name. *@ <option value="@CurrentTenant.Name">@CurrentTenant.Name</option> } </select> <button type="submit" class="btn btn-sm btn-outline-primary mt-2 w-100">@MultiTenancyStringLocalizer["Switch"] </button> </form> </div>
This will look something like that:
Let's implement lookup endpoint, we cannot use Saas endpoints since they require authorization.
- Create
ITenantLookupAppService.cs
in your Application.Contracts layer:
public interface ITenantLookupAppService : IApplicationService { Task<PagedResultDto<TenantLookupDto>> GetListAsync(TenantLookupPagedResultRequestDto input); } public class TenantLookupDto { public Guid Id { get; set; } public string Name { get; set; } } public class TenantLookupPagedResultRequestDto : PagedAndSortedResultRequestDto { public string? Filter { get; set; } } - Create `TenantLookupAppService.cs` in your **Application** layer: ```cs public class TenantLookupAppService : ApplicationService, ITenantLookupAppService { private readonly ITenantRepository _tenantRepository; public TenantLookupAppService(ITenantRepository tenantRepository) { _tenantRepository = tenantRepository; } public async Task<PagedResultDto<TenantLookupDto>> GetListAsync(TenantLookupPagedResultRequestDto input) { var tenants = await _tenantRepository.GetListAsync(skipCount: input.SkipCount, maxResultCount: input.MaxResultCount, filter: input.Filter); var count = await _tenantRepository.GetCountAsync(filter: input.Filter); return new PagedResultDto<TenantLookupDto>(count, tenants.Select(t => new TenantLookupDto { Id = t.Id, Name = t.Name }).ToList()); } }
- Now tenants will be listed and searchable here no matter how many you're, this ABP's Auto-Complete-Select implementation supports lazy-loading and searching, so it doesn't load all of them at the same time to the UI.
As a note: Submitting directly to a SwitchTenantModal endpoint, returns the switchtenant modal when succeeded, you can prevent it by submiting form by javascript instead page-submit directly.
- Create the following file in your project in the exact same folder structure:
-
0
Thank you so much for the solution
-
0
Do you have a sample of this script.."you can prevent it by submiting form by javascript instead page-submit directly"?
-
0
Create a file named
account.js
in the same folder alongsideAccount/Default.cshtml
file.Set the content:
$(function () { var $form = $('#tenant-switch-form'); if ($form.length > 0) { $form.on('submit', function (e) { e.preventDefault(); var $this = $(this); var formData = $this.serialize(); var action = $this.attr('action') || window.location.pathname; var method = $this.attr('method') || 'post'; $.ajax({ url: action, type: method.toUpperCase(), data: formData, success: function (data, status, xhr) { // On success, reload the page window.location.reload(); }, error: function (xhr, status, error) { alert('Tenant switch failed: ' + (xhr.responseText || error)); } }); }); } });
- Add it at the bottom of the page in
Account/Default.cshtml
<script src="~/Themes/LeptonX/Layouts/Account/account.js"></script>
- Add
id="tenant-switch-form"
to the tenant switch form. It should be looking like that:
<form id="tenant-switch-form" method="post" asp-page="/Abp/MultiTenancy/TenantSwitchModal"> <select name="Input.Name" class="auto-complete-select" <!-- ... -->
-
0
Works great! Thanks again