Fix settings page compatibility and add debug access
This commit is contained in:
@@ -157,6 +157,22 @@
|
||||
<div id="ml-storage-info" class="fieldDescription"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="ml-section">
|
||||
<legend>Debug</legend>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="ml-debug-item-id">Jellyfin item ID</label>
|
||||
<input id="ml-debug-item-id" type="text" class="emby-input ml-input" placeholder="32-character item ID">
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="ml-debug-user-id">User ID</label>
|
||||
<input id="ml-debug-user-id" type="text" class="emby-input ml-input" placeholder="Optional; leave blank for your own settings">
|
||||
</div>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-debug-open" is="emby-button" type="button" class="raised"><span>Open debug view</span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">The selected user’s classification and translation rules are shown. Viewing another user requires administrator access.</div>
|
||||
</fieldset>
|
||||
|
||||
<button id="ml-save" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button>
|
||||
<button id="ml-reload" is="emby-button" type="button" class="raised"><span>Reload</span></button>
|
||||
</form>
|
||||
@@ -1030,6 +1046,14 @@
|
||||
document.getElementById("ml-cache-refresh").addEventListener("click", loadCacheDiagnostics);
|
||||
document.getElementById("ml-cache-clear").addEventListener("click", clearCacheEntries);
|
||||
document.getElementById("ml-refresh-activity-refresh").addEventListener("click", loadRefreshDiagnostics);
|
||||
document.getElementById("ml-debug-open").addEventListener("click", function () {
|
||||
var itemId = document.getElementById("ml-debug-item-id").value.trim();
|
||||
if (!itemId) {
|
||||
M.setStatus(status, "Enter a Jellyfin item ID.", true);
|
||||
return;
|
||||
}
|
||||
window.open(M.debugUrl(itemId, document.getElementById("ml-debug-user-id").value.trim()), "_blank", "noopener");
|
||||
});
|
||||
document.getElementById("ml-export-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); M.showModal("ml-export-modal"); });
|
||||
document.getElementById("ml-import-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); M.showModal("ml-import-modal"); });
|
||||
document.getElementById("ml-cleanup-open").addEventListener("click", function () { document.getElementById("ml-cleanup-confirm").checked = false; M.showModal("ml-cleanup-modal"); });
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<label class="ml-inline-check">
|
||||
<span>View:</span>
|
||||
<select id="ml-debug-mode" class="emby-select ml-select">
|
||||
<option value="resolved">Resolved for current user</option>
|
||||
<option value="resolved">Resolved for selected user</option>
|
||||
<option value="stored">Stored facts and translations</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -44,12 +44,17 @@
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
function targetUserId() {
|
||||
return new URLSearchParams(location.search).get("userId") || "";
|
||||
}
|
||||
|
||||
function endpoint() {
|
||||
var id = encodeURIComponent(itemId());
|
||||
var locale = document.documentElement.lang || navigator.language || "";
|
||||
var suffix = locale ? "?mlLocale=" + encodeURIComponent(locale) : "";
|
||||
return mode.value === "stored"
|
||||
? "/Multilang/Debug/" + id
|
||||
if (mode.value === "stored") return "/Multilang/Debug/" + id;
|
||||
return targetUserId()
|
||||
? "/Multilang/Debug/" + id + "/" + encodeURIComponent(targetUserId()) + suffix
|
||||
: "/Multilang/Debug/" + id + "/self" + suffix;
|
||||
}
|
||||
|
||||
@@ -193,7 +198,7 @@
|
||||
|
||||
async function load() {
|
||||
M.setStatus(status, "Loading...");
|
||||
meta.textContent = "Item: " + itemId() + " | " + mode.options[mode.selectedIndex].text;
|
||||
meta.textContent = "Item: " + itemId() + " | " + mode.options[mode.selectedIndex].text + (targetUserId() ? " | User: " + targetUserId() : "");
|
||||
try {
|
||||
var data = await M.get(endpoint());
|
||||
lastJson = JSON.stringify(data, null, 2);
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
(function () {
|
||||
if (!Element.prototype.replaceChildren) {
|
||||
Element.prototype.replaceChildren = function () {
|
||||
this.textContent = "";
|
||||
for (let index = 0; index < arguments.length; index++) this.appendChild(arguments[index]);
|
||||
};
|
||||
}
|
||||
|
||||
const basePath = () => {
|
||||
const path = window.location.pathname || "";
|
||||
const marker = "/web/";
|
||||
@@ -37,6 +44,15 @@
|
||||
return h;
|
||||
}
|
||||
|
||||
function debugUrl(itemId, userId) {
|
||||
const query = new URLSearchParams();
|
||||
const t = token() || new URLSearchParams(location.search).get("token") || new URLSearchParams(location.search).get("api_key") || "";
|
||||
if (t) query.set("token", t);
|
||||
if (userId) query.set("userId", userId);
|
||||
const suffix = query.toString();
|
||||
return `${basePath()}/Multilang/DebugUi/${encodeURIComponent(String(itemId || "").trim())}${suffix ? `?${suffix}` : ""}`;
|
||||
}
|
||||
|
||||
async function request(path, options) {
|
||||
const response = await fetch(`${basePath()}${path}`, {
|
||||
cache: "no-store",
|
||||
@@ -142,6 +158,7 @@
|
||||
get: (path) => request(path),
|
||||
post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }),
|
||||
basePath,
|
||||
debugUrl,
|
||||
authHeaders,
|
||||
splitList,
|
||||
setStatus,
|
||||
|
||||
@@ -39,6 +39,18 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="ml-section">
|
||||
<legend>Debug</legend>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="ml-debug-item-id">Jellyfin item ID</label>
|
||||
<input id="ml-debug-item-id" type="text" class="emby-input ml-input" placeholder="32-character item ID">
|
||||
</div>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-debug-open" is="emby-button" type="button" class="raised"><span>Open debug view</span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">Shows how your classification and translation rules resolve for this item.</div>
|
||||
</fieldset>
|
||||
|
||||
<button id="ml-save-user" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button>
|
||||
<button id="ml-reset-user" is="emby-button" type="button" class="raised"><span>Reload</span></button>
|
||||
</form>
|
||||
@@ -1264,6 +1276,14 @@
|
||||
state.rules.SortLocale = $("ml-sort-locale").value || "Auto";
|
||||
renderSortLocaleAutoText();
|
||||
});
|
||||
$("ml-debug-open").addEventListener("click", function () {
|
||||
var itemId = $("ml-debug-item-id").value.trim();
|
||||
if (!itemId) {
|
||||
M.setStatus(status, "Enter a Jellyfin item ID.", true);
|
||||
return;
|
||||
}
|
||||
window.open(M.debugUrl(itemId), "_blank", "noopener");
|
||||
});
|
||||
$("ml-user-export").addEventListener("click", function () {
|
||||
downloadUserExport().catch(function (err) { M.setStatus(status, String(err), true); });
|
||||
});
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<Version>0.2.2</Version>
|
||||
<AssemblyVersion>0.2.2.0</AssemblyVersion>
|
||||
<FileVersion>0.2.2.0</FileVersion>
|
||||
<Version>0.2.3</Version>
|
||||
<AssemblyVersion>0.2.3.0</AssemblyVersion>
|
||||
<FileVersion>0.2.3.0</FileVersion>
|
||||
<Authors>ajp_anton</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
return parts.path === USER_RULES_ROUTE && parts.params.get("multilang") === "1";
|
||||
}
|
||||
|
||||
function isPreferencesMenuRoute() {
|
||||
const path = hashParts().path;
|
||||
return path === "mypreferences" || path === "mypreferencesmenu";
|
||||
}
|
||||
|
||||
function pathSegments(pathname) {
|
||||
return String(pathname || "")
|
||||
.toLowerCase()
|
||||
@@ -120,7 +125,7 @@
|
||||
|
||||
function ensureUserRulesLink() {
|
||||
let menuItem = document.getElementById(MENU_ID);
|
||||
if (!(location.hash || "").toLowerCase().includes("mypreferences")) {
|
||||
if (!isPreferencesMenuRoute()) {
|
||||
menuItem?.remove();
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +137,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const links = [...document.querySelectorAll('a[href^="#/mypreferences"]')];
|
||||
const preferencesPage = visiblePage();
|
||||
const links = preferencesPage ? [...preferencesPage.querySelectorAll('a[href^="#/mypreferences"]')] : [];
|
||||
const insertAfter = links[links.length - 1];
|
||||
if (!insertAfter) return;
|
||||
|
||||
@@ -421,7 +427,10 @@
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => mutations.forEach((m) => {
|
||||
if (m.type === "childList") m.addedNodes.forEach(scanNode);
|
||||
if (m.type === "childList") {
|
||||
m.addedNodes.forEach(scanNode);
|
||||
ensureUserRulesLink();
|
||||
}
|
||||
if (m.type === "attributes") rewriteElementImages(m.target);
|
||||
}));
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "src", "data-src"] });
|
||||
|
||||
@@ -185,10 +185,51 @@ test('saves admin configuration through the dashboard page', async ({ page, requ
|
||||
expect(config.PrecacheTvShowLibraries).toBeFalsy();
|
||||
});
|
||||
|
||||
test('renders provider controls without native replaceChildren support', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(Element.prototype, 'replaceChildren', { configurable: true, value: undefined });
|
||||
});
|
||||
await signIn(page);
|
||||
await page.goto('/web/#/configurationpage?name=Multilang');
|
||||
await expect(page.locator('#ml-provider-inputs input[data-provider]')).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('opens a resolved debug view from both settings pages', async ({ page }) => {
|
||||
await signIn(page);
|
||||
|
||||
await page.goto('/web/#/configurationpage?name=Multilang');
|
||||
await expect(page.locator('#ml-admin')).toBeVisible();
|
||||
await page.locator('#ml-debug-item-id').fill(fixture.orderedItemIds[0]);
|
||||
await page.locator('#ml-debug-user-id').fill(fixture.userId);
|
||||
const [adminDebug] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('#ml-debug-open').click()
|
||||
]);
|
||||
await expect(adminDebug).toHaveURL(new RegExp(`/Multilang/DebugUi/${fixture.orderedItemIds[0]}.*userId=${fixture.userId}`));
|
||||
await expect(adminDebug.locator('#ml-debug-status')).toHaveText('Loaded.');
|
||||
await adminDebug.close();
|
||||
|
||||
await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`);
|
||||
await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'User Menu' }).click();
|
||||
await expect(page.locator('.MuiMenu-list #mlUserRulesMenuItem')).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.locator('#mlUserRulesMenuItem').click();
|
||||
await expect(page.locator('#ml-user')).toBeVisible();
|
||||
await page.locator('#ml-debug-item-id').fill(fixture.orderedItemIds[0]);
|
||||
const [userDebug] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('#ml-debug-open').click()
|
||||
]);
|
||||
await expect(userDebug).toHaveURL(new RegExp(`/Multilang/DebugUi/${fixture.orderedItemIds[0]}`));
|
||||
await expect(userDebug).not.toHaveURL(/userId=/);
|
||||
await expect(userDebug.locator('#ml-debug-status')).toHaveText('Loaded.');
|
||||
await userDebug.close();
|
||||
});
|
||||
|
||||
test('saves a structured classification rule through the user settings page', async ({ page, request }) => {
|
||||
await signIn(page);
|
||||
await page.goto(`/web/#/mypreferences?userId=${fixture.userId}`);
|
||||
await page.getByRole('button', { name: 'User Menu' }).click();
|
||||
await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`);
|
||||
await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible();
|
||||
await page.locator('#mlUserRulesMenuItem').click();
|
||||
await expect(page.locator('#ml-user')).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user