Skip to content

Commit a81e062

Browse files
mart-eAntoineVDV
andcommitted
[IMP] odoo_theme: introduce fallback URLs for the page switchers
When a user clicks on the link of an alternate page in the version or language switcher, we now check if the page referenced by the target URL exists or not. If not, we generate a series of fallback URLs from the target URL and check whether the targeted resource exists or not, until we read the root of the documentation. As soon as we find a valid URL, we redirect the user to it. This is inspired by the behaviour of docs.python.org's version and language switchers. task-2534669 closes #2052 X-original-commit: 25e863a Signed-off-by: Antoine Vandevenne (anv) <[email protected]> Signed-off-by: Martin Trigaux (mat) <[email protected]> Co-authored-by: Antoine Vandevenne (anv) <[email protected]>
1 parent d5d232b commit a81e062

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

extensions/odoo_theme/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def setup(app):
1414
app.add_js_file('js/layout.js')
1515
app.add_js_file('js/menu.js')
1616
app.add_js_file('js/page_toc.js')
17+
app.add_js_file('js/switchers.js')
1718

1819
return {
1920
'parallel_read_safe': True,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
(function ($) {
2+
3+
document.addEventListener('DOMContentLoaded', () => {
4+
// Enable fallback URLs for broken redirects from the version or language switchers.
5+
_prepareSwitchersFallbacks();
6+
});
7+
8+
/**
9+
* Add event listeners on links in the version and language switchers.
10+
*
11+
* If a link is clicked, the user is redirected to the closest fallback URL (including the
12+
* original target URL) that is available.
13+
*/
14+
const _prepareSwitchersFallbacks = () => {
15+
document.querySelectorAll('a[class="dropdown-item"]').forEach(element => {
16+
element.addEventListener('click', async event => {
17+
if (element.hasAttribute('href')) {
18+
event.preventDefault();
19+
const fallbackUrls = await _generateFallbackUrls(element.getAttribute('href'));
20+
const fallbackUrl = await _getFirstValidUrl(fallbackUrls);
21+
window.location.href = fallbackUrl;
22+
}
23+
});
24+
});
25+
};
26+
27+
})();

extensions/odoo_theme/static/js/utils.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,100 @@ const _prepareAccordion = (tocElement) => {
6363
tocEntryList.parentNode.insertBefore(tocEntryWrapper, tocEntryList);
6464
});
6565
};
66+
67+
/**
68+
* Generate a list of fallback URLs from the closest to the furthest of the target URL and
69+
* return the first one that points to an existing resource.
70+
*
71+
* The generation consists of starting with the target URL and walking back toward the root of
72+
* the documentation while alternating between including the original language or not, if it was
73+
* included in the original URL. The last fallback URL is the root of the documentation with the
74+
* version stripped off to redirect the user to the index of the default version.
75+
*
76+
* Example:
77+
* 1. .../documentation/13.0/contributing/documentation.html
78+
* 2. .../documentation/13.0/contributing.html
79+
* 3. .../documentation/13.0
80+
* 4. .../documentation/
81+
*
82+
* Example:
83+
* 1. .../documentation/15.0/fr/administration/install.html
84+
* 2. .../documentation/15.0/administration/install.html
85+
* 3. .../documentation/15.0/fr/administration.html
86+
* 4. .../documentation/15.0/administration.html
87+
* 5. .../documentation/15.0/fr/
88+
* 6. .../documentation/15.0/
89+
* 7. .../documentation/
90+
*/
91+
const _generateFallbackUrls = async (targetUrl) => {
92+
93+
const _deconstructUrl = (urlObject) => {
94+
let version = '';
95+
let language = '';
96+
const originalPathParts = [];
97+
for (let fragment of urlObject.pathname.split('/').reverse()) {
98+
if (fragment.match(/^(?:saas-)?\d{2}\.\d$|^master$/)) {
99+
version = fragment;
100+
} else if (fragment.match(/^[a-z]{2}(?:_[A-Z]{2})?$/)) {
101+
language = fragment;
102+
} else if (fragment.length > 0) {
103+
originalPathParts.unshift(fragment);
104+
}
105+
}
106+
return [version, language, originalPathParts];
107+
};
108+
109+
const targetUrlObject = new URL(targetUrl);
110+
const [version, language, originalPathParts] = _deconstructUrl(targetUrlObject);
111+
const urlBase = targetUrlObject.origin;
112+
113+
// Generate the fallback URLs.
114+
const fallbackUrls = [];
115+
for (let i = originalPathParts.length; i >= 0; i--) {
116+
const fallbackPathParts = originalPathParts.slice(0, i);
117+
118+
// Append '.html' to the last path part if it is missing and the part is not the root.
119+
if (
120+
fallbackPathParts.length > 0
121+
&& !fallbackPathParts[fallbackPathParts.length - 1].endsWith('.html')
122+
) {
123+
fallbackPathParts[fallbackPathParts.length - 1] += '.html';
124+
}
125+
126+
// Build the fallback URL from the version, language and path parts, if any.
127+
if (version && language)
128+
fallbackUrls.push(
129+
`${urlBase}/${version}/${language}/${fallbackPathParts.join('/')}`,
130+
`${urlBase}/${version}/${fallbackPathParts.join('/')}`,
131+
);
132+
else if (version && !language)
133+
fallbackUrls.push(`${urlBase}/${version}/${fallbackPathParts.join('/')}`);
134+
else if (!version && language)
135+
fallbackUrls.push(
136+
`${urlBase}/${language}/${fallbackPathParts.join('/')}`,
137+
`${urlBase}/${fallbackPathParts.join('/')}`,
138+
);
139+
else if (!version && !language)
140+
fallbackUrls.push(`${urlBase}/${fallbackPathParts.join('/')}`);
141+
}
142+
return fallbackUrls;
143+
};
144+
145+
/**
146+
* Iterate over the provided URLs and return the first one that points to a valid resource.
147+
*
148+
* Since URLs don't have a protocol and cannot be fetched when the documentation is built locally
149+
* without the `ROOT` and `IS_REMOTE_BUILD` Make arguments, the URLs that don't have the protocol
150+
* 'http' or 'https' are not tested.
151+
*/
152+
const _getFirstValidUrl = async (urls) => {
153+
for (let url of urls) {
154+
if (url.startsWith('http')) {
155+
const response = await fetch(url);
156+
if (response.ok) {
157+
return url;
158+
}
159+
}
160+
}
161+
return urls[0]; // No valid URL found, return the first one.
162+
};

0 commit comments

Comments
 (0)