mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: add support for technical details in changelog entries
This commit is contained in:
@@ -2,7 +2,11 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||
import { getSectionTitle, groupAndOrderChanges } from "../utils";
|
||||
import {
|
||||
getSectionTitle,
|
||||
groupAndOrderChanges,
|
||||
isSectionCollapsible,
|
||||
} from "../utils";
|
||||
import type { Change } from "../utils";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,6 +27,19 @@ function buildMarkdown({
|
||||
const { grouped, orderedTypes } = groupAndOrderChanges({ changes });
|
||||
|
||||
for (const type of orderedTypes) {
|
||||
if (isSectionCollapsible({ type })) {
|
||||
lines.push(
|
||||
"<details>",
|
||||
`<summary>${getSectionTitle({ type })}</summary>`,
|
||||
"",
|
||||
);
|
||||
for (const change of grouped[type]) {
|
||||
lines.push(`- ${change.text}`);
|
||||
}
|
||||
lines.push("", "</details>", "");
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(`## ${getSectionTitle({ type })}`);
|
||||
for (const change of grouped[type]) {
|
||||
lines.push(`- ${change.text}`);
|
||||
@@ -54,7 +71,10 @@ export function CopyMarkdownButton({
|
||||
size="sm"
|
||||
variant="text"
|
||||
onClick={handleCopy}
|
||||
className={cn("flex items-center gap-1.5", copied && "pointer-events-none")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5",
|
||||
copied && "pointer-events-none",
|
||||
)}
|
||||
title="Copy as markdown"
|
||||
>
|
||||
{copied ? (
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { getSectionTitle, groupAndOrderChanges } from "../utils";
|
||||
import type { Release } from "../utils";
|
||||
import {
|
||||
getSectionTitle,
|
||||
groupAndOrderChanges,
|
||||
isSectionCollapsible,
|
||||
type Change,
|
||||
type Release,
|
||||
} from "../utils";
|
||||
import { ArrowRightIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
export function ReleaseArticle({
|
||||
variant,
|
||||
@@ -85,22 +92,61 @@ export function ReleaseChanges({ release }: { release: Release }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{orderedTypes.map((type) => (
|
||||
<div key={type} className="flex flex-col gap-1.5">
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{getSectionTitle({ type })}:
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1.5">
|
||||
{grouped[type].map((change) => (
|
||||
<ReleaseChangeSection key={type} type={type} changes={grouped[type]} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseChangeSection({
|
||||
type,
|
||||
changes,
|
||||
}: {
|
||||
type: string;
|
||||
changes: Change[];
|
||||
}) {
|
||||
const title = getSectionTitle({ type });
|
||||
|
||||
if (isSectionCollapsible({ type })) {
|
||||
return (
|
||||
<details className="group flex flex-col gap-1.5">
|
||||
<summary className="flex w-fit cursor-pointer list-none items-center gap-1.5 text-base font-semibold text-foreground [&::-webkit-details-marker]:hidden">
|
||||
<span>{title}:</span>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRightIcon}
|
||||
className="size-3 shrink-0 text-muted-foreground group-open:rotate-90"
|
||||
/>
|
||||
</summary>
|
||||
<ReleaseChangeList changes={changes} />
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="text-base font-semibold text-foreground">{title}:</h3>
|
||||
<ReleaseChangeList changes={changes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseChangeList({
|
||||
changes,
|
||||
className,
|
||||
}: {
|
||||
changes: Change[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<ul className={cn("list-disc space-y-1.5 pl-5", className)}>
|
||||
{changes.map((change) => (
|
||||
<li
|
||||
key={change.text}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
className="text-base leading-relaxed text-foreground"
|
||||
>
|
||||
{change.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,35 +3,64 @@ import { allChangelogs } from "content-collections";
|
||||
export type Change = { type: string; text: string };
|
||||
export type Release = (typeof allChangelogs)[number];
|
||||
|
||||
const knownSectionOrder = ["new", "improved", "fixed", "breaking"];
|
||||
|
||||
const knownSectionTitles: Record<string, string> = {
|
||||
new: "Features",
|
||||
improved: "Improvements",
|
||||
fixed: "Fixes",
|
||||
breaking: "Breaking Changes",
|
||||
type ChangeSectionConfig = {
|
||||
title: string;
|
||||
order: number;
|
||||
collapsible?: boolean;
|
||||
};
|
||||
|
||||
export function getSectionTitle({ type }: { type: string }): string {
|
||||
const knownSectionConfigs: Record<string, ChangeSectionConfig> = {
|
||||
new: { title: "Features", order: 0 },
|
||||
improved: { title: "Improvements", order: 1 },
|
||||
fixed: { title: "Fixes", order: 2 },
|
||||
breaking: { title: "Breaking Changes", order: 3 },
|
||||
technical: {
|
||||
title: "Technical details",
|
||||
order: 4,
|
||||
collapsible: true,
|
||||
},
|
||||
};
|
||||
|
||||
function getSectionConfig({ type }: { type: string }): ChangeSectionConfig {
|
||||
return (
|
||||
knownSectionTitles[type] ?? type.charAt(0).toUpperCase() + type.slice(1)
|
||||
knownSectionConfigs[type] ?? {
|
||||
title: type.charAt(0).toUpperCase() + type.slice(1),
|
||||
order: Number.MAX_SAFE_INTEGER,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getSectionTitle({ type }: { type: string }): string {
|
||||
return getSectionConfig({ type }).title;
|
||||
}
|
||||
|
||||
export function isSectionCollapsible({ type }: { type: string }): boolean {
|
||||
return getSectionConfig({ type }).collapsible ?? false;
|
||||
}
|
||||
|
||||
export function groupAndOrderChanges({ changes }: { changes: Change[] }) {
|
||||
const typeEncounterOrder = new Map<string, number>();
|
||||
const grouped = changes.reduce<Record<string, Change[]>>((acc, change) => {
|
||||
if (!typeEncounterOrder.has(change.type)) {
|
||||
typeEncounterOrder.set(change.type, typeEncounterOrder.size);
|
||||
}
|
||||
if (!acc[change.type]) acc[change.type] = [];
|
||||
acc[change.type].push(change);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const customTypes = Object.keys(grouped).filter(
|
||||
(type) => !knownSectionOrder.includes(type),
|
||||
const orderedTypes = Object.keys(grouped).sort((left, right) => {
|
||||
const orderDifference =
|
||||
getSectionConfig({ type: left }).order -
|
||||
getSectionConfig({ type: right }).order;
|
||||
if (orderDifference !== 0) {
|
||||
return orderDifference;
|
||||
}
|
||||
return (
|
||||
(typeEncounterOrder.get(left) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(typeEncounterOrder.get(right) ?? Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
const orderedTypes = [
|
||||
...knownSectionOrder.filter((type) => grouped[type]?.length > 0),
|
||||
...customTypes,
|
||||
];
|
||||
});
|
||||
|
||||
return { grouped, orderedTypes };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user