frontend/release-notes.jsx (171 lines of code) (raw):
import { useCallback, useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
import { authPatch, authPost } from "./utils";
const tableStyles = {
width: "100%",
tableLayout: "auto",
borderCollapse: "collapse",
};
const cellStyles = {
padding: "4px",
border: "1px solid #ddd",
verticalAlign: "top",
};
function BugLink({ bug }) {
if (bug) {
return <a href={`https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`}>{bug}</a>;
}
return <p />;
}
function ReleaseSpecific({ note, removeNote, releaseApiUrl }) {
const makeReleaseSpecific = () => {
// intentionally discarding id, url, and releases
// eslint-disable-next-line no-unused-vars
const { id, url, releases, ...rest } = note;
const copy = {
...rest,
releases: [releaseApiUrl],
};
const body = JSON.stringify(copy);
authPost("/rna/notes/", body)
.then(() => removeNote(note))
.catch((err) => alert(err.message));
};
if (note.releases.length === 1) {
return <p>Yes</p>;
}
return <input type="button" value="Make release-specific" onClick={makeReleaseSpecific} />;
}
function NoteRow({ note, removeNote, releaseApiUrl, converter }) {
return (
<tr>
<td style={cellStyles}>
<a href={`/admin/rna/note/${note.id}/`}>Edit</a>
</td>
<td style={cellStyles}>
{note.is_known_issue && note.is_known_issue !== releaseApiUrl ? "Known issue" : note.tag}
</td>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Markdown is safe/trusted input */}
<td style={cellStyles} dangerouslySetInnerHTML={{ __html: converter.makeHtml(note.note) }} />
<td style={cellStyles}>
<BugLink bug={note.bug} />
</td>
<td style={cellStyles}>{note.sort_num}</td>
<td style={cellStyles}>
<ReleaseSpecific note={note} removeNote={removeNote} releaseApiUrl={releaseApiUrl} />
</td>
<td style={cellStyles}>
<input type="button" value="Remove" onClick={() => removeNote(note)} />
</td>
</tr>
);
}
function NoteRows({ data, removeNote, releaseApiUrl, converter }) {
return (
<tbody>
{data.map((note) => (
<NoteRow
key={note.id}
note={note}
removeNote={removeNote}
releaseApiUrl={releaseApiUrl}
converter={converter}
/>
))}
</tbody>
);
}
function NoteHeader({ data }) {
return (
<thead>
<tr>
{data.map((header) => (
<th key={header} style={cellStyles}>
{header}
</th>
))}
</tr>
</thead>
);
}
function NoteTable({ url, releaseApiUrl, converter }) {
const [data, setData] = useState([]);
const getNotes = useCallback(() => {
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then(setData)
.catch((err) => alert(`Unable to get notes: ${err.message}`));
}, [url]);
const addNote = useCallback(
(id) => {
fetch(`/rna/notes/${id}/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then((note) => {
const releases = JSON.stringify({ releases: [...note.releases, releaseApiUrl] });
authPatch(note.url, releases)
.then(getNotes)
.catch((err) => alert(err.message));
})
.catch((err) => alert(`Unable to add note: ${err.message}`));
},
[releaseApiUrl, getNotes],
);
const removeNote = (note) => {
const releases = JSON.stringify({
releases: note.releases.filter((url) => url !== releaseApiUrl),
});
authPatch(note.url, releases)
.then(getNotes)
.catch((err) => alert(err.message));
};
useEffect(() => {
getNotes();
const origLookup = window.dismissRelatedLookupPopup;
window.dismissRelatedLookupPopup = (win, chosenId) => {
addNote(chosenId);
origLookup(win, chosenId);
};
const origAddAnother = window.dismissAddRelatedObjectPopup;
window.dismissAddRelatedObjectPopup = (win, newId, newRepr) => {
addNote(newId);
origAddAnother(win, newId, newRepr);
};
}, [addNote, getNotes]);
const headers = [
"Edit",
"Tag/Known issue",
"Note",
"Bug",
"Sort num",
"Release-specific",
"Remove",
];
return (
<div style={{ width: "100%", overflowX: "auto" }}>
<table style={tableStyles}>
<NoteHeader data={headers} />
<NoteRows
data={data}
removeNote={removeNote}
releaseApiUrl={releaseApiUrl}
converter={converter}
/>
</table>
</div>
);
}
const rootEl = document.getElementById("note-table");
if (rootEl) {
const releaseId = rootEl.dataset.releaseid;
const releaseApiUrl = `${window.location.origin}/rna/releases/${releaseId}/`;
const notesApiUrl = `${releaseApiUrl}notes/`;
const converter = new window.Markdown.Converter();
const root = ReactDOM.createRoot(rootEl);
root.render(<NoteTable url={notesApiUrl} releaseApiUrl={releaseApiUrl} converter={converter} />);
}