Merge pull request #813 from tdurieux/fix/saved-options-and-folder-icons

fix: preserve saved auto-update settings and clarify file tree icons
This commit is contained in:
Thomas Durieux
2026-09-09 14:10:32 +02:00
committed by GitHub
7 changed files with 118 additions and 49 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{
"core.min.js": "core.c5bd53363a.min.js",
"vendor.min.js": "vendor.741a424da5.min.js",
"vendor.min.js": "vendor.0f10f9accb.min.js",
"mermaid.min.js": "mermaid.f848a72d16.min.js",
"all.min.css": "all.076c089579.min.css",
"all.min.css": "all.7ac8730b9a.min.css",
"markdown.min.js": "markdown.ad7b1d71c3.min.js",
"pdf.min.js": "pdf.eaa7573247.min.js",
"notebook.min.js": "notebook.8844e2735f.min.js",
+1 -1
View File
File diff suppressed because one or more lines are too long
+30 -24
View File
@@ -1419,7 +1419,7 @@ a:hover {
background: rgba(128, 128, 128, 0.15);
color: var(--color);
}
.files .file.active > a .tree-icon-file { background: #5B7BA5; }
.files .file.active > a .tree-icon-file { color: #5B7BA5; }
.files .file.active > a .tree-count { color: var(--ink-muted); }
/* Tree icon shapes */
@@ -1429,37 +1429,43 @@ a:hover {
flex-shrink: 0;
}
/* Collapsed folder: outlined square */
/* Compact folder outline with a tab; expansion adds a light tint. */
.tree-icon-folder {
width: 9px;
height: 9px;
border-radius: 2px;
background: transparent;
border: 1.2px solid var(--ink-muted);
opacity: 0.45;
}
/* Open folder: filled brown square */
.files .folder.open > a .tree-icon-folder {
background: #A07D2E;
border-color: #A07D2E;
width: 14px;
height: 12px;
color: var(--ink-muted);
opacity: 0.75;
}
.tree-icon-folder svg {
display: block;
width: 100%;
height: 100%;
fill: currentColor;
fill-opacity: 0;
stroke: currentColor;
stroke-width: 1.2;
stroke-linejoin: round;
}
.files .folder.open > a .tree-icon-folder {
color: #A07D2E;
opacity: 0.9;
}
.files .folder.open > a .tree-icon-folder svg {
fill-opacity: 0.12;
}
.dark-mode .files .folder.open > a .tree-icon-folder {
background: #C9A03A;
border-color: #C9A03A;
color: #C9A03A;
}
.tree-icon-file {
width: 6px;
height: 6px;
background: #5B7BA5;
transform: rotate(45deg);
border-radius: 1px;
margin: 0 2px;
opacity: 0.7;
width: 16px;
font-size: 14px;
text-align: center;
color: #5B7BA5;
}
.dark-mode .tree-icon-file {
background: #7BA3D0;
.dark-mode .tree-icon-file,
.dark-mode .files .file.active > a .tree-icon-file {
color: #7BA3D0;
}
/* Expand/collapse triangle */
+16 -3
View File
@@ -1006,6 +1006,7 @@ export const anonymizeController = function (state, http, html, params, location
state._originalBranch = res.data.source.branch;
state.options = Object.assign({}, state.options, res.data.options);
state.conference = res.data.conference;
state._originalConference = res.data.conference;
state.repositoryID = res.data.source.repositoryID;
state._originalRepositoryID = res.data.source.repositoryID;
if (res.data.options.expirationDate) {
@@ -1030,6 +1031,7 @@ export const anonymizeController = function (state, http, html, params, location
state.source = res.data.source;
state.options = Object.assign({}, state.options, res.data.options);
state.conference = res.data.conference;
state._originalConference = res.data.conference;
if (res.data.options.expirationDate) {
state.options.expirationDate = new Date(res.data.options.expirationDate);
}
@@ -1062,6 +1064,7 @@ export const anonymizeController = function (state, http, html, params, location
state.source = res.data.source;
state.options = Object.assign({}, state.options, res.data.options);
state.conference = res.data.conference;
state._originalConference = res.data.conference;
if (res.data.options.expirationDate) {
state.options.expirationDate = new Date(res.data.options.expirationDate);
}
@@ -1486,12 +1489,20 @@ export const anonymizeController = function (state, http, html, params, location
// ========== SHARED LOGIC ==========
function getConference() {
if (!state.conference) return;
http.get("/api/conferences/" + state.conference).then(
const conference = state.conference;
state.conference_data = null;
if (!conference) return;
const preserveSavedOptions =
state.isUpdate && conference === state._originalConference;
http.get("/api/conferences/" + conference).then(
(res) => {
if (state.conference !== conference) return;
state.conference_data = res.data;
state.conference_data.startDate = new Date(state.conference_data.startDate);
state.conference_data.endDate = new Date(state.conference_data.endDate);
// Conference defaults must not overwrite an existing submission's
// saved settings when its edit form loads (#791).
if (preserveSavedOptions) return;
state.options.expirationDate = new Date(state.conference_data.endDate);
state.options.expirationMode = "remove";
state.options.update = state.conference_data.options.update;
@@ -1500,7 +1511,9 @@ export const anonymizeController = function (state, http, html, params, location
state.options.notebook = state.conference_data.options.notebook;
state.options.link = state.conference_data.options.link;
},
() => { state.conference_data = null; }
() => {
if (state.conference === conference) state.conference_data = null;
}
);
}
+2 -1
View File
@@ -86,7 +86,8 @@ export default {
const open = matching.value ? opens[path] !== false : !!opens[path];
const truncated = folder && props.page?.options?.truncatedFolders?.includes(path.slice(1));
const count = props.page?.fileCounts?.[path.slice(1)] || 0;
const icon = h("span", { class: folder ? "tree-icon-folder" : "tree-icon-file" });
const icon = h("span", { class: folder ? "tree-icon-folder" : "far fa-file tree-icon-file", "aria-hidden": "true" },
folder ? h("svg", { viewBox: "0 0 14 12", focusable: "false" }, h("path", { d: "M1.5 10.5v-9h4l1.5 2h5.5v7z" })) : null);
const label = h("span", { class: "tree-name" }, name);
const onClick = event => {
focus(event.currentTarget);
+18 -18
View File
File diff suppressed because one or more lines are too long
+49
View File
@@ -45,6 +45,55 @@ function explorer() {
}
describe("frontend production regressions", function () {
describe("conference defaults on edit (#791)", function () {
const conferenceData = () => ({
startDate: "2026-01-01", endDate: "2026-12-31",
options: { update: true, image: true, pdf: true, notebook: true, link: true },
});
function anonymize(params = {}) {
const h = harness();
h.defs.anonymizeController(h.scope, h.http, {}, params, {}, () => {}, Object.assign(() => 0, { cancel() {} }));
return h;
}
for (const id of ["repoId", "pullRequestId", "gistId"]) {
it(`preserves saved options when reopening a conference-linked ${id}`, async function () {
const h = anonymize({ [id]: "saved" });
h.requests[0].resolve({ data: { options: { update: true } } }); await h.flush();
h.requests.at(-1).resolve({ data: {
source: { fullName: "owner/repo", repositoryFullName: "owner/repo", branch: "main", commit: "abcdef", gistId: "123", pullRequestId: 1 },
conference: "conf",
options: { terms: [], update: false, image: false, expirationMode: "remove", expirationDate: "2026-11-01" },
} }); await h.flush();
h.watches.conference();
h.requests.at(-1).resolve({ data: conferenceData() }); await h.flush();
expect(h.scope.options.update).to.equal(false);
expect(h.scope.options.image).to.equal(false);
expect(h.scope.options.expirationDate.toISOString()).to.equal("2026-11-01T00:00:00.000Z");
expect(h.scope.conference_data).not.to.equal(null);
if (id === "repoId") {
h.scope.anonymizeRepo({ target: {} });
expect(h.requests.at(-1).body.options.update).to.equal(false);
expect(h.requests.at(-1).url).to.equal("/api/repo/saved");
}
});
}
it("applies defaults when selecting a different conference", async function () {
const h = anonymize();
h.scope.isUpdate = true; h.scope._originalConference = "old";
h.scope.conference = "new"; h.watches.conference();
h.requests.at(-1).resolve({ data: conferenceData() }); await h.flush();
expect(h.scope.options.update).to.equal(true);
expect(h.scope.options.expirationDate.toISOString()).to.equal("2026-12-31T00:00:00.000Z");
});
it("ignores defaults arriving after a conference is deselected", async function () {
const h = anonymize(); h.scope.conference = "conf"; h.watches.conference();
const pending = h.requests.at(-1);
h.scope.conference = ""; h.watches.conference();
pending.resolve({ data: conferenceData() }); await h.flush();
expect(h.scope.options.update).to.equal(false);
expect(h.scope.conference_data).to.equal(null);
});
});
it("sanitizes Org output before trusting it", async function () {
const h = explorer(); let untrusted;
h.context.Org = { Parser: function () { this.parse = () => ({ convert: () => ({ toString: () => '<img onerror="probe()">' }) }); }, ConverterHTML: {} };