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
4 changes: 2 additions & 2 deletions readthedocsext/theme/static/readthedocsext/theme/js/site.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{% load trans blocktrans from i18n %}
{% load user_has_github_app_account from readthedocs.socialaccounts %}

{% comment rst %}
Project automatic configuration view
Expand Down Expand Up @@ -32,8 +31,8 @@

{# Search prompt and dropdown #}
<label>{% trans "Repository name:" %}</label>
<div class="ui fluid disabled loading search"
data-bind="semanticui: {search: search_config()}, css: {disabled: is_loading(), loading: is_loading()}">
<div class="ui fluid disabled loading scrolling search"
data-bind="semanticui: {search: search_config(), popup: search_popup_config()}, css: {disabled: is_loading(), loading: is_loading()}">
<div class="ui fluid icon large input">
<input class="ui text" type="text" autofocus=true />
<i class="fa-duotone fa-search icon"></i>
Expand All @@ -53,7 +52,7 @@
{% endcomment %}
<script type="text/html" id="remote-repo-results">
{# fmt:off #}
<div data-bind="foreach: remote_repos" class="results">
<div data-bind="foreach: remote_repos">
<a class="result">
<div class="image">
<img data-bind="attr: {src: avatar_url}">
Expand All @@ -77,6 +76,123 @@
{% endblock project_add_automatic_search_result %}

</div>

{% comment %}
Popup to trigger help modal

This is triggered manually from the ``search_popup_config()`` observable
and is set up using the ``semanticui.popup`` binding on the sibling
element above.
{% endcomment %}
<div class="ui small wide primary popup">
<div class="header">{% trans "Not finding a repository?" %}</div>
<div class="description">
<p>
{% blocktrans trimmed %}
This list might be out of date or reconfiguration might be necessary to update repositories.
{% endblocktrans %}
</p>
<a class="ui small compact right floated inverted button"
data-bind="click: show_modal">{% trans "Repair" %}</a>
</div>
</div>

{% comment %}
Modal for resyncing and repair repository/organization permissions

This content is moved to a modal so that we have more UI to explain what is
happening to the user, especially in the case that the user is connected
via GHA and needs to reconfigure the application permissions.
{% endcomment %}
<div class="ui small modal"
data-bind="semanticui: { modal: search_modal_config() }">
<div class="header">
{% blocktrans trimmed %}
Repairing repository listing
{% endblocktrans %}
</div>
<div class="content">
<div class="ui segment">
<div class="ui small divided items">

{% if "githubapp" in socialaccount_providers %}
<div class="item">
<div class="content">
<div class="description">
{% comment %}
This terminology comes from the documentation for GitHub App management.
Our GHA is "installed" for users or organizations and GitHub users
"grant access to repositories" (or on the GHA installation page
"install GHA _for repositories_").
{% endcomment %}
{# TODO detect if users have only installed on individual repositories so we can be more definite with this guidance #}
{% blocktrans trimmed %}
If our GitHub App was only granted access to select repositories,
repositories without access will not display in your repository list.
You will need to grant access to additional repositories each time you create a project.
{% endblocktrans %}
</div>
<div class="extra">
<a class="ui right floated button"
href="https://github.com/apps/{{ GITHUB_APP_NAME }}/installations/new/"
target="_blank">
<i class="fa-brands fa-github icon"></i>
{% trans "Update GitHub App installation" %}
</a>
</div>
</div>
</div>
{% endif %}

{# This should show for all providers and manually connected repositories #}
<div class="item">
<div class="content">
<div class="description">
{% blocktrans trimmed %}
Your connected service account might be out of sync
and a manual refresh may be required to update your list of repositories.
{% endblocktrans %}
</div>
<div class="extra">
<a class="ui right floated button"
data-bind="click: sync_remote_repos, css: {disabled: is_syncing(), loading: is_syncing(), positive: is_synced()}">
<i class="fa-duotone fa-refresh icon"
data-bind="css: {'fa-refresh': !is_synced(), 'fa-check': is_synced()}"></i>
{% trans "Refresh your repositories" %}
</a>
</div>
</div>
</div>

{% if "github" in socialaccount_providers %}
{# This doesn't cover GitHub App providers, the fix for GHA is different #}
<div class="item">
<div class="content">
<div class="description">
{% blocktrans trimmed %}
If your repository list does not include repositories from one of your GitHub organizations,
our GitHub application may not be approved by the organization yet.
In this case, you should try requesting approval of our GitHub application from the organization.
{% endblocktrans %}
</div>
<div class="extra">
<a class="ui right floated button"
href="https://docs.readthedocs.com/platform/stable/reference/git-integration.html#github-permission-troubleshooting">
<i class="fa-duotone fa-circle-check icon"></i>
{% trans "Learn how to request approval" %}
</a>
</div>
</div>
</div>
{% endif %}

</div>
</div>
</div>
<div class="actions">
<div class="ui cancel button">{% trans "Go back" %}</div>
</div>
</div>
{% endblock project_add_automatic_search %}

{% block project_add_automatic_placeholder %}
Expand All @@ -99,28 +215,6 @@
</div>

</div>
<div class="ui small bottom attached center aligned message">
<p>{% trans "Can't find the repository you are searching for?" %}</p>

<p>
{% if user|user_has_github_app_account %}
<a class="ui mini black basic compact button"
href="https://github.com/apps/{{ GITHUB_APP_NAME }}/installations/new/"
target="_blank">
<i class="fa-brands fa-github icon"></i>
{% trans "Update GitHub App permissions" %}
</a>

{% trans "or" %}
{% endif %}

<button class="ui mini black basic compact button"
data-bind="click: sync_remote_repos, css: {disabled: is_syncing(), loading: is_syncing()}">
<i class="fa-duotone fa-refresh icon"></i>
{% trans "Refresh your repositories" %}
</button>
</p>
</div>
</div>
{% endblock project_add_automatic_placeholder %}

Expand Down
27 changes: 20 additions & 7 deletions src/js/application/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,6 @@ export const semanticui = {
const binding_value = ko.unwrap(value_accessor());
const jq_element = jquery(element);
for (const [key, value] of Object.entries(binding_value)) {
if (key === "modal") {
// modal is not supported here because the jQuery ``modal()`` plugin
// replaces ``<body>`` and this causes an error from Knockout, because
// the binding was applied to ``<body>`` more than once.
console.error("SemanticUI modal instantiation is not supported.");
return;
}
if (value !== undefined) {
if (typeof value === "function") {
const callback = (behavior, ...args) => {
Expand All @@ -361,6 +354,26 @@ export const semanticui = {
};
value(callback);
} else {
if (key === "modal") {
// We do something fun here and move the element into ``body``
// before the SUI initialization. The reason for this is that SUI
// will move the element automatically already, however when it
// does the KO bindings are all re-evaluated. This will cause
// exceptions to be thrown around duplicate binding definitions. To
// make the ``modal`` module play with KO nicely, we mark the modal
// as _not detachable_, meaning SUI won't move the element when
// initializing, and move the element manually so that the
// positioning is relative to ``body`` instead of being relative to
// the modal element's parent element. If detachable is manually
// configured, throw an error.
if (value.detachable == true) {
throw new Error(
"Setting a modal as detachable is not supported by the semanticui binding.",
);
}
value.detachable = false;
document.body.prepend(element);
}
// The value is probably an object, and is almost certainly a module
// configuration for initializing the module
console.debug(
Expand Down
53 changes: 53 additions & 0 deletions src/js/project/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ export class ProjectCreateView extends ResponsiveView {
/** Configuration passed in via :func:`~application.plugins.jsonInit`
* @observable {Object} Search configuration */
this.search_config = ko.observable();
/** @observable {Object} Search popup module configuration */
this.search_popup_config = ko.observable();
/** @observable {Object} Search modal module configuration */
this.search_modal_config = ko.observable(undefined);
/** @observable {Object} The selected repository */
this.selected = ko.observable();
/** @observable {Boolean} Is UI loading from the API currently? */
this.is_loading = ko.observable(false);
/** @observable {Boolean} Are remote repositories current resyncing? */
this.is_syncing = ko.observable(false);
/** @observable {Boolean} Are remote repositories done resyncing? */
this.is_synced = ko.observable(false);
/** @computed {Boolean} Is there a selected repository? */
this.is_selected = ko.computed(() => {
return this.selected() !== undefined;
Expand Down Expand Up @@ -94,6 +100,7 @@ export class ProjectCreateView extends ResponsiveView {
token: config.csrf_token,
};

this.is_synced(false);
this.is_syncing(true);
this.is_loading(true);

Expand All @@ -106,6 +113,7 @@ export class ProjectCreateView extends ResponsiveView {
.always(() => {
this.is_syncing(false);
this.is_loading(false);
this.is_synced(true);
});

return promise;
Expand All @@ -128,6 +136,31 @@ export class ProjectCreateView extends ResponsiveView {
const config = this.config();
const url = config.urls.remoterepository_list + "?full_name={query}";

// Configuration for the trigger of the popup element. We manually show the
// popup in the case that the user has tried searching multiple times
// unsuccessfully, or has a query with no results.
let attemptsRemaining = 3;
this.search_popup_config({
on: "manual",
position: "top right",
hoverable: true,
closable: true,
preserve: true,
onHidden: () => {
// If the user did something to hide the popup, like click outside the
// popup, reset the attempts so that the popup can show again.
attemptsRemaining = 3;
},
});

// Show repair modal immediately on view load if the URL contains `#repair` hash.
// Use this for linking users in support directly to this modal.
const show_modal = jquery(location).attr("hash") == "#repair";
this.search_modal_config({
autoShow: show_modal,
centered: false,
});

this.search_config({
// We use a Knockout template here, embedded in the template as a script
// element. This avoids string interpolation in JS and keeps HTML in one
Expand All @@ -153,6 +186,9 @@ export class ProjectCreateView extends ResponsiveView {
return output;
},
},
error: {
noResultsHeader: "No matching repositories found",
},
apiSettings: {
url: url,
},
Expand All @@ -167,6 +203,17 @@ export class ProjectCreateView extends ResponsiveView {
onSelect: (result, response) => {
this.selected(new RemoteRepository(result));
},
// Listen for results and decide to show the resync popup based on what
// the user's interaction with search results.
onResults: (response, fromCache) => {
if ((response && response.count == 0) || attemptsRemaining <= 0) {
// Search results are empty or user tried searching multiple times
// unsuccessfully so far. Calls with the behavior style call supported by
// :js:func:`application.plugins.semanticui`.
this.search_popup_config((popup) => popup("show"));
}
attemptsRemaining--;
},
});
}

Expand All @@ -177,5 +224,11 @@ export class ProjectCreateView extends ResponsiveView {
}
return true;
}

/** Show search modal */
show_modal() {
this.search_popup_config((popup) => popup("hide"));
this.search_modal_config((modal) => modal("show"));
}
}
Registry.add_view(ProjectCreateView);