Skip to content
Merged
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
174 changes: 91 additions & 83 deletions compendium/compendium/page/docs/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ frappe.ui.DocsBrowser = class DocsBrowser {
this.current_path = null;
this.current_locale = frappe.boot.lang || "en";
this.locales = [];
this._view_seq = 0;
this.layout_ready = Promise.resolve();
this.setup_layout();
}
Expand Down Expand Up @@ -83,24 +84,28 @@ frappe.ui.DocsBrowser = class DocsBrowser {
}

show() {
if (this._ignore_next_show) {
this._ignore_next_show = false;
return;
}

Comment thread
greptile-apps[bot] marked this conversation as resolved.
// Bump before ensure_locales so an in-flight switch_locale cannot apply
// after this navigation started but before load_view takes the sequence.
const seq = ++this._view_seq;
const route = frappe.get_route();
this.ensure_locales().then(() => {
if (seq !== this._view_seq) {
return;
}
const parsed = this.parse_route(route);
if (!parsed.locale) {
return this.resolve_and_redirect(parsed.path);
}
this.current_locale = parsed.locale;
this.load_tree().then(() => {
this.update_locale_picker();
if (parsed.path === null) {
if (!this.tree_data.length) {
this.show_empty_state();
return;
}
return this.select_first_page();
}
this.load_page(parsed.path);
});
if (parsed.path === null) {
return this.load_view("", true);
}
this.load_view(parsed.path);
});
}

Expand Down Expand Up @@ -139,44 +144,37 @@ frappe.ui.DocsBrowser = class DocsBrowser {
}
}

update_locale_picker() {
if (!this.locales.length) {
return Promise.resolve();
}

return frappe
.xcall("compendium.docs.get_page_variants", { path: this.current_path || "" })
.then((variants) => {
this.render_locale_picker(variants || []);
})
.catch(() => {
this.render_locale_picker(this.locales);
});
}

switch_locale(locale) {
if (!locale || locale === this.current_locale) {
return;
}

const path = this.current_path || "";
const route = ["docs", locale];
if (path) {
route.push(...path.split("/"));
}
const seq = ++this._view_seq;
frappe.xcall("compendium.docs.get_view", { path, locale }).then((view) => {
if (seq !== this._view_seq) {
if (this.$locale_select.val() === locale) {
this.$locale_select.val(this.current_locale);
}
return;
}

frappe
.xcall("compendium.docs.get_page", { path, locale })
.then(() => frappe.set_route(route))
.catch(() => {
frappe.xcall("compendium.docs.get_first_page", { locale }).then((first_path) => {
const fallback = ["docs", locale];
if (first_path) {
fallback.push(...first_path.split("/"));
}
frappe.set_route(fallback);
});
});
const target_path = view.page ? path : view.first_path;
const route = ["docs", locale];
if (target_path) {
route.push(...target_path.split("/"));
}

if (view.page || target_path == null || target_path === path) {
this.current_locale = locale;
this._ignore_next_show = true;
frappe.set_route(route);
this.apply_view(view);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return;
}

frappe.set_route(route);
});
}

parse_route(route) {
Expand All @@ -203,9 +201,13 @@ frappe.ui.DocsBrowser = class DocsBrowser {
}

resolve_and_redirect(path) {
const seq = this._view_seq;
return frappe
.xcall("compendium.docs.resolve_locale", { path: path || "" })
.then((result) => {
if (seq !== this._view_seq) {
return;
}
const route = ["docs", result.locale];
if (result.path) {
route.push(...result.path.split("/"));
Expand All @@ -214,19 +216,9 @@ frappe.ui.DocsBrowser = class DocsBrowser {
});
}

load_tree() {
return frappe
.xcall("compendium.docs.get_tree", { locale: this.current_locale })
.then((tree) => {
this.tree_data = tree || [];
this.page_paths = this.collect_page_paths(this.tree_data);
this.render_tree();
})
.catch(() => {
this.tree_data = [];
this.page_paths = new Set();
this.render_tree();
});
apply_tree(tree) {
this.tree_data = tree || [];
this.page_paths = this.collect_page_paths(this.tree_data);
}

collect_page_paths(nodes) {
Expand Down Expand Up @@ -328,22 +320,6 @@ frappe.ui.DocsBrowser = class DocsBrowser {
}
}

select_first_page() {
return frappe
.xcall("compendium.docs.get_first_page", { locale: this.current_locale })
.then((path) => {
if (path === null || path === undefined) {
this.show_empty_state();
return;
}
if (path === "") {
this.load_page("");
return;
}
this.navigate_to(path, true);
});
}

navigate_to(path, replace_route = false) {
const route = ["docs", this.current_locale];
if (path) {
Expand All @@ -359,36 +335,68 @@ frappe.ui.DocsBrowser = class DocsBrowser {
frappe.set_route(route);
return;
}
this.load_page(path);
this.load_view(path);
}

load_page(path) {
this.current_path = path;
this.expand_ancestors(path);
this.render_tree();
load_view(path, resolve_first = false) {
this.show_loading();

const seq = ++this._view_seq;
return frappe.call({
method: "compendium.docs.get_page",
args: { path, locale: this.current_locale },
method: "compendium.docs.get_view",
args: {
path: path || "",
locale: this.current_locale,
resolve_first: resolve_first ? 1 : 0,
},
callback: (response) => {
if (response.exc_type) {
if (seq !== this._view_seq) {
return;
}
if (response.exc_type && response.message == null) {
this.show_error(response, path);
return;
}
this.show_page(response.message);
this.apply_view(response.message || {}, { resolve_first });
},
});
}

apply_view(view, { resolve_first = false } = {}) {
this.apply_tree(view.tree);
this.render_locale_picker(view.variants || []);

if (resolve_first && view.path != null && view.path !== "") {
this._ignore_next_show = true;
this.navigate_to(view.path, true);
}

if (!view.page) {
this.current_path = view.path;
this.expand_ancestors(view.path);
this.render_tree();
if (!view.exc_type) {
this.show_empty_state();
} else {
this.show_error({ exc_type: view.exc_type }, view.path);
}
return;
}

this.current_path = view.page.path;
this.expand_ancestors(this.current_path);
this.render_tree();
this.show_page(view.page, view.variants);
}

show_loading() {
this.$state.addClass("hide");
this.$reading.removeClass("hide").addClass("docs-loading").html("");
this.$toc_pane.addClass("hide");
this.$content.removeClass("has-toc");
}

show_page(doc) {
show_page(doc, variants) {
this.$reading.removeClass("docs-loading hide");
this.$state.addClass("hide");
this.page.set_title(doc.title || __("Documentation"));
Expand All @@ -405,7 +413,7 @@ frappe.ui.DocsBrowser = class DocsBrowser {
this.scroll_to_heading();
this.highlight_code();
this.update_breadcrumbs(doc.path, doc.title);
this.update_locale_picker();
this.render_locale_picker(variants || []);
}

follow_in_page_anchor(event) {
Expand Down Expand Up @@ -561,7 +569,7 @@ frappe.ui.DocsBrowser = class DocsBrowser {
this.$state.removeClass("hide");
this.$state_message.text(__("No documentation is available for your account."));
this.update_breadcrumbs();
this.update_locale_picker();
this.render_locale_picker([]);
}

show_error(response, path) {
Expand Down
73 changes: 65 additions & 8 deletions compendium/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,70 @@ def get_page(path: str = "", locale: str | None = None):
"""Return rendered HTML and metadata for a documentation page at the given logical path."""
locale = normalize_locale(locale)
page = get_page_record(normalize_path(path), locale=locale, check_permission=True)
return build_page_payload(page, locale)


@frappe.whitelist()
def get_view(path: str = "", locale: str | None = None, resolve_first: int | bool = 0):
"""Return everything the docs browser needs for one screen.

Always includes the navigation tree. When the requested page is missing or not
permitted, `page` is None and `exc_type` explains why — the request still
succeeds so the client can render the sidebar. Pass `resolve_first` when the
route has a locale but no path; the first accessible page is selected.
"""
locale = normalize_locale(locale)
pages = discover_pages(locale)
tree = build_navigation_tree(locale)
first_path = get_first_page_path(pages)

if cint(resolve_first):
path = first_path
else:
path = normalize_path(path)

if path is None:
return {
"tree": tree,
"page": None,
"variants": [],
"path": None,
"first_path": None,
"exc_type": None,
}

page_record = pages.get(path)
if not page_record:
return {
"tree": tree,
"page": None,
"variants": [],
"path": path,
"first_path": first_path,
"exc_type": "DoesNotExistError",
}

if not is_permitted(page_record):
return {
"tree": tree,
"page": None,
"variants": [],
"path": path,
"first_path": first_path,
"exc_type": "PermissionError",
}

return {
"tree": tree,
"page": build_page_payload(page_record, locale),
"variants": get_page_variants(path),
"path": path,
"first_path": first_path,
"exc_type": None,
}


def build_page_payload(page, locale):
content = render_page_content(page.body, page.path, locale)
user_roles = set(get_user_roles())
matching_roles = [role for role in page.roles if role in user_roles]
Expand All @@ -52,14 +116,7 @@ def get_first_page(locale: str | None = None):
"""Return the logical path of the first accessible documentation page."""
locale = normalize_locale(locale)
pages = discover_pages(locale)
permitted = sorted(
(page for page in pages.values() if is_permitted(page)),
key=lambda page: (page.order, page.title.lower(), page.path),
)
if not permitted:
return None

return permitted[0].path
return get_first_page_path(pages)


@frappe.whitelist()
Expand Down
Loading
Loading