refactor: reuse ChangelogModal in HeaderOsVersion component (#1607)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- View OS release notes directly for the currently displayed version,
with a link to open in a new tab.
- Changelog modal supports viewing a specific release outside the update
flow.
- Improvements
- Changelog modal prioritizes a prettier docs view when available, with
fallback to raw notes.
- Enhanced theming with better dark mode support, including Azure theme
alignment.
- Clearer modal behavior: consistent open/close handling and titles
based on the selected release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Pujit Mehrotra
2025-08-21 12:11:04 -04:00
committed by GitHub
parent 6edd3a3d16
commit fd895cacf0
4 changed files with 99 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
{
"version": "4.14.0",
"version": "4.15.1",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],

View File

@@ -1545,7 +1545,7 @@ type InfoVersions implements Node {
core: CoreVersions!
"""Software package versions"""
packages: PackageVersions!
packages: PackageVersions
}
type Info implements Node {

View File

@@ -6,14 +6,14 @@ import { useQuery } from '@vue/apollo-composable';
import { BellAlertIcon, ExclamationTriangleIcon, InformationCircleIcon, DocumentTextIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { Badge, DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@unraid/ui';
import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE, getReleaseNotesUrl } from '~/helpers/urls';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { INFO_VERSIONS_QUERY } from './UserProfile/versions.query';
import ReleaseNotesModal from '~/components/ReleaseNotesModal.vue';
import ChangelogModal from '~/components/UpdateOs/ChangelogModal.vue';
const { t } = useI18n();
@@ -36,6 +36,23 @@ const displayOsVersion = computed(() => versionsResult.value?.info?.versions?.co
const apiVersion = computed(() => versionsResult.value?.info?.versions?.core?.api || null);
const showOsReleaseNotesModal = ref(false);
// Create release object for current version to pass to ChangelogModal
const currentVersionRelease = computed(() => {
if (!displayOsVersion.value) return null;
const version = displayOsVersion.value;
const releaseNotesUrl = getReleaseNotesUrl(version).toString();
return {
version,
name: `Unraid ${version}`,
date: undefined, // We don't know the release date for current version
changelog: null, // No raw changelog for current version
changelogPretty: releaseNotesUrl,
sha256: null, // No update functionality for current version
};
});
const openApiChangelog = () => {
window.open('https://github.com/unraid/api/releases', '_blank');
};
@@ -184,10 +201,10 @@ const updateOsStatus = computed(() => {
</div>
<!-- OS Release Notes Modal -->
<ReleaseNotesModal
v-if="displayOsVersion"
<ChangelogModal
:open="showOsReleaseNotesModal"
:version="displayOsVersion"
:release="currentVersionRelease"
view-docs-label="Open in new tab"
:t="t"
@close="showOsReleaseNotesModal = false"
/>

View File

@@ -23,23 +23,61 @@ import Modal from '~/components/Modal.vue';
export interface Props {
open?: boolean;
t: ComposerTranslation;
// When provided, uses prop data instead of store data (for viewing release notes)
release?: {
version: string;
name?: string;
date?: string;
changelog?: string | null;
changelogPretty?: string;
sha256?: string | null;
} | null;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
release: null,
});
const emit = defineEmits<{
close: [];
}>();
const purchaseStore = usePurchaseStore();
const updateOsStore = useUpdateOsStore();
const themeStore = useThemeStore();
const { darkMode } = storeToRefs(themeStore);
const { darkMode, theme } = storeToRefs(themeStore);
const isDarkMode = computed(() => {
if (theme.value.name === 'azure') {
return true;
}
return darkMode.value;
});
const { availableWithRenewal, releaseForUpdate, changelogModalVisible } = storeToRefs(updateOsStore);
const { setReleaseForUpdate, fetchAndConfirmInstall } = updateOsStore;
// Determine if we're in prop mode (viewing specific release) or store mode (update workflow)
const isPropMode = computed(() => !!props.release);
// Use prop data when provided, store data when not
const currentRelease = computed(() => props.release || releaseForUpdate.value);
// Modal visibility: use prop when in prop mode, store visibility when in store mode
const modalVisible = computed(() => isPropMode.value ? props.open : changelogModalVisible.value);
const showExtendKeyButton = computed(() => {
return availableWithRenewal.value;
return !isPropMode.value && availableWithRenewal.value;
});
// Handle modal closing - emit event in prop mode, clear store in store mode
const handleClose = () => {
if (isPropMode.value) {
emit('close');
} else {
setReleaseForUpdate(null);
}
};
// iframe navigation handling
const iframeRef = ref<HTMLIFrameElement | null>(null);
const hasNavigated = ref(false);
@@ -47,11 +85,11 @@ const currentIframeUrl = ref<string | null>(null);
const actualIframeSrc = ref<string | null>(null);
const docsChangelogUrl = computed(() => {
return releaseForUpdate.value?.changelogPretty ?? null;
return currentRelease.value?.changelogPretty ?? null;
});
const showRawChangelog = computed<boolean>(() => {
return !docsChangelogUrl.value && !!releaseForUpdate.value?.changelog;
return Boolean(!docsChangelogUrl.value && currentRelease.value?.changelog);
});
const handleDocsPostMessages = (event: MessageEvent) => {
@@ -80,7 +118,7 @@ const handleDocsPostMessages = (event: MessageEvent) => {
const sendThemeToIframe = () => {
if (iframeRef.value && iframeRef.value.contentWindow) {
try {
const message = { type: 'theme-update', theme: darkMode.value ? 'dark' : 'light' };
const message = { type: 'theme-update', theme: isDarkMode.value ? 'dark' : 'light' };
iframeRef.value.contentWindow.postMessage(message, '*');
} catch (error) {
console.error('Failed to send theme to iframe:', error);
@@ -120,7 +158,7 @@ watch(docsChangelogUrl, (newUrl) => {
});
// Only need to watch for theme changes
watch(darkMode, () => {
watch(isDarkMode, () => {
// The iframe will only pick up the message if it has sent theme-ready
sendThemeToIframe();
});
@@ -129,16 +167,16 @@ watch(darkMode, () => {
<template>
<Modal
v-if="releaseForUpdate?.version"
v-if="currentRelease?.version"
:center-content="false"
max-width="max-w-[800px]"
:open="changelogModalVisible"
:open="modalVisible"
:show-close-x="true"
:t="t"
:tall-content="true"
:title="t('Unraid OS {0} Changelog', [releaseForUpdate.version])"
:title="t('Unraid OS {0} Changelog', [currentRelease.version])"
:disable-overlay-close="false"
@close="setReleaseForUpdate(null)"
@close="handleClose"
>
<template #main>
<div class="flex flex-col gap-4 min-w-[280px] sm:min-w-[400px]">
@@ -156,12 +194,12 @@ watch(darkMode, () => {
<!-- Fallback to raw changelog -->
<RawChangelogRenderer
v-else-if="showRawChangelog && releaseForUpdate?.changelog"
:changelog="releaseForUpdate?.changelog"
:version="releaseForUpdate?.version"
:date="releaseForUpdate?.date"
v-else-if="showRawChangelog && currentRelease?.changelog"
:changelog="currentRelease?.changelog"
:version="currentRelease?.version"
:date="currentRelease?.date"
:t="t"
:changelog-pretty="releaseForUpdate?.changelogPretty"
:changelog-pretty="currentRelease?.changelogPretty"
/>
<!-- Loading state -->
@@ -189,32 +227,34 @@ watch(darkMode, () => {
<!-- View on docs button -->
<BrandButton
v-if="currentIframeUrl || releaseForUpdate?.changelogPretty"
v-if="currentIframeUrl || currentRelease?.changelogPretty"
variant="underline"
:external="true"
:href="currentIframeUrl || releaseForUpdate?.changelogPretty"
:icon="ArrowTopRightOnSquareIcon"
:href="currentIframeUrl || currentRelease?.changelogPretty"
:icon-right="ArrowTopRightOnSquareIcon"
aria-label="View on Docs"
/>
</div>
<!-- Action buttons -->
<BrandButton
v-if="showExtendKeyButton"
:icon="KeyIcon"
:icon-right="ArrowTopRightOnSquareIcon"
@click="purchaseStore.renew()"
>
{{ props.t('Extend License to Update') }}
</BrandButton>
<BrandButton
v-else-if="releaseForUpdate?.sha256"
:icon="ServerStackIcon"
:icon-right="ArrowRightIcon"
@click="fetchAndConfirmInstall(releaseForUpdate.sha256)"
>
{{ props.t('Continue') }}
</BrandButton>
<!-- Action buttons (only in store mode for update workflow) -->
<template v-if="!isPropMode">
<BrandButton
v-if="showExtendKeyButton"
:icon="KeyIcon"
:icon-right="ArrowTopRightOnSquareIcon"
@click="purchaseStore.renew()"
>
{{ props.t('Extend License to Update') }}
</BrandButton>
<BrandButton
v-else-if="currentRelease?.sha256"
:icon="ServerStackIcon"
:icon-right="ArrowRightIcon"
@click="fetchAndConfirmInstall(currentRelease.sha256)"
>
{{ props.t('Continue') }}
</BrandButton>
</template>
</div>
</template>
</Modal>