Update whats new modal logic
This commit is contained in:
@@ -16,11 +16,28 @@
|
|||||||
const currentVersion = $derived(versionQuery.data?.version ?? null);
|
const currentVersion = $derived(versionQuery.data?.version ?? null);
|
||||||
const buildDate = $derived(versionQuery.data?.build_date ?? null);
|
const buildDate = $derived(versionQuery.data?.build_date ?? null);
|
||||||
const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local');
|
const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local');
|
||||||
const currentRelease = $derived(
|
|
||||||
releaseHistoryQuery.data?.find((r) => r.tag_name === currentVersion) ??
|
function getMinorPrefix(tag: string): string | null {
|
||||||
(isDev ? releaseHistoryQuery.data?.[0] : null) ??
|
const m = tag.replace(/^v/, '').match(/^(\d+\.\d+)\./);
|
||||||
null
|
return m ? m[1] : null;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
// Collect all releases sharing the same minor version (e.g. v1.3.0, v1.3.1, …)
|
||||||
|
const minorReleases = $derived.by(() => {
|
||||||
|
const releases = releaseHistoryQuery.data;
|
||||||
|
if (!releases || releases.length === 0) return [];
|
||||||
|
|
||||||
|
const versionToMatch = isDev ? releases[0].tag_name : currentVersion;
|
||||||
|
if (!versionToMatch) return [];
|
||||||
|
|
||||||
|
const prefix = getMinorPrefix(versionToMatch);
|
||||||
|
if (!prefix) {
|
||||||
|
const exact = releases.find((r) => r.tag_name === versionToMatch);
|
||||||
|
return exact ? [exact] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases.filter((r) => getMinorPrefix(r.tag_name) === prefix);
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
updateAvailable = updateCheckQuery.data?.update_available ?? false;
|
updateAvailable = updateCheckQuery.data?.update_available ?? false;
|
||||||
@@ -31,10 +48,4 @@
|
|||||||
updateAvailable={updateCheckQuery.data?.update_available ?? false}
|
updateAvailable={updateCheckQuery.data?.update_available ?? false}
|
||||||
latestVersion={updateCheckQuery.data?.latest_version ?? null}
|
latestVersion={updateCheckQuery.data?.latest_version ?? null}
|
||||||
/>
|
/>
|
||||||
<WhatsNewModal
|
<WhatsNewModal {currentVersion} {buildDate} releases={minorReleases} />
|
||||||
{currentVersion}
|
|
||||||
{buildDate}
|
|
||||||
releaseTag={currentRelease?.tag_name ?? null}
|
|
||||||
releaseBody={currentRelease?.body ?? null}
|
|
||||||
releaseName={currentRelease?.name ?? null}
|
|
||||||
/>
|
|
||||||
|
|||||||
@@ -3,43 +3,53 @@
|
|||||||
import { isWhatsNewDismissed, dismissWhatsNew } from '$lib/stores/version.svelte';
|
import { isWhatsNewDismissed, dismissWhatsNew } from '$lib/stores/version.svelte';
|
||||||
import { renderMarkdown } from '$lib/utils/markdown';
|
import { renderMarkdown } from '$lib/utils/markdown';
|
||||||
import { X, Sparkles, ExternalLink } from 'lucide-svelte';
|
import { X, Sparkles, ExternalLink } from 'lucide-svelte';
|
||||||
|
import type { GitHubRelease } from '$lib/queries/VersionQuery.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentVersion: string | null;
|
currentVersion: string | null;
|
||||||
buildDate: string | null;
|
buildDate: string | null;
|
||||||
releaseTag: string | null;
|
releases: GitHubRelease[];
|
||||||
releaseBody: string | null;
|
|
||||||
releaseName: string | null;
|
|
||||||
}
|
}
|
||||||
let { currentVersion, buildDate, releaseTag, releaseBody, releaseName }: Props = $props();
|
let { currentVersion, buildDate, releases }: Props = $props();
|
||||||
|
|
||||||
let dialogEl: HTMLDialogElement | undefined = $state();
|
let dialogEl: HTMLDialogElement | undefined = $state();
|
||||||
let renderedBody = $state('');
|
let renderedSections: { tag: string; name: string | null; html: string }[] = $state([]);
|
||||||
|
|
||||||
const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local');
|
const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local');
|
||||||
|
const latestRelease = $derived(releases.length > 0 ? releases[0] : null);
|
||||||
|
|
||||||
// In dev: key dismissal to build_date so modal shows once per rebuild, not every refresh
|
// In dev: key dismissal to build_date so modal shows once per rebuild, not every refresh
|
||||||
// In prod: key to release tag so modal shows once per new version
|
// In prod: key to latest release tag so modal re-shows when a new patch lands
|
||||||
const dismissKey = $derived(isDev ? (buildDate ?? 'dev') : (releaseTag ?? currentVersion));
|
const dismissKey = $derived(
|
||||||
|
isDev ? (buildDate ?? 'dev') : (latestRelease?.tag_name ?? currentVersion)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasContent = $derived(releases.some((r) => r.body && r.body.trim().length > 0));
|
||||||
|
|
||||||
const shouldShow = $derived(
|
const shouldShow = $derived(
|
||||||
currentVersion !== null &&
|
currentVersion !== null && dismissKey !== null && hasContent && !isWhatsNewDismissed(dismissKey)
|
||||||
dismissKey !== null &&
|
|
||||||
releaseBody !== null &&
|
|
||||||
releaseBody.trim().length > 0 &&
|
|
||||||
!isWhatsNewDismissed(dismissKey)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (releaseBody && releaseBody.trim()) {
|
const withContent = releases.filter((r) => r.body && r.body.trim());
|
||||||
renderMarkdown(releaseBody)
|
if (withContent.length === 0) {
|
||||||
.then((html) => {
|
renderedSections = [];
|
||||||
renderedBody = html;
|
return;
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
renderedBody = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
withContent.map(async (r) => ({
|
||||||
|
tag: r.tag_name,
|
||||||
|
name: r.name,
|
||||||
|
html: await renderMarkdown(r.body!)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.then((sections) => {
|
||||||
|
renderedSections = sections;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
renderedSections = [];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -102,22 +112,28 @@
|
|||||||
|
|
||||||
<div class="divider my-0 opacity-10"></div>
|
<div class="divider my-0 opacity-10"></div>
|
||||||
|
|
||||||
{#if releaseName}
|
{#if renderedSections.length > 0}
|
||||||
<p class="text-base-content mt-4 mb-4 text-sm font-semibold border-l-2 border-accent/50 pl-3">
|
|
||||||
{releaseName}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if renderedBody}
|
|
||||||
<div
|
<div
|
||||||
class="whats-new-content release-notes-prose prose prose-sm max-h-[55vh] max-w-none text-base-content/75 overflow-y-auto rounded-lg border border-base-content/5 bg-base-100/50 p-4 {releaseName
|
class="whats-new-content release-notes-prose max-h-[55vh] max-w-none overflow-y-auto rounded-lg border border-base-content/5 bg-base-100/50 p-4 mt-4"
|
||||||
? ''
|
|
||||||
: 'mt-4'}"
|
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via DOMPurify -->
|
{#each renderedSections as section, i}
|
||||||
{@html renderedBody}
|
{#if i > 0}
|
||||||
|
<div class="divider my-4 opacity-20"></div>
|
||||||
|
{/if}
|
||||||
|
<p
|
||||||
|
class="text-base-content text-sm font-semibold border-l-2 border-accent/50 pl-3 {i > 0
|
||||||
|
? ''
|
||||||
|
: 'mt-0'} mb-3"
|
||||||
|
>
|
||||||
|
{section.name ?? section.tag}
|
||||||
|
</p>
|
||||||
|
<div class="prose prose-sm max-w-none text-base-content/75">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via DOMPurify -->
|
||||||
|
{@html section.html}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if hasContent}
|
||||||
<div class="flex justify-center py-12">
|
<div class="flex justify-center py-12">
|
||||||
<span class="loading loading-spinner loading-md text-accent/60"></span>
|
<span class="loading loading-spinner loading-md text-accent/60"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user