mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
feat: move to iframe for changelog (#1388)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Changelog modal now displays changelog documentation within an embedded iframe if a URL is available, allowing navigation within the iframe and providing a "Back to Changelog" button to return to the original view. - "View on Docs" button dynamically updates to reflect the current page within the iframe. - Added support for displaying a formatted changelog string and testing modal behavior with or without this content. - Introduced a new component to fetch, parse, and render changelogs from URLs with enhanced markdown handling. - Update OS store extended to manage changelog display and release stability, consolidating changelog state and actions. - **Bug Fixes** - Close button in modal dialogs is now visible on all screen sizes. - **Chores** - Updated the default server state, which may affect registration device counts and types. - Added new localization string for changelog titles with version placeholders. - Removed deprecated changelog store and related tests, simplifying state management. - Refined backup and restoration scripts for unraid-components directory to use move operations with improved logging. - Improved modal visibility state handling by consolidating changelog modal controls into the main update OS store. - Added URL origin and pattern checks for changelog iframe navigation security. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -134,8 +134,8 @@ done
|
||||
# Handle the unraid-components directory
|
||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||
if [ -d "$DIR" ] && [ ! -d "$DIR-" ]; then
|
||||
cp -rp "$DIR" "$DIR-"
|
||||
echo "Backed up directory: $DIR"
|
||||
mv "$DIR" "$DIR-"
|
||||
echo "Moved directory: $DIR to $DIR-"
|
||||
fi
|
||||
|
||||
echo "Backup complete."
|
||||
@@ -204,12 +204,13 @@ exit 0
|
||||
|
||||
# Handle the unraid-components directory
|
||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||
# certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
|
||||
# Remove the archive's contents before restoring
|
||||
if [ -d "$DIR" ]; then
|
||||
rm -rf "$DIR"
|
||||
fi
|
||||
if [ -d "$DIR-" ]; then
|
||||
mv -f "$DIR-" "$DIR"
|
||||
mv "$DIR-" "$DIR"
|
||||
echo "Restored directory: $DIR- to $DIR"
|
||||
fi
|
||||
]]>
|
||||
</INLINE>
|
||||
|
||||
@@ -402,6 +402,7 @@ class WebComponentTranslations
|
||||
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
|
||||
'Your Trial has expired' => _('Your Trial has expired'),
|
||||
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
||||
'Unraid OS {0} Changelog' => sprintf(_('Unraid OS %s Changelog'), '{0}'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('UpdateOs Store', () => {
|
||||
describe('State and Getters', () => {
|
||||
it('should initialize with correct default values', () => {
|
||||
expect(store.checkForUpdatesLoading).toBe(false);
|
||||
expect(store.modalOpen).toBe(false);
|
||||
expect(store.updateOsModalVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should have computed properties with the right types', () => {
|
||||
@@ -86,15 +86,15 @@ describe('UpdateOs Store', () => {
|
||||
await store.localCheckForUpdate();
|
||||
|
||||
expect(WebguiCheckForUpdate).toHaveBeenCalled();
|
||||
expect(store.modalOpen).toBe(true);
|
||||
expect(store.updateOsModalVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should set modal open state', () => {
|
||||
store.setModalOpen(true);
|
||||
expect(store.modalOpen).toBe(true);
|
||||
expect(store.updateOsModalVisible).toBe(true);
|
||||
|
||||
store.setModalOpen(false);
|
||||
expect(store.modalOpen).toBe(false);
|
||||
expect(store.updateOsModalVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors when checking for updates', async () => {
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* UpdateOsChangelog store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
vi.mock('~/helpers/markdown', () => ({
|
||||
Markdown: {
|
||||
create: () => ({
|
||||
setOptions: vi.fn(),
|
||||
parse: vi.fn().mockResolvedValue('<h1>Test Title</h1><p>Test content</p>'),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('~/helpers/urls', () => ({
|
||||
DOCS_RELEASE_NOTES: {
|
||||
toString: () => 'https://docs.unraid.net/unraid-os/release-notes/',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('marked-base-url', () => ({
|
||||
baseUrl: vi.fn().mockReturnValue(vi.fn()),
|
||||
}));
|
||||
|
||||
vi.mock('semver/functions/prerelease', () => ({
|
||||
default: vi.fn((version) => (version && version.includes('-') ? ['beta', '1'] : null)),
|
||||
}));
|
||||
|
||||
const mockRequestText = vi.fn().mockResolvedValue('# Test Changelog\n\nTest content');
|
||||
vi.mock('~/composables/services/request', () => ({
|
||||
request: {
|
||||
url: () => ({
|
||||
get: () => ({
|
||||
text: mockRequestText,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSend = vi.fn();
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
send: mockSend,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockStableRelease: Partial<ServerUpdateOsResponse> = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
date: '2023-10-15',
|
||||
isEligible: true,
|
||||
isNewer: true,
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
sha256: 'test-sha256',
|
||||
};
|
||||
|
||||
const mockBetaRelease: Partial<ServerUpdateOsResponse> = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.5-beta1',
|
||||
};
|
||||
|
||||
describe('UpdateOsChangelog Store', () => {
|
||||
let store: ReturnType<typeof useUpdateOsChangelogStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useUpdateOsChangelogStore();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Suppress console output
|
||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('Store API', () => {
|
||||
it('should initialize with default values', () => {
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
expect(store.parseChangelogFailed).toBe('');
|
||||
});
|
||||
|
||||
it('should set and get releaseForUpdate', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
expect(store.releaseForUpdate).toEqual(mockStableRelease);
|
||||
|
||||
store.setReleaseForUpdate(null);
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
});
|
||||
|
||||
it('should determine if release is stable', () => {
|
||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
||||
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(store.isReleaseForUpdateStable).toBe(true);
|
||||
|
||||
store.setReleaseForUpdate(mockBetaRelease as ServerUpdateOsResponse);
|
||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
||||
});
|
||||
|
||||
it('should have a method to fetch and confirm install', () => {
|
||||
store.fetchAndConfirmInstall('test-sha256');
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
{
|
||||
sha256: 'test-sha256',
|
||||
type: 'updateOs',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'forUpc'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have computed properties for changelog display', async () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(typeof store.mutatedParsedChangelog).toBe('string');
|
||||
expect(typeof store.parsedChangelogTitle).toBe('string');
|
||||
});
|
||||
|
||||
it('should clear changelog data when release is set to null', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
store.setReleaseForUpdate(null);
|
||||
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
expect(store.parseChangelogFailed).toBe('');
|
||||
});
|
||||
|
||||
it('should handle state transitions when changing releases', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
const differentRelease = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.6',
|
||||
};
|
||||
store.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(store.releaseForUpdate).toEqual(differentRelease);
|
||||
});
|
||||
|
||||
it('should have proper error handling for failed requests', async () => {
|
||||
mockRequestText.mockRejectedValueOnce(new Error('Network error'));
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.parseChangelogFailed).toBeTruthy();
|
||||
expect(store.parseChangelogFailed).toContain('error');
|
||||
});
|
||||
|
||||
it('should fetch and parse changelog when releaseForUpdate changes', async () => {
|
||||
const internalStore = useUpdateOsChangelogStore();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
internalStore.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).toHaveBeenCalled();
|
||||
|
||||
mockRequestText.mockClear();
|
||||
|
||||
const differentRelease = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.6',
|
||||
changelog: 'https://example.com/different-changelog.md',
|
||||
};
|
||||
|
||||
internalStore.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).toHaveBeenCalled();
|
||||
|
||||
mockRequestText.mockClear();
|
||||
|
||||
internalStore.setReleaseForUpdate(null);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,7 @@ import type {
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
|
||||
const state: ServerState = 'ENOKEYFILE' as ServerState;
|
||||
const state: ServerState = 'BASIC' as ServerState;
|
||||
const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
|
||||
const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
|
||||
const keyfileBase64 = '';
|
||||
|
||||
@@ -121,7 +121,7 @@ const computedVerticalCenter = computed<string>(() => {
|
||||
]"
|
||||
class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
|
||||
>
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px sm:block">
|
||||
<button
|
||||
class="rounded-md text-foreground bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
type="button"
|
||||
|
||||
@@ -5,14 +5,12 @@ import { storeToRefs } from 'pinia';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||
const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
const { modalOpen: updateOsModalVisible } = storeToRefs(useUpdateOsStore());
|
||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdateOsChangelogStore());
|
||||
const { updateOsModalVisible, changelogModalVisible } = storeToRefs(useUpdateOsStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -20,7 +18,7 @@ const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdat
|
||||
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
||||
<UpcTrial :t="t" :open="trialModalVisible" />
|
||||
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
|
||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
||||
<UpdateOsChangelogModal :t="t" :open="changelogModalVisible" />
|
||||
<ActivationModal :t="t" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import {
|
||||
ArrowSmallRightIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
EyeIcon,
|
||||
KeyIcon,
|
||||
ServerStackIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { BrandButton, BrandLoading } from '@unraid/ui';
|
||||
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import RawChangelogRenderer from '~/components/UpdateOs/RawChangelogRenderer.vue';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
import { allowedDocsOriginRegex, allowedDocsUrlRegex } from '~/helpers/urls';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
@@ -30,86 +29,143 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
// const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const { availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const { releaseForUpdate, mutatedParsedChangelog, parseChangelogFailed, parsedChangelogTitle } =
|
||||
storeToRefs(updateOsChangelogStore);
|
||||
const { availableWithRenewal, releaseForUpdate, changelogModalVisible } = storeToRefs(updateOsStore);
|
||||
const { setReleaseForUpdate, fetchAndConfirmInstall } = updateOsStore;
|
||||
|
||||
const showExtendKeyButton = computed(() => {
|
||||
return availableWithRenewal.value;
|
||||
});
|
||||
|
||||
// iframe navigation handling
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
||||
const hasNavigated = ref(false);
|
||||
const currentIframeUrl = ref<string | null>(null);
|
||||
|
||||
const docsChangelogUrl = computed(() => {
|
||||
return releaseForUpdate.value?.changelogPretty ?? null;
|
||||
});
|
||||
|
||||
const showRawChangelog = computed<boolean>(() => {
|
||||
return !docsChangelogUrl.value && !!releaseForUpdate.value?.changelog;
|
||||
});
|
||||
|
||||
const handleIframeNavigationMessage = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data &&
|
||||
event.data.type === 'unraid-docs-navigation' &&
|
||||
iframeRef.value &&
|
||||
event.source === iframeRef.value.contentWindow &&
|
||||
allowedDocsOriginRegex.test(event.origin)
|
||||
) {
|
||||
if (
|
||||
typeof event.data.url === 'string' &&
|
||||
allowedDocsUrlRegex.test(event.data.url)
|
||||
) {
|
||||
if (event.data.url !== docsChangelogUrl.value) {
|
||||
hasNavigated.value = true;
|
||||
} else {
|
||||
hasNavigated.value = false;
|
||||
}
|
||||
currentIframeUrl.value = event.data.url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleIframeNavigationMessage);
|
||||
// Set initial value
|
||||
currentIframeUrl.value = docsChangelogUrl.value;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', handleIframeNavigationMessage);
|
||||
});
|
||||
|
||||
const revertToInitialChangelog = () => {
|
||||
if (iframeRef.value && docsChangelogUrl.value) {
|
||||
iframeRef.value.src = docsChangelogUrl.value;
|
||||
hasNavigated.value = false;
|
||||
currentIframeUrl.value = docsChangelogUrl.value;
|
||||
}
|
||||
};
|
||||
|
||||
watch(docsChangelogUrl, (newUrl) => {
|
||||
currentIframeUrl.value = newUrl;
|
||||
hasNavigated.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-if="releaseForUpdate?.version"
|
||||
:center-content="false"
|
||||
:error="!!parseChangelogFailed"
|
||||
max-width="max-w-800px"
|
||||
:open="!!releaseForUpdate"
|
||||
:open="changelogModalVisible"
|
||||
:show-close-x="true"
|
||||
:t="t"
|
||||
:tall-content="true"
|
||||
:title="parsedChangelogTitle ?? undefined"
|
||||
@close="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
:title="t('Unraid OS {0} Changelog', [releaseForUpdate.version])"
|
||||
:disable-overlay-close="false"
|
||||
@close="setReleaseForUpdate(null)"
|
||||
>
|
||||
<template #main>
|
||||
<div
|
||||
v-if="mutatedParsedChangelog"
|
||||
class="text-16px sm:text-18px prose prose-a:text-unraid-red hover:prose-a:no-underline hover:prose-a:text-unraid-red/60 dark:prose-a:text-orange hover:dark:prose-a:text-orange/60"
|
||||
v-html="mutatedParsedChangelog"
|
||||
/>
|
||||
|
||||
<div v-else-if="parseChangelogFailed" class="text-center flex flex-col gap-4 prose">
|
||||
<h2 class="text-lg text-unraid-red italic font-semibold">
|
||||
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
|
||||
</p>
|
||||
<div v-if="releaseForUpdate?.changelogPretty" class="flex self-center">
|
||||
<BrandButton
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
variant="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t('View Changelog on Docs') }}
|
||||
</BrandButton>
|
||||
<div class="flex flex-col gap-4 min-w-[280px] sm:min-w-[400px]">
|
||||
<!-- iframe for changelog if available -->
|
||||
<div v-if="docsChangelogUrl" class="w-[calc(100%+3rem)] h-[475px] -mx-6 -my-6">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="docsChangelogUrl"
|
||||
class="w-full h-full border-0 rounded-md"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
title="Unraid Changelog"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
||||
>
|
||||
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
||||
<p>{{ props.t('Fetching & parsing changelog…') }}</p>
|
||||
<!-- Fallback to raw changelog -->
|
||||
<RawChangelogRenderer
|
||||
v-else-if="showRawChangelog && releaseForUpdate?.changelog"
|
||||
:changelog="releaseForUpdate?.changelog"
|
||||
:version="releaseForUpdate?.version"
|
||||
:date="releaseForUpdate?.date"
|
||||
:t="t"
|
||||
:changelog-pretty="releaseForUpdate?.changelogPretty"
|
||||
/>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div
|
||||
v-else
|
||||
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
||||
>
|
||||
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
||||
<p>{{ props.t('Loading changelog…') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
|
||||
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
|
||||
<!-- Back to changelog button (when navigated away) -->
|
||||
<BrandButton
|
||||
v-if="hasNavigated && docsChangelogUrl"
|
||||
variant="underline"
|
||||
:icon="XMarkIcon"
|
||||
@click="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
>
|
||||
{{ props.t('Close') }}
|
||||
</BrandButton>
|
||||
:icon="ArrowLeftIcon"
|
||||
aria-label="Back to Changelog"
|
||||
@click="revertToInitialChangelog"
|
||||
/>
|
||||
|
||||
<!-- View on docs button -->
|
||||
<BrandButton
|
||||
v-if="releaseForUpdate?.changelogPretty"
|
||||
v-if="currentIframeUrl || releaseForUpdate?.changelogPretty"
|
||||
variant="underline"
|
||||
:external="true"
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
:icon="EyeIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t('View on Docs') }}
|
||||
</BrandButton>
|
||||
:href="currentIframeUrl || releaseForUpdate?.changelogPretty"
|
||||
:icon="ArrowTopRightOnSquareIcon"
|
||||
aria-label="View on Docs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<BrandButton
|
||||
v-if="showExtendKeyButton"
|
||||
variant="fill"
|
||||
@@ -122,8 +178,8 @@ const showExtendKeyButton = computed(() => {
|
||||
<BrandButton
|
||||
v-else-if="releaseForUpdate?.sha256"
|
||||
:icon="ServerStackIcon"
|
||||
:icon-right="ArrowSmallRightIcon"
|
||||
@click="updateOsChangelogStore.fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
||||
:icon-right="ArrowRightIcon"
|
||||
@click="fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
||||
>
|
||||
{{ props.t('Continue') }}
|
||||
</BrandButton>
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useAccountStore } from '~/store/account';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
@@ -35,7 +34,6 @@ const accountStore = useAccountStore();
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const {
|
||||
regExp,
|
||||
@@ -181,7 +179,7 @@ const actionButtons = computed((): BrandButtonProps[] | null => {
|
||||
buttons.push({
|
||||
variant: availableWithRenewal.value ? 'outline' : undefined,
|
||||
click: async () =>
|
||||
await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
||||
await updateOsStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
||||
icon: EyeIcon,
|
||||
text: availableWithRenewal.value
|
||||
? props.t('View Changelog')
|
||||
|
||||
141
web/components/UpdateOs/RawChangelogRenderer.vue
Normal file
141
web/components/UpdateOs/RawChangelogRenderer.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { Markdown } from '@/helpers/markdown';
|
||||
import { BrandButton } from '@unraid/ui';
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
changelog: string; // This is the URL to the changelog, not the content
|
||||
version?: string;
|
||||
date?: string;
|
||||
t: ComposerTranslation;
|
||||
changelogPretty?: string;
|
||||
}>();
|
||||
|
||||
const parsedChangelog = ref<string>("");
|
||||
const parsedChangelogTitle = ref<string>("");
|
||||
const parseChangelogFailed = ref<string>("");
|
||||
const isLoading = ref<boolean>(false);
|
||||
|
||||
// Fetch and parse the changelog using the same logic as in updateOsChangelog.ts
|
||||
const fetchAndParseChangelog = async () => {
|
||||
if (!props.changelog) {
|
||||
isLoading.value = false;
|
||||
parseChangelogFailed.value = 'No changelog URL provided';
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
parseChangelogFailed.value = "";
|
||||
try {
|
||||
// Fetch the changelog content from the URL
|
||||
const res = await fetch(props.changelog);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status} while fetching changelog`);
|
||||
}
|
||||
const data = await res.text();
|
||||
if (!data || data.trim() === "") {
|
||||
parseChangelogFailed.value = 'Changelog is empty';
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
let firstHeading = true;
|
||||
const marked = Markdown.create();
|
||||
// open links in new tab & replace .md from links
|
||||
const renderer = new marked.Renderer();
|
||||
// Set base URL for relative links
|
||||
const baseUrl = "https://docs.unraid.net/go/release-notes/";
|
||||
renderer.link = ({ href, title, tokens }) => {
|
||||
const linkText = renderer.parser.parseInline(tokens);
|
||||
let cleanHref = href.replace(".md", ""); // remove .md from href
|
||||
if (cleanHref.startsWith("#")) {
|
||||
cleanHref = `${baseUrl}${props.version}${cleanHref}`;
|
||||
}
|
||||
if (!cleanHref.startsWith("http")) {
|
||||
cleanHref = `${baseUrl}${cleanHref}`;
|
||||
}
|
||||
return `<a href="${cleanHref}" ${title ? `title="${title}"` : ""} target="_blank" rel="noopener noreferrer">${linkText}</a>`;
|
||||
};
|
||||
// Add heading renderer
|
||||
renderer.heading = ({ text, depth }) => {
|
||||
// Capture the first h1 title
|
||||
if (depth === 1 && firstHeading) {
|
||||
firstHeading = false;
|
||||
parsedChangelogTitle.value = `Version ${props.version} ${props.date ? `(${props.date})` : ''}`;
|
||||
return `<br />`;
|
||||
}
|
||||
// Only add IDs for h2 and above (depth > 1)
|
||||
if (depth > 1) {
|
||||
const id = text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-");
|
||||
return `<h${depth} id="${id}">${text}</h${depth}>`;
|
||||
}
|
||||
return `<h${depth}>${text}</h${depth}>`;
|
||||
};
|
||||
marked.setOptions({
|
||||
renderer,
|
||||
});
|
||||
parsedChangelog.value = await marked.parse(
|
||||
(data as string) ?? "Changelog fetch data empty"
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const caughtError = error as Error;
|
||||
parseChangelogFailed.value =
|
||||
caughtError && caughtError?.message
|
||||
? caughtError.message
|
||||
: `Failed to parse ${props.version} changelog`;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the changelog when the component is mounted or props change
|
||||
onMounted(fetchAndParseChangelog);
|
||||
watch(() => props.changelog, fetchAndParseChangelog, { immediate: true });
|
||||
|
||||
// Used to remove the first <h1></h1> and its contents from the parsedChangelog
|
||||
const mutatedParsedChangelog = computed(() => {
|
||||
if (parsedChangelog.value) {
|
||||
return parsedChangelog.value.replace(/<h1>(.*?)<\/h1>/, "");
|
||||
}
|
||||
return parsedChangelog.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none markdown-body p-4 overflow-auto">
|
||||
<div v-if="parseChangelogFailed" class="text-center flex flex-col gap-4 prose">
|
||||
<h2 class="text-lg text-unraid-red italic font-semibold">
|
||||
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
||||
</h2>
|
||||
<p>
|
||||
{{
|
||||
props.t(`It's highly recommended to review the changelog before continuing your update`)
|
||||
}}
|
||||
</p>
|
||||
<div v-if="props.changelogPretty" class="flex self-center">
|
||||
<BrandButton
|
||||
:href="props.changelogPretty"
|
||||
variant="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t('View Changelog on Docs') }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="parsedChangelogTitle" class="mb-4">
|
||||
<h1>{{ parsedChangelogTitle }}</h1>
|
||||
</div>
|
||||
<div v-if="mutatedParsedChangelog" v-html="mutatedParsedChangelog"></div>
|
||||
<div v-else-if="isLoading" class="flex flex-col items-center justify-center py-8">
|
||||
<span class="mx-auto animate-spin border-2 border-gray-300 rounded-full w-8 h-8 border-t-unraid-red"></span>
|
||||
<p class="ml-2">{{ props.t('Loading changelog...') }}</p>
|
||||
</div>
|
||||
<div v-else class="text-center py-8">
|
||||
<p>{{ props.t('No changelog content available') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -40,6 +40,9 @@ const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/',
|
||||
|
||||
const SUPPORT = new URL('https://unraid.net');
|
||||
|
||||
export const allowedDocsOriginRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?$/;
|
||||
export const allowedDocsUrlRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?\//;
|
||||
|
||||
export {
|
||||
ACCOUNT,
|
||||
ACCOUNT_CALLBACK,
|
||||
|
||||
@@ -384,5 +384,6 @@
|
||||
"Your Trial key has been extended!": "Your Trial key has been extended!",
|
||||
"Create an Unraid.net account and activate your key": "Create an Unraid.net account and activate your key",
|
||||
"Device is ready to configure": "Device is ready to configure",
|
||||
"Secure your device": "Secure your device"
|
||||
"Secure your device": "Secure your device",
|
||||
"Unraid OS {0} Changelog": "Unraid OS {0} Changelog"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const changelog = ref('');
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(updateOsChangelogStore);
|
||||
onMounted(async () => {
|
||||
const response = await fetch('https://releases.unraid.net/json');
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const { changelogModalVisible } = storeToRefs(updateOsStore);
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
async function showChangelogModalFromReleasesEndpoint() {
|
||||
const response = await fetch('https://releases.unraid.net/os?branch=stable¤t_version=6.12.3');
|
||||
const data = await response.json();
|
||||
if (data.length > 0) {
|
||||
changelog.value = data[0].changelog;
|
||||
}
|
||||
});
|
||||
updateOsStore.setReleaseForUpdate(data);
|
||||
}
|
||||
|
||||
function showChangelogModal() {
|
||||
// Simulate receiving a release for update
|
||||
updateOsChangelogStore.setReleaseForUpdate({
|
||||
function showChangelogModalWithTestData() {
|
||||
updateOsStore.setReleaseForUpdate({
|
||||
version: '6.12.3',
|
||||
date: '2023-07-15',
|
||||
changelog: changelog.value,
|
||||
changelog: 'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
||||
changelogPretty: 'https://docs.unraid.net/go/release-notes/6.12.3',
|
||||
name: '6.12.3',
|
||||
isEligible: true,
|
||||
isNewer: true,
|
||||
isNewer: true,
|
||||
sha256: '1234567890'
|
||||
});
|
||||
}
|
||||
function showChangelogWithoutPretty() {
|
||||
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: '',
|
||||
name: '6.12.3',
|
||||
isEligible: true,
|
||||
isNewer: true,
|
||||
sha256: '1234567890'
|
||||
});
|
||||
}
|
||||
|
||||
function showChangelogBrokenParse() {
|
||||
updateOsStore.setReleaseForUpdate({
|
||||
version: '6.12.3',
|
||||
date: '2023-07-15',
|
||||
changelog: null,
|
||||
changelogPretty: undefined, // intentionally broken
|
||||
name: '6.12.3',
|
||||
isEligible: true,
|
||||
isNewer: true,
|
||||
sha256: '1234567890'
|
||||
});
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Changelog</h1>
|
||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
||||
<div class="mb-6">
|
||||
<UpdateOsChangelogModal :t="t" :open="changelogModalVisible" />
|
||||
<div class="mb-6 flex flex-col gap-4 max-w-md">
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
@click="showChangelogModal"
|
||||
@click="showChangelogModalFromReleasesEndpoint"
|
||||
>
|
||||
Test Changelog Modal
|
||||
Test Changelog Modal (from releases endpoint)
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
@click="showChangelogModalWithTestData"
|
||||
>
|
||||
Test Changelog Modal (with test data)
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
@click="showChangelogWithoutPretty"
|
||||
>
|
||||
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 class="prose max-w-none">
|
||||
<div v-html="changelog"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import { WebguiCheckForUpdate, WebguiUpdateCancel } from '~/composables/services/webgui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import prerelease from 'semver/functions/prerelease';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
@@ -23,7 +25,9 @@ extend(relativeTime);
|
||||
export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
// state
|
||||
const checkForUpdatesLoading = ref<boolean>(false);
|
||||
const modalOpen = ref<boolean>(false);
|
||||
const updateOsModalVisible = ref<boolean>(false);
|
||||
const releaseForUpdate = ref<ServerUpdateOsResponse | null>(null);
|
||||
const changelogModalVisible = computed(() => !!releaseForUpdate.value);
|
||||
// getters from other stores
|
||||
const serverStore = useServerStore();
|
||||
|
||||
@@ -59,6 +63,33 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
*/
|
||||
const availableRequiresAuth = computed((): boolean => !updateOsResponse.value?.sha256);
|
||||
|
||||
// Changelog logic
|
||||
const changelogUrl = computed((): string => releaseForUpdate.value?.changelog || '');
|
||||
const changelogPretty = computed(() => releaseForUpdate.value?.changelogPretty ?? null);
|
||||
const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
|
||||
releaseForUpdate.value = release;
|
||||
};
|
||||
// isReleaseForUpdateStable logic (true if no prerelease in version)
|
||||
const isReleaseForUpdateStable = computed(() => {
|
||||
if (!releaseForUpdate.value?.version) return false;
|
||||
return !prerelease(releaseForUpdate.value.version);
|
||||
});
|
||||
// fetchAndConfirmInstall logic
|
||||
const callbackStore = useCallbackActionsStore();
|
||||
const fetchAndConfirmInstall = (sha256: string) => {
|
||||
callbackStore.send(
|
||||
window.location.href,
|
||||
[
|
||||
{
|
||||
sha256,
|
||||
type: 'updateOs',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'forUpc'
|
||||
);
|
||||
};
|
||||
|
||||
// actions
|
||||
const localCheckForUpdate = async (): Promise<void> => {
|
||||
checkForUpdatesLoading.value = true;
|
||||
@@ -95,7 +126,7 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
};
|
||||
|
||||
const setModalOpen = (val: boolean) => {
|
||||
modalOpen.value = val;
|
||||
updateOsModalVisible.value = val;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -103,14 +134,21 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
available,
|
||||
availableWithRenewal,
|
||||
checkForUpdatesLoading,
|
||||
modalOpen,
|
||||
updateOsModalVisible,
|
||||
changelogModalVisible,
|
||||
releaseForUpdate,
|
||||
updateOsIgnoredReleases,
|
||||
// getters
|
||||
availableReleaseDate,
|
||||
availableRequiresAuth,
|
||||
changelogUrl,
|
||||
changelogPretty,
|
||||
isReleaseForUpdateStable,
|
||||
// actions
|
||||
localCheckForUpdate,
|
||||
cancelUpdate,
|
||||
setModalOpen,
|
||||
setReleaseForUpdate,
|
||||
fetchAndConfirmInstall,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { Markdown } from '@/helpers/markdown';
|
||||
import { DOCS_RELEASE_NOTES } from '~/helpers/urls';
|
||||
import { baseUrl } from 'marked-base-url';
|
||||
import prerelease from 'semver/functions/prerelease';
|
||||
|
||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import { request } from '~/composables/services/request';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
|
||||
export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => {
|
||||
const callbackStore = useCallbackActionsStore();
|
||||
|
||||
const releaseForUpdate = ref<ServerUpdateOsResponse | null>(null);
|
||||
watch(releaseForUpdate, async (newVal, oldVal) => {
|
||||
console.debug('[releaseForUpdate] watch', newVal, oldVal);
|
||||
resetChangelogDetails(); // reset values when setting and unsetting a selected release
|
||||
// Fetch and parse the changelog when the user selects a release
|
||||
if (newVal) {
|
||||
await fetchAndParseChangelog();
|
||||
}
|
||||
});
|
||||
|
||||
const changelogUrl = computed((): string => {
|
||||
if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
releaseForUpdate.value?.changelog ??
|
||||
`https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`
|
||||
);
|
||||
});
|
||||
|
||||
const isReleaseForUpdateStable = computed(() =>
|
||||
releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false
|
||||
);
|
||||
const parsedChangelog = ref<string>('');
|
||||
const parseChangelogFailed = ref<string>('');
|
||||
// used to remove the first <h1></h1> and it's contents from the parsedChangelog
|
||||
const mutatedParsedChangelog = computed(() => {
|
||||
if (parsedChangelog.value) {
|
||||
return parsedChangelog.value.replace(/<h1>(.*?)<\/h1>/, '');
|
||||
}
|
||||
return parsedChangelog.value;
|
||||
});
|
||||
// used to extract the first <h1></h1> and it's contents from the parsedChangelog for the modal header title
|
||||
const parsedChangelogTitle = computed(() => {
|
||||
if (parseChangelogFailed.value) {
|
||||
return parseChangelogFailed.value;
|
||||
}
|
||||
if (parsedChangelog.value) {
|
||||
return (
|
||||
parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ??
|
||||
`Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`
|
||||
);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
|
||||
console.debug('[setReleaseForUpdate]', release);
|
||||
releaseForUpdate.value = release;
|
||||
};
|
||||
const resetChangelogDetails = () => {
|
||||
console.debug('[resetChangelogDetails]');
|
||||
parsedChangelog.value = '';
|
||||
parseChangelogFailed.value = '';
|
||||
};
|
||||
const fetchAndParseChangelog = async () => {
|
||||
console.debug('[fetchAndParseChangelog]');
|
||||
try {
|
||||
const changelogMarkdownRaw = await request
|
||||
.url(changelogUrl.value ?? '')
|
||||
.get()
|
||||
.text();
|
||||
|
||||
// set base url for relative links
|
||||
const marked = Markdown.create(baseUrl(DOCS_RELEASE_NOTES.toString()));
|
||||
|
||||
// open links in new tab & replace .md from links
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.link = ({ href, title, tokens }) => {
|
||||
const linkText = renderer.parser.parseInline(tokens);
|
||||
const cleanHref = href.replace('.md', ''); // remove .md from href
|
||||
return `<a href="${cleanHref}" ${title ? `title="${title}"` : ''} target="_blank">${linkText}</a>`;
|
||||
};
|
||||
|
||||
marked.setOptions({
|
||||
renderer,
|
||||
});
|
||||
|
||||
parsedChangelog.value = await marked.parse(changelogMarkdownRaw);
|
||||
} catch (error: unknown) {
|
||||
const caughtError = error as Error;
|
||||
parseChangelogFailed.value =
|
||||
caughtError && caughtError?.message
|
||||
? caughtError.message
|
||||
: `Failed to parse ${releaseForUpdate.value?.version} changelog`;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAndConfirmInstall = (sha256: string) => {
|
||||
callbackStore.send(
|
||||
window.location.href,
|
||||
[
|
||||
{
|
||||
sha256,
|
||||
type: 'updateOs',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'forUpc'
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
// state
|
||||
parseChangelogFailed,
|
||||
releaseForUpdate,
|
||||
// getters
|
||||
isReleaseForUpdateStable,
|
||||
mutatedParsedChangelog,
|
||||
parsedChangelogTitle,
|
||||
// actions
|
||||
setReleaseForUpdate,
|
||||
fetchAndConfirmInstall,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user