Sticky headers (#3416)

* Make headers sticky

Closes #3344.

* Decrease headline margin bottom on mobile

* increases gap

* adds bottom padding

* Update BTCPayServer/Views/UIApps/UpdatePointOfSale.cshtml

* add "_blank" to view action

* Fix markup and tests

* Spacing updates

* Try test fix

* Re-add sticky account header and add test logs for timeout check

* Fix timeout issues

* Apply scroll padding on pages with sticky header

Co-authored-by: dstrukt <gfxdsign@gmail.com>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2022-02-07 10:37:45 +01:00 committed by GitHub
parent c652a2f122
commit 20a9472ee2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 276 additions and 291 deletions

View file

@ -47,6 +47,7 @@ namespace BTCPayServer.Tests
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
TestLogs.LogInformation("Checking admin permissions");
//not an admin, so this permission should not show
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
await user.MakeAdmin();
@ -56,30 +57,34 @@ namespace BTCPayServer.Tests
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking super admin key");
//this api key has access to everything
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyServerSettings permissions");
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
Policies.CanModifyServerSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyStoreSettings permissions");
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
Policies.CanModifyStoreSettings);
@ -97,12 +102,18 @@ namespace BTCPayServer.Tests
option.Click();
s.Driver.FindElement(By.Id("Generate")).Click();
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyStoreSettings with StoreId permissions");
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.Id("Generate")).Click();
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking no permissions");
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
@ -110,6 +121,8 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
});
TestLogs.LogInformation("Checking authorize screen");
//let's test the authorized screen now
//options for authorize are:
@ -157,6 +170,9 @@ namespace BTCPayServer.Tests
Assert.Equal(callbackUrl, s.Driver.Url);
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
TestLogs.LogInformation("Checking authorized permissions");
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
@ -191,7 +207,10 @@ namespace BTCPayServer.Tests
}
s.Driver.FindElement(By.Id("Generate")).Click();
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
TestLogs.LogInformation("Checking API key permissions");
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, "api/v1/api-keys/current", tester.PayTester.HttpClient);
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
}

View file

@ -135,6 +135,15 @@ retry:
}
public static void WaitForAndClick(this IWebDriver driver, By selector)
{
// Try fast path
try
{
driver.FindElement(selector).Click();
return;
}
catch { }
// Sometimes, selenium complain, so we enter hack territory
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
@ -158,22 +167,8 @@ retry:
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
{
var element = driver.FindElement(selector);
if ((value && !element.Selected) || (!value && element.Selected))
{
try
{
driver.WaitForAndClick(selector);
}
catch (ElementClickInterceptedException)
{
element.SendKeys(" ");
}
}
if (value != element.Selected)
{
driver.SetCheckbox(selector, value);
}
driver.WaitForAndClick(selector);
}
}
}

View file

@ -585,6 +585,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
@ -595,9 +597,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
var posBaseUrl = s.Driver.Url.Replace("/cart", "");
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
@ -607,6 +614,9 @@ namespace BTCPayServer.Tests
s.Driver.Url = posBaseUrl + "/cart";
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact(Timeout = TestTimeout)]
@ -622,14 +632,26 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
Assert.Equal("currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact(Timeout = TestTimeout)]
@ -1529,10 +1551,19 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.FindElement(By.CssSelector("#crowdfund-body-contribution-container .perk")).Click();
s.Driver.FindElement(By.PartialLinkText("LNURL")).Click();
lnurl = s.Driver.FindElement(By.ClassName("lnurl"))
@ -1540,7 +1571,8 @@ namespace BTCPayServer.Tests
LNURL.LNURL.Parse(lnurl, out tag);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
}
[Fact]
@ -1552,7 +1584,6 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
var cryptoCode = "BTC";
s.RegisterNewUser(true);
//ln address tests
s.CreateNewStore();

View file

@ -14,11 +14,26 @@
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js" asp-append-version="true"></bundle>
}
<partial name="_StatusMessage" />
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
<form method="post">
<div class="sticky-header-setup"></div>
<header class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
@if (Model.ModelWithMinimumData)
{
<a class="btn btn-secondary" asp-action="ViewCrowdfund" asp-controller="UIAppsPublic" asp-route-appId="@Model.AppId" id="ViewApp" target="app_@Model.AppId">View</a>
}
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
</div>
</header>
<script>
const { offsetHeight } = document.querySelector('.sticky-header-setup + .sticky-header');
document.documentElement.style.scrollPaddingTop = `calc(${offsetHeight}px + var(--btcpay-space-m))`;
</script>
<partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
@ -123,6 +138,7 @@
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
</div>
<h3 class="mt-5 mb-4">Contributions</h3>
<div class="form-check mb-3">
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check-input" />
@ -144,6 +160,7 @@
<label asp-for="EnforceTargetAmount" class="form-check-label"></label>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<h3 class="mt-5 mb-4">Crowdfund Behavior</h3>
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
@ -155,9 +172,9 @@
<label asp-for="UseAllStoreInvoices" class="form-check-label"></label>
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<h3 class="mt-5 mb-4">Sound</h3>
<div class="form-group mb-0">
<div class="form-group d-flex align-items-center mb-0">
<input asp-for="SoundsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#SoundsEnabledSettings" aria-expanded="@Model.SoundsEnabled" aria-controls="SoundsEnabledSettings"/>
<label asp-for="SoundsEnabled" class="form-label mb-0"></label>
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
@ -169,9 +186,9 @@
<span asp-validation-for="Sounds" class="text-danger"></span>
</div>
</div>
<h3 class="mt-5 mb-4">Animation</h3>
<div class="form-group mb-3">
<div class="form-group d-flex align-items-center mb-0">
<input asp-for="AnimationsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#AnimationsEnabledSettings" aria-expanded="@Model.AnimationsEnabled" aria-controls="AnimationsEnabledSettings"/>
<label asp-for="AnimationsEnabled" class="form-label mb-0"></label>
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
@ -183,9 +200,9 @@
<span asp-validation-for="AnimationColors" class="text-danger"></span>
</div>
</div>
<h3 class="mt-5 mb-4">Discussion</h3>
<div class="form-group mb-3">
<div class="form-group d-flex align-items-center mb-0">
<input asp-for="DisqusEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#DisqusEnabledSettings" aria-expanded="@Model.DisqusEnabled" aria-controls="DisqusEnabledSettings"/>
<label asp-for="DisqusEnabled" class="form-label mb-0"></label>
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
@ -235,27 +252,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-9 mt-2">
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
@if (Model.ModelWithMinimumData)
{
<div class="btn-group">
<a class="btn btn-secondary flex-grow-1" asp-action="ViewCrowdfund" asp-controller="UIAppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View Crowdfund</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewCrowdfund" asp-controller="UIAppsPublic" asp-route-appId="@Model.AppId"
target="viewapp_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
}
</div>
</div>
</div>
</form>
<h3 class="mt-5 mb-4">Other Actions</h3>

View file

@ -5,11 +5,23 @@
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
}
<partial name="_StatusMessage" />
<h2 class="mt-1 mb-4">@ViewData["Title"]</h2>
<form method="post">
<div class="sticky-header-setup"></div>
<header class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
<a class="btn btn-secondary" asp-action="ViewPointOfSale" asp-controller="UIAppsPublic" asp-route-appId="@Model.Id" id="ViewApp" target="app_@Model.AppId">View</a>
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
</div>
</header>
<script>
const { offsetHeight } = document.querySelector('.sticky-header-setup + .sticky-header');
document.documentElement.style.scrollPaddingTop = `calc(${offsetHeight}px + var(--btcpay-space-m))`;
</script>
<partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
@ -204,13 +216,11 @@
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre class="p-3">@Model.ExampleCallback</pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
</ul>
</p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
</ul>
</div>
</div>
</div>
@ -243,23 +253,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-9 mt-2">
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
<div class="btn-group">
<a class="btn btn-secondary flex-grow-1" asp-action="ViewPointOfSale" asp-controller="UIAppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View Point of Sale</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewPointOfSale" asp-controller="UIAppsPublic" asp-route-appId="@Model.Id"
target="viewapp_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
</div>
</div>
</div>
</form>
<h3 class="mt-5 mb-4">Other Actions</h3>
@ -332,7 +325,7 @@
</div>
</script>
<script>
var posStyleSelector = document.getElementById('DefaultView');
const posStyleSelector = document.getElementById('DefaultView');
posStyleSelector.addEventListener('change', function(e) {
handleStyleSelected(e.target.value);
});
@ -362,7 +355,7 @@
/** Show/hide discounts section */
var discountsSection = document.getElementById('discounts');
const discountsSection = document.getElementById('discounts');
function hideDiscountsSection() {
hideElement(discountsSection);
@ -376,7 +369,7 @@
/** Show/hide button text section */
var buttonPriceTextSection = document.getElementById('button-price-text');
const buttonPriceTextSection = document.getElementById('button-price-text');
function hideButtonPriceTextSection() {
hideElement(buttonPriceTextSection);
@ -390,7 +383,7 @@
/** Show/hide custom payments amount seciton */
var customPaymentAmountSection = document.getElementById('custom-payments');
const customPaymentAmountSection = document.getElementById('custom-payments');
function hideCustomPaymentAmountSection() {
hideElement(customPaymentAmountSection);
@ -404,7 +397,7 @@
/** Show/hide tips seciton */
var tipsSection = document.getElementById('tips');
const tipsSection = document.getElementById('tips');
function hideTipsSection() {
hideElement(tipsSection);

View file

@ -1,13 +1,21 @@
@inject SignInManager<ApplicationUser> SignInManager
<h2 class="mt-1 mb-4">Account Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a id="SectionNav-@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="UIManage" asp-action="Index">Account</a>
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="UIManage" asp-action="ChangePassword">Password</a>
<a id="SectionNav-@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="UIManage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a>
<a id="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="UIManage" asp-action="APIKeys">API Keys</a>
<a id="SectionNav-@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="UIManage" asp-action="NotificationSettings">Notifications</a>
<a id="SectionNav-@ManageNavPages.LoginCodes.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.LoginCodes)" asp-controller="UIManage" asp-action="LoginCodes">Login Codes</a>
<vc:ui-extension-point location="user-nav" model="@Model"/>
</div>
</nav>
<div class="sticky-header-setup"></div>
<header class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Account Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a id="SectionNav-@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="UIManage" asp-action="Index">Account</a>
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="UIManage" asp-action="ChangePassword">Password</a>
<a id="SectionNav-@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="UIManage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a>
<a id="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="UIManage" asp-action="APIKeys">API Keys</a>
<a id="SectionNav-@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="UIManage" asp-action="NotificationSettings">Notifications</a>
<a id="SectionNav-@ManageNavPages.LoginCodes.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.LoginCodes)" asp-controller="UIManage" asp-action="LoginCodes">Login Codes</a>
<vc:ui-extension-point location="user-nav" model="@Model"/>
</div>
</nav>
</header>
<script>
const { offsetHeight } = document.querySelector('.sticky-header-setup + .sticky-header');
document.documentElement.style.scrollPaddingTop = `calc(${offsetHeight}px + var(--btcpay-space-m))`;
</script>

View file

@ -1,21 +1,28 @@
@using BTCPayServer.Configuration
@inject BTCPayServerOptions _btcPayServerOptions
<h2 class="mt-1 mb-4">Server Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
@if (_btcPayServerOptions.DockerDeployment)
{
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
}
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Plugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" asp-action="ListPlugins">Plugins</a>
<vc:ui-extension-point location="server-nav" model="@Model"/>
</div>
</nav>
<div class="sticky-header-setup"></div>
<header class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Server Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
@if (_btcPayServerOptions.DockerDeployment)
{
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
}
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Plugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" asp-action="ListPlugins">Plugins</a>
<vc:ui-extension-point location="server-nav" model="@Model"/>
</div>
</nav>
</header>
<script>
const { offsetHeight } = document.querySelector('.sticky-header-setup + .sticky-header');
document.documentElement.style.scrollPaddingTop = `calc(${offsetHeight}px + var(--btcpay-space-m))`;
</script>

View file

@ -1,14 +1,22 @@
@using BTCPayServer.Client
<h2 class="mt-1 mb-4">Store Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.General))" class="nav-link @ViewData.IsActivePage(StoreNavPages.General)" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="UIStores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
<vc:ui-extension-point location="store-nav" model="@Model"/>
</div>
</nav>
<div class="sticky-header-setup"></div>
<header class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Store Settings</h2>
<nav id="SectionNav">
<div class="nav">
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.General))" class="nav-link @ViewData.IsActivePage(StoreNavPages.General)" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="UIStores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
<vc:ui-extension-point location="store-nav" model="@Model"/>
</div>
</nav>
</header>
<script>
const { offsetHeight } = document.querySelector('.sticky-header-setup + .sticky-header');
document.documentElement.style.scrollPaddingTop = `calc(${offsetHeight}px + var(--btcpay-space-m))`;
</script>

View file

@ -10617,25 +10617,25 @@ fieldset:disabled .btn {
}
/* Scrollbar - first works on Firefox, rest on WebKit-based browsers */
* {
--btcpay-scrollbar-width: .375rem;
--btcpay-scrollbar-color: var(--btcpay-neutral-500);
--btcpay-scrollbar-bg: transparent;
scrollbar-width: var(--btcpay-scrollbar-width);
scrollbar-color: var(--btcpay-scrollbar-color) var(--btcpay-scrollbar-bg);
--btcpay-scrollbar-width: .375rem;
--btcpay-scrollbar-color: var(--btcpay-neutral-500);
--btcpay-scrollbar-bg: transparent;
scrollbar-width: var(--btcpay-scrollbar-width);
scrollbar-color: var(--btcpay-scrollbar-color) var(--btcpay-scrollbar-bg);
}
*::-webkit-scrollbar {
width: var(--btcpay-scrollbar-width);
width: var(--btcpay-scrollbar-width);
}
*::-webkit-scrollbar-track {
background: var(--btcpay-scrollbar-bg);
background: var(--btcpay-scrollbar-bg);
}
*::-webkit-scrollbar-thumb {
background-color: var(--btcpay-scrollbar-color);
border-radius: 1rem;
border: 1rem solid var(--btcpay-scrollbar-bg);
background-color: var(--btcpay-scrollbar-color);
border-radius: 1rem;
border: 1rem solid var(--btcpay-scrollbar-bg);
}
html {
@ -10901,149 +10901,59 @@ input[type=number].hide-number-spin {
color: var(--btcpay-danger);
}
/* Custom space utils, as Bootstrap has a different spacing scale for > M */
.pt-l { padding-top: var(--btcpay-space-l); }
.pt-xl { padding-top: var(--btcpay-space-xl); }
.pb-l { padding-bottom: var(--btcpay-space-l); }
.pb-xl { padding-bottom: var(--btcpay-space-xl); }
.mt-l { margin-top: var(--btcpay-space-l); }
.mt-xl { margin-top: var(--btcpay-space-xl); }
.mb-l { margin-bottom: var(--btcpay-space-l); }
.mb-xl { margin-bottom: var(--btcpay-space-xl); }
/* Negative margin utils (only include bare minimum) */
.mt-n1 {
margin-top: -0.25rem !important;
}
.mt-n2 {
margin-top: -0.5rem !important;
}
.mt-n3 {
margin-top: -1rem !important;
}
.mt-n4 {
margin-top: -1.5rem !important;
}
.mt-n5 {
margin-top: -3rem !important;
}
.me-n1 {
margin-right: -0.25rem !important;
}
.me-n2 {
margin-right: -0.5rem !important;
}
.me-n3 {
margin-right: -1rem !important;
}
.me-n4 {
margin-right: -1.5rem !important;
}
.me-n5 {
margin-right: -3rem !important;
}
.mb-n1 {
margin-bottom: -0.25rem !important;
}
.mb-n2 {
margin-bottom: -0.5rem !important;
}
.mb-n3 {
margin-bottom: -1rem !important;
}
.mb-n4 {
margin-bottom: -1.5rem !important;
}
.mb-n5 {
margin-bottom: -3rem !important;
}
.ms-n1 {
margin-left: -0.25rem !important;
}
.ms-n2 {
margin-left: -0.5rem !important;
}
.ms-n3 {
margin-left: -1rem !important;
}
.ms-n4 {
margin-left: -1.5rem !important;
}
.ms-n5 {
margin-left: -3rem !important;
}
.mt-n1 { margin-top: -0.25rem !important; }
.mt-n2 { margin-top: -0.5rem !important; }
.mt-n3 { margin-top: -1rem !important; }
.mt-n4 { margin-top: -1.5rem !important; }
.mt-n5 { margin-top: -3rem !important; }
.me-n1 { margin-right: -0.25rem !important; }
.me-n2 { margin-right: -0.5rem !important; }
.me-n3 { margin-right: -1rem !important; }
.me-n4 { margin-right: -1.5rem !important; }
.me-n5 { margin-right: -3rem !important; }
.mb-n1 { margin-bottom: -0.25rem !important; }
.mb-n2 { margin-bottom: -0.5rem !important; }
.mb-n3 { margin-bottom: -1rem !important; }
.mb-n4 { margin-bottom: -1.5rem !important; }
.mb-n5 { margin-bottom: -3rem !important; }
.ms-n1 { margin-left: -0.25rem !important; }
.ms-n2 { margin-left: -0.5rem !important; }
.ms-n3 { margin-left: -1rem !important; }
.ms-n4 { margin-left: -1.5rem !important; }
.ms-n5 { margin-left: -3rem !important; }
@media (min-width: 992px) {
.mt-lg-n1 {
margin-top: -0.25rem !important;
}
.mt-lg-n2 {
margin-top: -0.5rem !important;
}
.mt-lg-n3 {
margin-top: -1rem !important;
}
.mt-lg-n4 {
margin-top: -1.5rem !important;
}
.mt-lg-n5 {
margin-top: -3rem !important;
}
.me-lg-n1 {
margin-right: -0.25rem !important;
}
.me-lg-n2 {
margin-right: -0.5rem !important;
}
.me-lg-n3 {
margin-right: -1rem !important;
}
.me-lg-n4 {
margin-right: -1.5rem !important;
}
.me-lg-n5 {
margin-right: -3rem !important;
}
.mb-lg-n1 {
margin-bottom: -0.25rem !important;
}
.mb-lg-n2 {
margin-bottom: -0.5rem !important;
}
.mb-lg-n3 {
margin-bottom: -1rem !important;
}
.mb-lg-n4 {
margin-bottom: -1.5rem !important;
}
.mb-lg-n5 {
margin-bottom: -3rem !important;
}
.ms-lg-n1 {
margin-left: -0.25rem !important;
}
.ms-lg-n2 {
margin-left: -0.5rem !important;
}
.ms-lg-n3 {
margin-left: -1rem !important;
}
.ms-lg-n4 {
margin-left: -1.5rem !important;
}
.ms-lg-n5 {
margin-left: -3rem !important;
}
.mt-lg-n1 { margin-top: -0.25rem !important; }
.mt-lg-n2 { margin-top: -0.5rem !important; }
.mt-lg-n3 { margin-top: -1rem !important; }
.mt-lg-n4 { margin-top: -1.5rem !important; }
.mt-lg-n5 { margin-top: -3rem !important; }
.me-lg-n1 { margin-right: -0.25rem !important; }
.me-lg-n2 { margin-right: -0.5rem !important; }
.me-lg-n3 { margin-right: -1rem !important; }
.me-lg-n4 { margin-right: -1.5rem !important; }
.me-lg-n5 { margin-right: -3rem !important; }
.mb-lg-n1 { margin-bottom: -0.25rem !important; }
.mb-lg-n2 { margin-bottom: -0.5rem !important; }
.mb-lg-n3 { margin-bottom: -1rem !important; }
.mb-lg-n4 { margin-bottom: -1.5rem !important; }
.mb-lg-n5 { margin-bottom: -3rem !important; }
.ms-lg-n1 { margin-left: -0.25rem !important; }
.ms-lg-n2 { margin-left: -0.5rem !important; }
.ms-lg-n3 { margin-left: -1rem !important; }
.ms-lg-n4 { margin-left: -1.5rem !important; }
.ms-lg-n5 { margin-left: -3rem !important; }
}
.btcpay-pills input {

View file

@ -120,8 +120,8 @@
}
#mainContent > section {
padding: 0;
flex: 1;
padding: var(--content-padding-top) var(--content-padding-horizontal) var(--content-padding-bottom);
}
#StoreSelector {
@ -335,6 +335,28 @@
background: var(--btcpay-nav-bg-active);
}
/* Sticky Header: The <div class="sticky-header-setup"></div> needs to be included once
before the first sticky-header on the page. The sticky-header has a padding-top so
that it does not scroll underneath the fixed header on mobile. The sticky-header-setup
negates that padding with a negative margin, so that everything fits in the end. */
.sticky-header-setup {
margin-top: calc(var(--content-padding-top) * -1);
}
.sticky-header {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 1020;
background: var(--btcpay-body-bg);
padding-top: var(--content-padding-top);
padding-bottom: var(--btcpay-space-l);
}
.sticky-header #SectionNav {
margin-bottom: calc(var(--btcpay-space-l) * -1);
}
/* Footer */
.btcpay-footer {
font-size: var(--btcpay-font-size-s);
@ -357,10 +379,12 @@
@media (max-width: 991px) {
:root {
--header-height: var(--mobile-header-height);
--content-padding-top: var(--btcpay-space-m);
--content-padding-top: calc(var(--header-height) + var(--btcpay-space-m));
--content-padding-bottom: var(--btcpay-space-xl);
--content-padding-horizontal: var(--btcpay-space-m);
/* Prevent anchors from disappearing underneath the fixed header */
scroll-padding: calc(var(--header-height) + var(--content-padding-top));
scroll-padding: var(--content-padding-top);
}
#mainMenu {
@ -471,11 +495,6 @@
z-index: 0;
}
#mainContent > section {
margin-top: var(--header-height);
padding: var(--content-padding-top) var(--btcpay-space-m) var(--btcpay-space-xl);
}
#SectionNav {
--scroll-indicator-spacing: var(--btcpay-space-m);
position: relative;
@ -526,6 +545,9 @@
@media (min-width: 992px) {
:root {
--header-height: var(--desktop-header-height);
--content-padding-top: 5rem;
--content-padding-bottom: 5rem;
--content-padding-horizontal: var(--btcpay-space-xl);
}
#mainMenu {
@ -575,10 +597,6 @@
max-width: calc(100vw - var(--sidebar-width) - (2 * var(--btcpay-space-xl)) - 1rem); /* 1rem for scrollbar */
}
#mainContent > section {
padding: 5rem var(--btcpay-space-xl);
}
#mainContent > section,
.btcpay-footer > .container {
margin: 0;