feat: send active unraid theme to docs (#1400)

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

- **New Features**
- The changelog modal now automatically syncs its theme (dark or light
mode) with the rest of the application.
- Added a new test button to preview the changelog from a localhost URL.
- **Improvements**
- Enhanced navigation handling within the embedded changelog, ensuring
only allowed links are followed.
- Improved communication between the modal and its embedded content for
a more seamless user experience.
- Updated the app layout to better support light and dark themes with
consistent background and text colors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-05-21 13:35:30 -07:00
committed by GitHub
parent 9ce2fee380
commit f71943b62b
3 changed files with 138 additions and 58 deletions

View File

@@ -10,13 +10,14 @@ import {
ServerStackIcon, ServerStackIcon,
} from '@heroicons/vue/24/solid'; } from '@heroicons/vue/24/solid';
import { BrandButton, BrandLoading } from '@unraid/ui'; import { BrandButton, BrandLoading } from '@unraid/ui';
import { allowedDocsOriginRegex, allowedDocsUrlRegex } from '~/helpers/urls';
import type { ComposerTranslation } from 'vue-i18n'; import type { ComposerTranslation } from 'vue-i18n';
import RawChangelogRenderer from '~/components/UpdateOs/RawChangelogRenderer.vue'; import RawChangelogRenderer from '~/components/UpdateOs/RawChangelogRenderer.vue';
import { usePurchaseStore } from '~/store/purchase'; import { usePurchaseStore } from '~/store/purchase';
import { useThemeStore } from '~/store/theme';
import { useUpdateOsStore } from '~/store/updateOs'; import { useUpdateOsStore } from '~/store/updateOs';
import { allowedDocsOriginRegex, allowedDocsUrlRegex } from '~/helpers/urls';
export interface Props { export interface Props {
open?: boolean; open?: boolean;
@@ -29,6 +30,8 @@ const props = withDefaults(defineProps<Props>(), {
const purchaseStore = usePurchaseStore(); const purchaseStore = usePurchaseStore();
const updateOsStore = useUpdateOsStore(); const updateOsStore = useUpdateOsStore();
const themeStore = useThemeStore();
const { darkMode } = storeToRefs(themeStore);
const { availableWithRenewal, releaseForUpdate, changelogModalVisible } = storeToRefs(updateOsStore); const { availableWithRenewal, releaseForUpdate, changelogModalVisible } = storeToRefs(updateOsStore);
const { setReleaseForUpdate, fetchAndConfirmInstall } = updateOsStore; const { setReleaseForUpdate, fetchAndConfirmInstall } = updateOsStore;
@@ -40,6 +43,7 @@ const showExtendKeyButton = computed(() => {
const iframeRef = ref<HTMLIFrameElement | null>(null); const iframeRef = ref<HTMLIFrameElement | null>(null);
const hasNavigated = ref(false); const hasNavigated = ref(false);
const currentIframeUrl = ref<string | null>(null); const currentIframeUrl = ref<string | null>(null);
const actualIframeSrc = ref<string | null>(null);
const docsChangelogUrl = computed(() => { const docsChangelogUrl = computed(() => {
return releaseForUpdate.value?.changelogPretty ?? null; return releaseForUpdate.value?.changelogPretty ?? null;
@@ -49,36 +53,50 @@ const showRawChangelog = computed<boolean>(() => {
return !docsChangelogUrl.value && !!releaseForUpdate.value?.changelog; return !docsChangelogUrl.value && !!releaseForUpdate.value?.changelog;
}); });
const handleIframeNavigationMessage = (event: MessageEvent) => { const handleDocsPostMessages = (event: MessageEvent) => {
// Common checks for all iframe messages
if ( if (
event.data && event.data &&
event.data.type === 'unraid-docs-navigation' &&
iframeRef.value && iframeRef.value &&
event.source === iframeRef.value.contentWindow && event.source === iframeRef.value.contentWindow &&
allowedDocsOriginRegex.test(event.origin) (allowedDocsOriginRegex.test(event.origin) || event.origin === 'http://localhost:3000')
) { ) {
if ( // Handle navigation events
typeof event.data.url === 'string' && if (event.data.type === 'unraid-docs-navigation') {
allowedDocsUrlRegex.test(event.data.url) if (typeof event.data.url === 'string' && allowedDocsUrlRegex.test(event.data.url)) {
) { hasNavigated.value = event.data.url !== docsChangelogUrl.value;
if (event.data.url !== docsChangelogUrl.value) { currentIframeUrl.value = event.data.url;
hasNavigated.value = true;
} else {
hasNavigated.value = false;
} }
currentIframeUrl.value = event.data.url; }
// Handle theme ready events
else if (event.data.type === 'theme-ready') {
sendThemeToIframe();
} }
} }
}; };
// Keep this function just for the watch handler
const sendThemeToIframe = () => {
if (iframeRef.value && iframeRef.value.contentWindow) {
try {
const message = { type: 'theme-update', theme: darkMode.value ? 'dark' : 'light' };
iframeRef.value.contentWindow.postMessage(message, '*');
} catch (error) {
console.error('Failed to send theme to iframe:', error);
}
}
};
// Attach event listener right away instead of waiting for mount
onMounted(() => { onMounted(() => {
window.addEventListener('message', handleIframeNavigationMessage); // Set initial values only
// Set initial value window.addEventListener('message', handleDocsPostMessages);
currentIframeUrl.value = docsChangelogUrl.value; currentIframeUrl.value = docsChangelogUrl.value;
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('message', handleIframeNavigationMessage); window.removeEventListener('message', handleDocsPostMessages);
}); });
const revertToInitialChangelog = () => { const revertToInitialChangelog = () => {
@@ -92,7 +110,20 @@ const revertToInitialChangelog = () => {
watch(docsChangelogUrl, (newUrl) => { watch(docsChangelogUrl, (newUrl) => {
currentIframeUrl.value = newUrl; currentIframeUrl.value = newUrl;
hasNavigated.value = false; hasNavigated.value = false;
if (newUrl) {
actualIframeSrc.value = newUrl;
} else {
actualIframeSrc.value = null;
}
}); });
// Only need to watch for theme changes
watch(darkMode, () => {
// The iframe will only pick up the message if it has sent theme-ready
sendThemeToIframe();
});
</script> </script>
<template> <template>
@@ -113,10 +144,11 @@ watch(docsChangelogUrl, (newUrl) => {
<!-- iframe for changelog if available --> <!-- iframe for changelog if available -->
<div v-if="docsChangelogUrl" class="w-[calc(100%+3rem)] h-[475px] -mx-6 -my-6"> <div v-if="docsChangelogUrl" class="w-[calc(100%+3rem)] h-[475px] -mx-6 -my-6">
<iframe <iframe
v-if="actualIframeSrc"
ref="iframeRef" ref="iframeRef"
:src="docsChangelogUrl" :src="actualIframeSrc"
class="w-full h-full border-0 rounded-md" class="w-full h-full border-0 rounded-md"
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin allow-top-navigation"
title="Unraid Changelog" title="Unraid Changelog"
></iframe> ></iframe>
</div> </div>

View File

@@ -1,25 +1,41 @@
<template> <template>
<client-only> <div class="text-black bg-white dark:text-white dark:bg-black">
<div class="flex flex-row items-center justify-center gap-6 p-6 text-gray-200 bg-zinc-800"> <client-only>
<template v-for="route in routes" :key="route.path"> <div class="flex flex-row items-center justify-center gap-6 p-6 bg-white dark:bg-zinc-800">
<NuxtLink <template v-for="route in routes" :key="route.path">
:to="route.path" <NuxtLink
class="underline hover:no-underline focus:no-underline" :to="route.path"
active-class="text-orange" class="underline hover:no-underline focus:no-underline"
> active-class="text-orange"
{{ formatRouteName(route.name) }} >
</NuxtLink> {{ formatRouteName(route.name) }}
</template> </NuxtLink>
<ModalsCe /> </template>
</div> <ModalsCe />
<slot /> </div>
</client-only> <slot />
</client-only>
</div>
</template> </template>
<script setup> <script setup>
import ModalsCe from '~/components/Modals.ce.vue'; import ModalsCe from '~/components/Modals.ce.vue';
import { useThemeStore } from '~/store/theme';
const router = useRouter(); const router = useRouter();
const themeStore = useThemeStore();
const { theme } = storeToRefs(themeStore);
// Watch for theme changes (satisfies linter by using theme)
watch(
theme,
() => {
// Theme is being watched for reactivity
console.debug('Theme changed:', theme.value);
},
{ immediate: true }
);
const routes = computed(() => { const routes = computed(() => {
return router return router
.getRoutes() .getRoutes()
@@ -37,3 +53,9 @@ function formatRouteName(name) {
.join(' '); .join(' ');
} }
</script> </script>
<style lang="postcss">
/* Import theme styles */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -7,6 +7,9 @@ const updateOsStore = useUpdateOsStore();
const { changelogModalVisible } = storeToRefs(updateOsStore); const { changelogModalVisible } = storeToRefs(updateOsStore);
const { t } = useI18n(); const { t } = useI18n();
onBeforeMount(() => {
// Register custom elements if needed for ColorSwitcherCe
});
async function showChangelogModalFromReleasesEndpoint() { async function showChangelogModalFromReleasesEndpoint() {
const response = await fetch('https://releases.unraid.net/os?branch=stable&current_version=6.12.3'); const response = await fetch('https://releases.unraid.net/os?branch=stable&current_version=6.12.3');
@@ -26,6 +29,7 @@ function showChangelogModalWithTestData() {
sha256: '1234567890' sha256: '1234567890'
}); });
} }
function showChangelogWithoutPretty() { function showChangelogWithoutPretty() {
updateOsStore.setReleaseForUpdate({ updateOsStore.setReleaseForUpdate({
version: '6.12.3', version: '6.12.3',
@@ -52,37 +56,59 @@ function showChangelogBrokenParse() {
}); });
} }
function showChangelogFromLocalhost() {
updateOsStore.setReleaseForUpdate({
version: '6.12.3',
date: '2023-07-15',
changelog: 'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
changelogPretty: 'http://localhost:3000/unraid-os/release-notes/6.12.3',
name: '6.12.3',
isEligible: true,
isNewer: true,
sha256: '1234567890'
});
}
</script> </script>
<template> <template>
<div class="container mx-auto p-6"> <div class="container mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Changelog</h1> <h1 class="text-2xl font-bold mb-6">Changelog</h1>
<UpdateOsChangelogModal :t="t" :open="changelogModalVisible" /> <UpdateOsChangelogModal :t="t" :open="changelogModalVisible" />
<div class="mb-6 flex flex-col gap-4 max-w-md"> <div class="mb-6 flex flex-col gap-4">
<button <ColorSwitcherCe />
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" <div class="max-w-md flex flex-col gap-4">
@click="showChangelogModalFromReleasesEndpoint" <button
> class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
Test Changelog Modal (from releases endpoint) @click="showChangelogModalFromReleasesEndpoint"
</button> >
<button Test Changelog Modal (from releases endpoint)
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" </button>
@click="showChangelogModalWithTestData" <button
> class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
Test Changelog Modal (with test data) @click="showChangelogFromLocalhost"
</button> >
<button Test Local Pretty Changelog (:3000)
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" </button>
@click="showChangelogWithoutPretty" <button
> class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
Test Without Pretty Changelog @click="showChangelogModalWithTestData"
</button> >
<button Test Changelog Modal (with test data)
class="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600" </button>
@click="showChangelogBrokenParse" <button
> class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
Test Broken Parse Changelog @click="showChangelogWithoutPretty"
</button> >
Test Without Pretty Changelog
</button>
<button
class="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
@click="showChangelogBrokenParse"
>
Test Broken Parse Changelog
</button>
</div>
</div> </div>
</div> </div>
</template> </template>