extend bundled release notes

This commit is contained in:
callumalpass 2026-06-10 07:10:17 +10:00
parent 3a27991755
commit d9d6f511af
4 changed files with 130 additions and 33 deletions

View file

@ -34,6 +34,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
## Added
- Added Canvas Bases to the companion plugin documentation and first-install starter note. See [Canvas Bases](https://tasknotes.dev/companion-plugins/canvas-bases/) for setup and TaskNotes integration details.
- Added a TaskNotes starter note that is created and opened automatically on first install to introduce task creation, inline conversion, and embedded Bases views.
- Added a runtime API surface for companion plugins to show or populate the standard TaskNotes task context menu.
- (#2000) Added separate settings for using the active note as a project during normal task creation and during inline task creation or instant conversion. Thanks to @BlueScreen32 for suggesting the split.
@ -48,6 +49,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
- Added a local review-type lint check that catches dependency-resolution warnings from Obsidian's online review before submission.
- Improved dependency and project relationship cache updates so ordinary note edits avoid unnecessary relationship recalculation, while blocked and blocking state stays current when dependency status changes.
- Improved filter and view query caching so filter options update immediately after relevant task-field changes while large Task List, Kanban, and Calendar queries avoid repeated vault scans.
- Extended the in-app release notes bundle to include the current major release, with collapsed versions rendered only when opened.
## Fixed

View file

@ -28,7 +28,7 @@ function getVersionDate(version) {
}
}
// Get all release note files and bundle versions since last minor (includes pre-release versions)
// Get all release note files and bundle versions from the current major series (includes pre-release versions)
const releaseFiles = readdirSync("docs/releases")
.filter(f => f.match(/^\d+\.\d+\.\d+(?:-[\w.]+)?\.md$/))
.map(f => f.replace('.md', ''))
@ -46,21 +46,10 @@ if (!current) {
process.exit(1);
}
// Find all versions in current minor series (e.g., 3.25.x)
const currentMinorVersions = releaseFiles.filter(v =>
v.major === current.major && v.minor === current.minor
);
// Find all versions from previous minor series (e.g., 3.24.x)
const previousMinorVersions = releaseFiles.filter(v =>
v.major === current.major && v.minor === current.minor - 1
);
// Bundle current minor + all patches from previous minor
const versionsToBundle = [
...currentMinorVersions.map(v => v.full),
...previousMinorVersions.map(v => v.full)
];
// Bundle all patches from the current major series.
const versionsToBundle = releaseFiles
.filter(v => v.major === current.major)
.map(v => v.full);
// Fetch dates and sort by date (newest first)
const versionsWithDates = versionsToBundle.map(version => ({
@ -74,6 +63,7 @@ const versionsWithDates = versionsToBundle.map(version => ({
// Sort by date descending (newest first)
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
const bundledVersionList = versionsWithDates.map(({ version }) => version);
// Generate imports and metadata
const imports = versionsWithDates.map(({ version }, index) =>
@ -112,4 +102,4 @@ ${releaseNotesArray}
writeFileSync("src/releaseNotes.ts", content);
console.log(`✓ Generated release notes bundle for version ${currentVersion}`);
console.log(` Bundled versions: ${versionsToBundle.join(', ')}`);
console.log(` Bundled versions: ${bundledVersionList.join(', ')}`);

View file

@ -1,7 +1,7 @@
import { ItemView, WorkspaceLeaf, MarkdownRenderer } from "obsidian";
import { format, parseISO } from "date-fns";
import TaskNotesPlugin from "../main";
import { ReleaseNoteVersion } from "../releaseNotes";
import type { ReleaseNoteVersion } from "../releaseNotes";
export const RELEASE_NOTES_VIEW_TYPE = "tasknotes-release-notes";
@ -80,6 +80,24 @@ export class ReleaseNotesView extends ItemView {
}
}
private async renderVersionContent(
content: HTMLElement,
versionData: ReleaseNoteVersion
): Promise<void> {
// Transform issue references into clickable links and render the markdown
const transformedNotes = this.transformIssueLinks(versionData.content);
const releaseContent = versionData.isCurrent
? `${this.plugin.i18n.translate("views.releaseNotes.baseFilesNotice")}\n\n${transformedNotes}`
: transformedNotes;
await MarkdownRenderer.render(
this.plugin.app,
releaseContent,
content,
"",
this
);
}
/**
* Create a collapsible section for a release version
*/
@ -378,24 +396,26 @@ export class ReleaseNotesView extends ItemView {
content.classList.add("tn-static-padding-0-16px-16px-16px-f1aa998c");
content.style.display = isExpanded ? "block" : "none";
// Transform issue references into clickable links and render the markdown
const transformedNotes = this.transformIssueLinks(versionData.content);
const releaseContent = versionData.isCurrent
? `${this.plugin.i18n.translate("views.releaseNotes.baseFilesNotice")}\n\n${transformedNotes}`
: transformedNotes;
await MarkdownRenderer.render(
this.plugin.app,
releaseContent,
content,
"",
this
);
let renderPromise: Promise<void> | null = null;
const renderIfNeeded = () => {
if (!renderPromise) {
renderPromise = this.renderVersionContent(content, versionData);
}
return renderPromise;
};
if (isExpanded) {
await renderIfNeeded();
}
// Toggle functionality
header.addEventListener("click", () => {
const isCurrentlyExpanded = content.style.display !== "none";
content.style.display = isCurrentlyExpanded ? "none" : "block";
chevron.textContent = isCurrentlyExpanded ? "▶" : "▼";
if (!isCurrentlyExpanded) {
void renderIfNeeded();
}
});
}
@ -516,11 +536,11 @@ export class ReleaseNotesView extends ItemView {
starMessage.appendText(messageText);
}
// Render all versions
// Create all version sections
const versionsContainer = container.createEl("div", { cls: "release-notes-versions" });
for (let i = 0; i < this.releaseNotesBundle.length; i++) {
const versionData = this.releaseNotesBundle[i];
// Current version and first patch in minor series expanded, others collapsed
// Current version and newest bundled version expanded, others collapsed
const isExpanded = versionData.isCurrent || i === 0;
await this.createVersionSection(versionsContainer, versionData, isExpanded);
}

View file

@ -1,4 +1,6 @@
import { transformReleaseNoteIssueLinks } from "../../../src/views/ReleaseNotesView";
import { MarkdownRenderer } from "obsidian";
import { ReleaseNotesView, transformReleaseNoteIssueLinks } from "../../../src/views/ReleaseNotesView";
import type { ReleaseNoteVersion } from "../../../src/releaseNotes";
describe("transformReleaseNoteIssueLinks", () => {
it("links a single parenthesized issue reference", () => {
@ -26,3 +28,86 @@ describe("transformReleaseNoteIssueLinks", () => {
expect(output).toBe(input);
});
});
describe("ReleaseNotesView", () => {
const createView = () =>
new ReleaseNotesView(
{} as never,
{
app: {},
i18n: {
translate: (key: string) =>
key === "views.releaseNotes.baseFilesNotice" ? "Base files notice" : key,
},
} as never,
[],
"4.10.2"
);
const releaseNote = (overrides: Partial<ReleaseNoteVersion> = {}): ReleaseNoteVersion => ({
version: "4.10.1",
content: "Release content",
date: null,
isCurrent: false,
...overrides,
});
const createSection = async (versionData: ReleaseNoteVersion, isExpanded: boolean) => {
const view = createView();
const container = document.createElement("div");
await (view as unknown as {
createVersionSection: (
container: HTMLElement,
versionData: ReleaseNoteVersion,
isExpanded: boolean
) => Promise<void>;
}).createVersionSection(container, versionData, isExpanded);
return container;
};
const flushPromises = async () => {
await Promise.resolve();
await Promise.resolve();
};
beforeEach(() => {
jest.mocked(MarkdownRenderer.render).mockClear();
});
it("renders initially expanded release notes immediately", async () => {
const container = await createSection(releaseNote(), true);
expect(MarkdownRenderer.render).toHaveBeenCalledTimes(1);
expect(container.querySelector(".release-notes-version-content")?.textContent).toBe(
"Release content"
);
});
it("defers collapsed release note rendering until first expansion", async () => {
const container = await createSection(releaseNote(), false);
expect(MarkdownRenderer.render).not.toHaveBeenCalled();
expect(container.querySelector(".release-notes-version-content")?.textContent).toBe("");
container.querySelector<HTMLElement>(".release-notes-version-header")?.click();
await flushPromises();
expect(MarkdownRenderer.render).toHaveBeenCalledTimes(1);
expect(container.querySelector(".release-notes-version-content")?.textContent).toBe(
"Release content"
);
});
it("does not render the same release notes more than once", async () => {
const container = await createSection(releaseNote(), false);
const header = container.querySelector<HTMLElement>(".release-notes-version-header");
header?.click();
await flushPromises();
header?.click();
header?.click();
await flushPromises();
expect(MarkdownRenderer.render).toHaveBeenCalledTimes(1);
});
});