Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions readthedocsext/theme/static/readthedocsext/theme/js/site.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

101 changes: 62 additions & 39 deletions readthedocsext/theme/templates/account/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
</div>
</div>

<div class="ui small stackable teal centered fluid wrapping menu"
data-bind="semanticui: {tabs: {history: true, autoTabActivation: false}}">
<div class="ui small stackable teal centered fluid wrapping menu" data-bind="semanticui: {tabs: {
{% if last_login_tab %}autoTabActivation: '{{ last_login_tab }}'{% endif %}
}}">

<a class="item" data-tab="vcs">
<i class="fas fa-cloud icon"></i>
Expand All @@ -42,6 +43,7 @@

{# If allowed providers is given, disable the email menu item #}
<a class="{% if allowed_providers %}disabled{% endif %} item"
{% if last_login_tab == "email" %} data-tooltip="Last used" data-variation="teal visible" data-position="top center" {% endif %}
data-tab="email">
<i class="fas fa-envelope icon"></i>
{% block authentication_email_text %}
Expand All @@ -50,8 +52,7 @@
</a>

{% if USE_ORGANIZATIONS %}
<a class="item"
href="{% url "saml_resolve_login" %}{% if redirect_field_value %}?{{ redirect_field_name }}={{ redirect_field_value }}{% endif %}">
<a class="item" data-tab="sso">
<i class="fas fa-shield-alt icon"></i>
{% trans "Single sign-on" %}
</a>
Expand All @@ -68,44 +69,66 @@
</div>
</div>

<div class="ui basic center aligned tab segment" data-tab="vcs">
<div class="ui relaxed list">
{% block authentication_vcs %}
{# Translators: this will read "Log in with GitHub", where "log in" is a verb #}
{% blocktrans asvar text_log_in %}Log in using{% endblocktrans %}
{% include "socialaccount/snippets/provider_list.html" with process="login" verbiage=text_log_in %}
{% endblock authentication_vcs %}
<div data-bind="using: LoginView()">
<div class="ui basic center aligned tab segment" data-tab="vcs">
<div class="ui relaxed list">
{% block authentication_vcs %}
{# Translators: this will read "Log in with GitHub", where "log in" is a verb #}
{% blocktrans asvar text_log_in %}Log in using{% endblocktrans %}
{% include "socialaccount/snippets/provider_list.html" with process="login" verbiage=text_log_in %}
{% endblock authentication_vcs %}
</div>
</div>
</div>
<div class="ui basic vertically fitted tab segment" data-tab="email">
{% block authentication_email %}
<p class="ui center aligned vertically fitted small basic fluid segment">
{# The /email addition is to route to the tab page on the login/signup forms, using the tab module history setting #}
{% blocktrans trimmed %}
Need an account? <a href="{{ signup_url }}#/email">Sign up</a> first.
{% endblocktrans %}
</p>

<form class="ui form"
method="post"
action="{% url "account_login" %}#/email">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}" />
{% endif %}

<div class="ui stackable relaxed text menu">
<a class="item" href="{% url 'account_reset_password' %}">{% trans "Forgot your password?" %}</a>
<div class="right menu">
<button class="ui primary button" type="submit">{% trans "Log in" %}</button>
<div class="ui basic vertically fitted tab segment" data-tab="email">
{% block authentication_email %}
<p class="ui center aligned vertically fitted small basic fluid segment">
{# The /email addition is to route to the tab page on the login/signup forms, using the tab module history setting #}
{% blocktrans trimmed %}
Need an account? <a href="{{ signup_url }}#/email">Sign up</a> first.
{% endblocktrans %}
</p>

<form class="ui form"
method="post"
action="{% url "account_login" %}#/email">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}" />
{% endif %}

<div class="ui stackable relaxed text menu">
<a class="item" href="{% url 'account_reset_password' %}">{% trans "Forgot your password?" %}</a>
<div class="right menu">
<button class="ui primary button"
data-bind="click: save_login_method"
data-provider="email"
Comment on lines +106 to +107
Copy link
Contributor

@agjohnson agjohnson Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still working against KO a bit. We should really not need to do Event or Element manipulation to use data with KO. The two patterns that we already have available are:

data-bind="click: () => { save_login_method('email') }"

Or better if you need to do something like ensure the click event is handled normally (true is returned from the function):

data-bind="click: save_login_method.bind($data, 'email')"

Both from: https://knockoutjs.com/documentation/click-binding.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. I didn't know that using a KO function in data-bind that reads data-* attributes was a mixing of patterns 👍🏼

type="submit">{% trans "Log in" %}</button>
</div>
</div>
</div>

</form>
{% endblock authentication_email %}
</form>
{% endblock authentication_email %}
</div>

{% if USE_ORGANIZATIONS %}
<div class="ui basic center aligned tab segment" data-tab="sso">
<a href="{% url "saml_resolve_login" %}{% if redirect_field_value %}?{{ redirect_field_name }}={{ redirect_field_value }}{% endif %}">

<button class="ui button"
data-bind="click: save_login_method"
data-provider="sso"
type="submit"
{% if last_login_method == "sso" %} data-tooltip="Last used" data-variation="teal visible" data-position="right center" {% endif %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all need translated:

Suggested change
{% if last_login_method == "sso" %} data-tooltip="Last used" data-variation="teal visible" data-position="right center" {% endif %}
{% if last_login_method == "sso" %} data-tooltip="{% trans "Last used" %}" data-variation="teal visible" data-position="right center" {% endif %}

title="Single sign-on">
<i class="fas fa-shield-alt icon" aria-hidden="true"></i>
{% trans "Log in using single sign-on" %}
</button>
</a>
</div>
{% endif %}
</div>

{% block authentication_extra %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
{% trans "GitHub" as provider_name %}
<li class="item">
<a class="ui button"
{% if last_login_method == "github" or last_login_method == "githubapp" %} data-tooltip="Last used" data-variation="teal visible" data-position="right center" {% endif %}
data-bind="click: $root.show_modal('github-select')"
title="{{ provider_name }}">
<i class="fa-brands fa-github icon"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
action="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
{% csrf_token %}

{# djlint: off D018 #}
<button class="ui {{ button_classes }} button"
data-bind="click: save_login_method"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause the connected service page to throw exceptions and break. That view will not have this binding, so it can only be added conditionally.

data-provider="{{ provider.id }}"
type="submit"
{% if last_login_method == provider.id|lower %} data-tooltip="Last used" data-variation="teal visible" data-position="right center" {% endif %}
title="{{ provider.name }}">
{# djlint: on #}

<i class="fa-brands fa-{{ provider.name|lower }} icon"></i>
{% blocktrans trimmed with provider_name=provider.app.name|default:provider.name verbiage=verbiage|default:'Connect to' %}
{{ verbiage }} {{ provider_name }}
Expand Down
51 changes: 51 additions & 0 deletions src/js/account/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Registry } from "../application/registry";

/**
* LoginView manages login-related functionality including saving the
* last used login method to a cookie for session persistence.
*
* Usage in a binding context:
*
* .. code:: html
*
* <div data-bind="using: LoginView()">
* <form method="post" action="...">
* <button data-bind="click: save_login_method" data-provider="github">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted above how this should probably use KO patterns instead of mixing element attribute and KO patterns. I don't see where else we're using data-provider

* Log in using GitHub
* </button>
* </form>
* </div>
*/
export class LoginView {
static view_name = "LoginView";

constructor() {}

/**
* Save the provider used for login.
*
* This could be used like:
*
* .. code:: html
*
* <form method="post" action="...">
* <button data-bind="click: save_login_method" data-provider="github">
* Log in using GitHub
* </button>
* </form>
*
* @param {Object} data - Context data
* @param {Event} event - Click event
* @returns {knockout_click}
*/
save_login_method(data, event) {
const elem = event.currentTarget;
if (window.isSecureContext) {
console.debug("Setting last login method: ", elem.dataset.provider);
cookieStore.set("last-login-method", elem.dataset.provider);
}
return true;
}
}

Registry.add_view(LoginView);
1 change: 1 addition & 0 deletions src/js/application/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as plugins from "./plugins";
import { Registry } from "./registry";

// Application views
import * as account_views from "../account";
import * as build_views from "../build";
import * as core_views from "../core";
import * as gold_views from "../gold";
Expand Down
10 changes: 10 additions & 0 deletions src/js/application/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ export class ApplicationView {
if (found_modal.length === 0) {
console.debug("Modal not found:", selector);
}

// Remove "visible" variation property to remove "Last used" tooltip
const buttons = document.querySelectorAll(".button.ui");
for (const button of buttons) {
if (button.dataset.variation) {
const variation = button.dataset.variation.split(" ");
variation.pop("visible");
button.dataset.variation = variation.join(" ");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should always use KO view code to manipulate the DOM. Using JS queries or jQuery to manipulate the DOM in addition is mixing patterns and should be always avoided.

I'm a bit confused what this is trying to do though. What is the intention here? It also seems like this would affect all modals, which we don't want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the way I found to solve this issue:

Peek.2025-11-26.17-58.webm

I'm not sure how to solve it in a better way.

Copy link
Contributor

@agjohnson agjohnson Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah, there are better ways to achieve that.

I think part of what is making this complicated is mixing non-Knockout JS patterns, template code, and SUI HTML-only tooltips together. The display issues seem to be from HTML-only tooltips mostly too, so maybe that isn't a good direction. Instead the solution here would be more consistent if it leaned into using Knockout more, and JS driven tooltips/popups are more robust anyways.

};
}

Expand Down