mirror of
https://github.com/unraid/api.git
synced 2026-05-14 20:10:10 -05:00
feat: improved update ui (#1691)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Global awaitable confirmation modal for notification actions. * “Ignore this release” toggle that persists to the server when used. * New test pages and standalone test controls for the update modal and theme switching. * **Refactor** * Update modal rebuilt with a responsive layout, unified “Update Available” title, revised action logic, and centralized modal plumbing. * **Style** * OS Update highlight block, improved spacing, refreshed iconography, and tooltips clarifying actions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
This is the Unraid API monorepo containing multiple packages that provide API functionality for Unraid servers. It uses pnpm workspaces with the following structure:
|
||||
|
||||
- `/api` - Core NestJS API server with GraphQL
|
||||
- `/web` - Nuxt.js frontend application
|
||||
- `/web` - Vue 3 frontend application
|
||||
- `/unraid-ui` - Vue 3 component library
|
||||
- `/plugin` - Unraid plugin package (.plg)
|
||||
- `/packages` - Shared packages and API plugins
|
||||
@@ -128,9 +128,6 @@ Enables GraphQL playground at `http://tower.local/graphql`
|
||||
- **Use Mocks Correctly**: Mocks should be used as nouns, not verbs.
|
||||
|
||||
#### Vue Component Testing
|
||||
|
||||
- This is a Nuxt.js app but we are testing with vitest outside of the Nuxt environment
|
||||
- Nuxt is currently set to auto import so some vue files may need compute or ref imported
|
||||
- Use pnpm when running terminal commands and stay within the web directory
|
||||
- Tests are located under `web/__test__`, run with `pnpm test`
|
||||
- Use `mount` from Vue Test Utils for component testing
|
||||
|
||||
@@ -51,7 +51,7 @@ const classes = computed(() => {
|
||||
});
|
||||
|
||||
const needsBrandGradientBackground = computed(() => {
|
||||
return ['outline-solid', 'outline-primary'].includes(props.variant ?? '');
|
||||
return ['outline', 'outline-solid', 'outline-primary'].includes(props.variant ?? '');
|
||||
});
|
||||
|
||||
const isLink = computed(() => Boolean(props.href));
|
||||
|
||||
Vendored
+3
@@ -31,6 +31,7 @@ declare module 'vue' {
|
||||
ChangelogModal: typeof import('./src/components/UpdateOs/ChangelogModal.vue')['default']
|
||||
CheckUpdateResponseModal: typeof import('./src/components/UpdateOs/CheckUpdateResponseModal.vue')['default']
|
||||
'ColorSwitcher.standalone': typeof import('./src/components/ColorSwitcher.standalone.vue')['default']
|
||||
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
|
||||
'ConnectSettings.standalone': typeof import('./src/components/ConnectSettings/ConnectSettings.standalone.vue')['default']
|
||||
Console: typeof import('./src/components/Docker/Console.vue')['default']
|
||||
Detail: typeof import('./src/components/LayoutViews/Detail/Detail.vue')['default']
|
||||
@@ -97,6 +98,8 @@ declare module 'vue' {
|
||||
SsoButtons: typeof import('./src/components/sso/SsoButtons.vue')['default']
|
||||
SsoProviderButton: typeof import('./src/components/sso/SsoProviderButton.vue')['default']
|
||||
Status: typeof import('./src/components/UpdateOs/Status.vue')['default']
|
||||
'TestThemeSwitcher.standalone': typeof import('./src/components/TestThemeSwitcher.standalone.vue')['default']
|
||||
'TestUpdateModal.standalone': typeof import('./src/components/UpdateOs/TestUpdateModal.standalone.vue')['default']
|
||||
'ThemeSwitcher.standalone': typeof import('./src/components/ThemeSwitcher.standalone.vue')['default']
|
||||
ThirdPartyDrivers: typeof import('./src/components/UpdateOs/ThirdPartyDrivers.vue')['default']
|
||||
Trial: typeof import('./src/components/UserProfile/Trial.vue')['default']
|
||||
|
||||
@@ -179,6 +179,14 @@
|
||||
</div>
|
||||
<a href="/test-pages/os-management.html">Open →</a>
|
||||
</div>
|
||||
|
||||
<div class="page-item">
|
||||
<div>
|
||||
<h3>Update Modal Testing <span class="badge new">NEW</span></h3>
|
||||
<p>Test various update scenarios including expired licenses, renewals, and auth requirements</p>
|
||||
</div>
|
||||
<a href="/test-update-modal.html">Open →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from '@unraid/ui';
|
||||
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
|
||||
const { isOpen, state, handleConfirm, handleCancel } = useConfirm();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="isOpen" @update:open="!$event && handleCancel()">
|
||||
<DialogContent>
|
||||
<DialogHeader v-if="state">
|
||||
<DialogTitle>{{ state.title }}</DialogTitle>
|
||||
<DialogDescription v-if="state.description">
|
||||
{{ state.description }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter v-if="state">
|
||||
<div class="flex w-full justify-between gap-3">
|
||||
<Button variant="outline" @click="handleCancel">
|
||||
{{ state.cancelText }}
|
||||
</Button>
|
||||
<Button :variant="state.confirmVariant" @click="handleConfirm">
|
||||
{{ state.confirmText }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@unraid/ui';
|
||||
import { Settings } from 'lucide-vue-next';
|
||||
|
||||
import ConfirmDialog from '~/components/ConfirmDialog.vue';
|
||||
import {
|
||||
archiveAllNotifications,
|
||||
deleteArchivedNotifications,
|
||||
@@ -37,10 +38,12 @@ import NotificationsList from '~/components/Notifications/List.vue';
|
||||
import { useTrackLatestSeenNotification } from '~/composables/api/use-notifications';
|
||||
import { useFragment } from '~/composables/gql';
|
||||
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
|
||||
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
|
||||
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
|
||||
const { mutate: recalculateOverview } = useMutation(resetOverview);
|
||||
const { confirm } = useConfirm();
|
||||
const importance = ref<Importance | undefined>(undefined);
|
||||
|
||||
const filterItems = [
|
||||
@@ -52,17 +55,26 @@ const filterItems = [
|
||||
];
|
||||
|
||||
const confirmAndArchiveAll = async () => {
|
||||
if (confirm('This will archive all notifications on your Unraid server. Continue?')) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Archive All Notifications',
|
||||
description: 'This will archive all notifications on your Unraid server. Continue?',
|
||||
confirmText: 'Archive All',
|
||||
confirmVariant: 'primary',
|
||||
});
|
||||
if (confirmed) {
|
||||
await archiveAll();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAndDeleteArchives = async () => {
|
||||
if (
|
||||
confirm(
|
||||
'This will permanently delete all archived notifications currently on your Unraid server. Continue?'
|
||||
)
|
||||
) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete All Archived Notifications',
|
||||
description:
|
||||
'This will permanently delete all archived notifications currently on your Unraid server. This action cannot be undone.',
|
||||
confirmText: 'Delete All',
|
||||
confirmVariant: 'destructive',
|
||||
});
|
||||
if (confirmed) {
|
||||
await deleteArchives();
|
||||
}
|
||||
};
|
||||
@@ -230,4 +242,7 @@ const prepareToViewNotifications = () => {
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<!-- Global Confirm Dialog -->
|
||||
<ConfirmDialog />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { Select } from '@unraid/ui';
|
||||
|
||||
import type { Theme } from '~/themes/types';
|
||||
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { theme } = storeToRefs(themeStore);
|
||||
|
||||
// Available theme options
|
||||
const items = [
|
||||
{ value: 'white', label: 'Light' },
|
||||
{ value: 'black', label: 'Dark' },
|
||||
{ value: 'azure', label: 'Azure' },
|
||||
{ value: 'gray', label: 'Gray' },
|
||||
];
|
||||
|
||||
// Current theme value
|
||||
const currentTheme = computed({
|
||||
get: () => theme.value.name,
|
||||
set: (value: string) => {
|
||||
const newTheme: Theme = {
|
||||
...theme.value,
|
||||
name: value,
|
||||
};
|
||||
themeStore.setTheme(newTheme);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-white">Theme:</span>
|
||||
<Select v-model="currentTheme" :items="items" placeholder="Select theme" class="w-32" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,27 +2,30 @@
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { ArrowDownTrayIcon } from '@heroicons/vue/24/outline';
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CogIcon,
|
||||
EyeIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import {
|
||||
BrandButton,
|
||||
BrandLoading,
|
||||
Button,
|
||||
cn,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
Label,
|
||||
ResponsiveModal,
|
||||
ResponsiveModalFooter,
|
||||
ResponsiveModalHeader,
|
||||
ResponsiveModalTitle,
|
||||
Switch,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@unraid/ui';
|
||||
|
||||
import type { BrandButtonProps } from '@unraid/ui';
|
||||
@@ -96,6 +99,9 @@ watch(updateOsIgnoredReleases, (newVal, oldVal) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get the localized 'Close' text for comparison
|
||||
const localizedCloseText = computed(() => props.t('Close'));
|
||||
|
||||
const notificationsSettings = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value
|
||||
? props.t(
|
||||
@@ -115,30 +121,18 @@ const modalCopy = computed((): ModalCopy | null => {
|
||||
};
|
||||
}
|
||||
|
||||
// Use the release date
|
||||
let formattedReleaseDate = '';
|
||||
if (availableReleaseDate.value) {
|
||||
// build string with prefix
|
||||
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
|
||||
}
|
||||
|
||||
if (availableWithRenewal.value) {
|
||||
const description = regUpdatesExpired.value
|
||||
? `${props.t('Eligible for updates released on or before {0}.', [formattedRegExp.value])} ${props.t('Extend your license to access the latest updates.')}`
|
||||
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
|
||||
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
|
||||
title: props.t('Update Available'),
|
||||
description: description,
|
||||
};
|
||||
} else if (available.value) {
|
||||
const description = availableRequiresAuth.value
|
||||
? props.t('Release requires verification to update')
|
||||
: undefined;
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Update Available', [available.value]),
|
||||
description: description
|
||||
? `<p>${formattedReleaseDate}</p><p>${description}</p>`
|
||||
: formattedReleaseDate,
|
||||
title: props.t('Update Available'),
|
||||
description: undefined,
|
||||
};
|
||||
} else if (!available.value && !availableWithRenewal.value) {
|
||||
return {
|
||||
@@ -169,8 +163,18 @@ const extraLinks = computed((): BrandButtonProps[] => {
|
||||
});
|
||||
|
||||
const actionButtons = computed((): BrandButtonProps[] | null => {
|
||||
// If ignoring release, show close button as primary action
|
||||
if (ignoreThisRelease.value && (available.value || availableWithRenewal.value)) {
|
||||
return [
|
||||
{
|
||||
click: () => close(),
|
||||
text: props.t('Close'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// update not available or no action buttons default closing
|
||||
if (!available.value || ignoreThisRelease.value) {
|
||||
if (!available.value && !availableWithRenewal.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -274,20 +278,58 @@ const modalWidth = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="open" @update:open="(value) => !value && close()">
|
||||
<DialogContent :class="modalWidth" :show-close-button="!checkForUpdatesLoading">
|
||||
<DialogHeader v-if="modalCopy?.title">
|
||||
<DialogTitle>
|
||||
<ResponsiveModal
|
||||
:open="open"
|
||||
:dialog-class="modalWidth"
|
||||
:sheet-class="'h-full'"
|
||||
:show-close-button="!checkForUpdatesLoading"
|
||||
@update:open="(value) => !value && close()"
|
||||
>
|
||||
<div class="flex h-full flex-col">
|
||||
<ResponsiveModalHeader v-if="modalCopy?.title">
|
||||
<ResponsiveModalTitle>
|
||||
{{ modalCopy.title }}
|
||||
</DialogTitle>
|
||||
</ResponsiveModalTitle>
|
||||
<DialogDescription v-if="modalCopy?.description">
|
||||
<span v-html="modalCopy.description" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</ResponsiveModalHeader>
|
||||
|
||||
<div v-if="renderMainSlot" class="flex flex-col gap-4">
|
||||
<div v-if="renderMainSlot" class="flex flex-1 flex-col gap-6 overflow-y-auto px-6">
|
||||
<BrandLoading v-if="checkForUpdatesLoading" class="mx-auto w-[150px]" />
|
||||
<div v-else class="flex flex-col gap-y-4">
|
||||
<div v-else class="flex flex-col gap-y-6">
|
||||
<!-- OS Update highlight section -->
|
||||
<div v-if="available || availableWithRenewal" class="flex flex-col items-center gap-4 py-4">
|
||||
<div class="bg-primary/10 flex items-center justify-center rounded-full p-4">
|
||||
<ArrowDownTrayIcon class="text-primary h-8 w-8" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h2 class="text-foreground text-3xl font-bold">
|
||||
{{ availableWithRenewal || available }}
|
||||
</h2>
|
||||
<p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm">
|
||||
Released on {{ userFormattedReleaseDate }}
|
||||
</p>
|
||||
<p
|
||||
v-if="availableRequiresAuth && !availableWithRenewal"
|
||||
class="mt-2 text-center text-sm text-amber-500"
|
||||
>
|
||||
{{ t('Requires verification to update') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div
|
||||
class="hover:bg-muted/50 flex cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors"
|
||||
@click="ignoreThisRelease = !ignoreThisRelease"
|
||||
>
|
||||
<Switch v-model="ignoreThisRelease" @click.stop />
|
||||
<Label class="text-muted-foreground cursor-pointer text-sm">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="extraLinks.length > 0"
|
||||
:class="cn('xs:!flex-row flex flex-col justify-center gap-2')"
|
||||
@@ -295,7 +337,7 @@ const modalWidth = computed(() => {
|
||||
<BrandButton
|
||||
v-for="item in extraLinks"
|
||||
:key="item.text"
|
||||
:btn-style="item.variant ?? undefined"
|
||||
:variant="item.variant ?? undefined"
|
||||
:href="item.href ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
@@ -306,23 +348,8 @@ const modalWidth = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="available || availableWithRenewal" class="mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 rounded p-2">
|
||||
<Switch
|
||||
v-model="ignoreThisRelease"
|
||||
:class="
|
||||
ignoreThisRelease
|
||||
? 'from-unraid-red to-orange bg-linear-to-r'
|
||||
: 'data-[state=unchecked]:bg-opacity-10 data-[state=unchecked]:bg-foreground data-[state=unchecked]:bg-transparent'
|
||||
"
|
||||
/>
|
||||
<Label class="text-base">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="updateOsIgnoredReleases.length > 0"
|
||||
v-if="updateOsIgnoredReleases.length > 0 && !(available || availableWithRenewal)"
|
||||
class="mx-auto flex w-full max-w-[640px] flex-col gap-2"
|
||||
>
|
||||
<h3 class="text-left text-base font-semibold italic">
|
||||
@@ -338,7 +365,7 @@ const modalWidth = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<ResponsiveModalFooter>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
@@ -347,31 +374,75 @@ const modalWidth = computed(() => {
|
||||
)
|
||||
"
|
||||
>
|
||||
<div :class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-2')">
|
||||
<Button variant="ghost" @click="close">
|
||||
<XMarkIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('Close') }}
|
||||
</Button>
|
||||
<Button variant="ghost" @click="accountStore.updateOs()">
|
||||
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('More options') }}
|
||||
</Button>
|
||||
<div
|
||||
v-if="actionButtons"
|
||||
:class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-3')"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" @click="accountStore.updateOs()">
|
||||
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('More Options') }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-xs">
|
||||
<div class="flex items-start gap-2">
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="text-muted-foreground mt-0.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<p class="text-left text-sm">
|
||||
{{
|
||||
t(
|
||||
'Manage update preferences including beta access and version selection at account.unraid.net'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-2')">
|
||||
<BrandButton
|
||||
v-for="item in actionButtons"
|
||||
:key="item.text"
|
||||
:btn-style="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text ?? '')"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-3')">
|
||||
<template v-for="item in actionButtons" :key="item.text">
|
||||
<TooltipProvider v-if="ignoreThisRelease && item.text === localizedCloseText">
|
||||
<Tooltip :delay-duration="300">
|
||||
<TooltipTrigger as-child>
|
||||
<BrandButton
|
||||
:variant="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="item.text ?? ''"
|
||||
:title="item.title ? item.title : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
'You can opt back in to an ignored release by clicking on the Check for Updates button in the header anytime'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BrandButton
|
||||
v-else
|
||||
:variant="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="item.text ?? ''"
|
||||
:title="item.title ? item.title : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</ResponsiveModalFooter>
|
||||
</div>
|
||||
</ResponsiveModal>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button, Label, Switch } from '@unraid/ui';
|
||||
|
||||
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
const { t } = useI18n();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// Test scenarios
|
||||
const testScenarios = [
|
||||
{
|
||||
id: 'expired-ineligible',
|
||||
name: 'Expired key with ineligible update',
|
||||
description: 'License expired, update available but not eligible',
|
||||
serverState: 'EEXPIRED' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'normal-update',
|
||||
name: 'Normal update available',
|
||||
description: 'Active license with eligible update',
|
||||
serverState: 'BASIC' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'abc123def456789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'renewal-required',
|
||||
name: 'Update requires renewal',
|
||||
description: 'License expired > 1 year, update requires renewal',
|
||||
serverState: 'STARTER' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'no-update',
|
||||
name: 'No update available',
|
||||
description: 'Already on latest version',
|
||||
serverState: 'BASIC' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.0.0',
|
||||
name: 'Unraid 7.0.0',
|
||||
date: '2024-01-15',
|
||||
isNewer: false,
|
||||
isEligible: true,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.0.0.md',
|
||||
sha256: 'xyz789abc123',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trial-update',
|
||||
name: 'Trial with update',
|
||||
description: 'Trial license with update available',
|
||||
serverState: 'TRIAL' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'def456ghi789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro-auth-required',
|
||||
name: 'Pro license - auth required',
|
||||
description: 'Pro license but authentication required for download',
|
||||
serverState: 'PRO' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Component state
|
||||
const selectedScenario = ref('normal-update');
|
||||
const ignoreRelease = ref(false);
|
||||
const checkingForUpdates = ref(false);
|
||||
const ignoredReleases = ref<string[]>([]);
|
||||
|
||||
// Use the store's modal state directly
|
||||
const modalOpen = computed({
|
||||
get: () => updateOsStore.updateOsModalVisible,
|
||||
set: (value) => updateOsStore.setModalOpen(value),
|
||||
});
|
||||
|
||||
// Apply scenario
|
||||
const applyScenario = () => {
|
||||
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
|
||||
if (!scenario) return;
|
||||
|
||||
// Set server state
|
||||
const currentTime = Date.now();
|
||||
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
|
||||
const regExp =
|
||||
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
|
||||
|
||||
// Apply update response
|
||||
if (scenario.serverState === 'EEXPIRED') {
|
||||
serverStore.$patch({
|
||||
expireTime: expiredTime,
|
||||
state: scenario.serverState,
|
||||
regExp: undefined,
|
||||
});
|
||||
} else if (scenario.serverState === 'STARTER') {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState,
|
||||
regExp: regExp,
|
||||
regTy: 'Starter',
|
||||
});
|
||||
} else {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState,
|
||||
regExp: undefined,
|
||||
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
|
||||
|
||||
// Apply ignored releases
|
||||
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
|
||||
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
|
||||
ignoredReleases.value.push(scenario.updateResponse.version);
|
||||
}
|
||||
} else {
|
||||
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
|
||||
}
|
||||
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
|
||||
};
|
||||
|
||||
// Watch for scenario changes
|
||||
watch([selectedScenario, ignoreRelease], () => {
|
||||
applyScenario();
|
||||
});
|
||||
|
||||
// Watch for loading state changes
|
||||
watch(checkingForUpdates, (value) => {
|
||||
updateOsStore.checkForUpdatesLoading = value;
|
||||
});
|
||||
|
||||
// Open modal with scenario
|
||||
const openModal = () => {
|
||||
applyScenario();
|
||||
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
|
||||
updateOsStore.setModalOpen(true);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
applyScenario();
|
||||
|
||||
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto max-w-4xl p-6">
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Test various update scenarios for the CheckUpdateResponseModal component
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Scenario Selection -->
|
||||
<div class="space-y-4">
|
||||
<Label class="text-lg font-semibold">Select Test Scenario</Label>
|
||||
<div class="space-y-3">
|
||||
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
:id="scenario.id"
|
||||
:value="scenario.id"
|
||||
v-model="selectedScenario"
|
||||
class="mt-1 rounded-full"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<Label :for="scenario.id" class="block cursor-pointer font-medium">
|
||||
{{ scenario.name }}
|
||||
</Label>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="space-y-4 border-t pt-4">
|
||||
<h3 class="font-semibold">Options</h3>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
|
||||
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
|
||||
<Label for="checking-updates" class="cursor-pointer"
|
||||
>Show checking for updates loading state</Label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="space-y-2 border-t pt-4">
|
||||
<h3 class="font-semibold">Current Scenario Details</h3>
|
||||
<div class="space-y-1 font-mono text-sm">
|
||||
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
|
||||
<p>
|
||||
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Eligible:</span>
|
||||
{{ currentScenario?.updateResponse.isEligible }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Has SHA256:</span>
|
||||
{{ !!currentScenario?.updateResponse.sha256 }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Ignored Releases:</span>
|
||||
{{ ignoredReleases.join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Modal Button -->
|
||||
<div class="border-t pt-4">
|
||||
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal Component -->
|
||||
<CheckUpdateResponseModal
|
||||
:open="modalOpen"
|
||||
@update:open="
|
||||
(val: boolean) => {
|
||||
modalOpen = val;
|
||||
}
|
||||
"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,4 +140,14 @@ export const componentMappings: ComponentMapping[] = [
|
||||
selector: ['unraid-toaster', 'uui-toaster'],
|
||||
appId: 'toaster',
|
||||
},
|
||||
{
|
||||
loader: () => import('../UpdateOs/TestUpdateModal.standalone.vue'),
|
||||
selector: 'unraid-test-update-modal',
|
||||
appId: 'test-update-modal',
|
||||
},
|
||||
{
|
||||
loader: () => import('../TestThemeSwitcher.standalone.vue'),
|
||||
selector: 'unraid-test-theme-switcher',
|
||||
appId: 'test-theme-switcher',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmVariant?: 'primary' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const isOpen = ref(false);
|
||||
const state = shallowRef<ConfirmState | null>(null);
|
||||
|
||||
export function useConfirm() {
|
||||
const confirm = (options: ConfirmOptions): Promise<boolean> => {
|
||||
// Resolve any existing dialog promise with false before opening a new one
|
||||
if (state.value?.resolve) {
|
||||
const previousResolve = state.value.resolve;
|
||||
previousResolve(false);
|
||||
state.value = null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
state.value = {
|
||||
...options,
|
||||
confirmText: options.confirmText ?? 'Confirm',
|
||||
cancelText: options.cancelText ?? 'Cancel',
|
||||
confirmVariant: options.confirmVariant ?? 'primary',
|
||||
resolve,
|
||||
};
|
||||
isOpen.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
state.value?.resolve(true);
|
||||
isOpen.value = false;
|
||||
state.value = null;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
state.value?.resolve(false);
|
||||
isOpen.value = false;
|
||||
state.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
confirm,
|
||||
isOpen,
|
||||
state,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button, Label, Switch } from '@unraid/ui';
|
||||
import { useDummyServerStore } from '~/_data/serverState';
|
||||
|
||||
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
const { t } = useI18n();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const serverStore = useServerStore();
|
||||
const dummyServerStore = useDummyServerStore();
|
||||
|
||||
// Test scenarios
|
||||
const testScenarios = [
|
||||
{
|
||||
id: 'expired-ineligible',
|
||||
name: 'Expired key with ineligible update',
|
||||
description: 'License expired, update available but not eligible',
|
||||
serverState: 'EEXPIRED',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'normal-update',
|
||||
name: 'Normal update available',
|
||||
description: 'Active license with eligible update',
|
||||
serverState: 'BASIC',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: 'abc123def456789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'renewal-required',
|
||||
name: 'Update requires renewal',
|
||||
description: 'License expired > 1 year, update requires renewal',
|
||||
serverState: 'STARTER',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'no-update',
|
||||
name: 'No update available',
|
||||
description: 'Already on latest version',
|
||||
serverState: 'BASIC',
|
||||
updateResponse: {
|
||||
version: '7.0.0',
|
||||
name: 'Unraid 7.0.0',
|
||||
date: '2024-01-15',
|
||||
isNewer: false,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.0.0/',
|
||||
sha256: 'xyz789abc123',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trial-update',
|
||||
name: 'Trial with update',
|
||||
description: 'Trial license with update available',
|
||||
serverState: 'TRIAL',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'def456ghi789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro-auth-required',
|
||||
name: 'Pro license - auth required',
|
||||
description: 'Pro license but authentication required for download',
|
||||
serverState: 'PRO',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Component state
|
||||
const selectedScenario = ref('normal-update');
|
||||
const modalOpen = ref(false);
|
||||
const ignoreRelease = ref(false);
|
||||
const checkingForUpdates = ref(false);
|
||||
const ignoredReleases = ref<string[]>([]);
|
||||
|
||||
// Apply scenario
|
||||
const applyScenario = () => {
|
||||
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
|
||||
if (!scenario) return;
|
||||
|
||||
// Apply server state
|
||||
dummyServerStore.selector =
|
||||
scenario.serverState === 'EEXPIRED' || scenario.serverState === 'STARTER' ? 'default' : 'default';
|
||||
|
||||
// Set server state
|
||||
const currentTime = Date.now();
|
||||
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
|
||||
const regExp =
|
||||
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
|
||||
|
||||
// Apply update response
|
||||
if (scenario.serverState === 'EEXPIRED') {
|
||||
serverStore.$patch({
|
||||
expireTime: expiredTime,
|
||||
state: 'EEXPIRED' as ServerState,
|
||||
regExp: undefined,
|
||||
});
|
||||
} else if (scenario.serverState === 'STARTER') {
|
||||
serverStore.$patch({
|
||||
state: 'STARTER' as ServerState,
|
||||
regExp: regExp,
|
||||
regTy: 'Starter',
|
||||
});
|
||||
} else {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState as ServerState,
|
||||
regExp: undefined,
|
||||
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
|
||||
|
||||
// Apply ignored releases
|
||||
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
|
||||
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
|
||||
ignoredReleases.value.push(scenario.updateResponse.version);
|
||||
}
|
||||
} else {
|
||||
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
|
||||
}
|
||||
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
|
||||
};
|
||||
|
||||
// Watch for scenario changes
|
||||
watch([selectedScenario, ignoreRelease], () => {
|
||||
applyScenario();
|
||||
});
|
||||
|
||||
// Open modal with scenario
|
||||
const openModal = () => {
|
||||
applyScenario();
|
||||
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
|
||||
modalOpen.value = true;
|
||||
updateOsStore.setModalOpen(true);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
applyScenario();
|
||||
|
||||
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto max-w-4xl p-6">
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Test various update scenarios for the CheckUpdateResponseModal component
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<!-- Scenario Selection -->
|
||||
<div class="space-y-4">
|
||||
<Label class="text-lg font-semibold">Select Test Scenario</Label>
|
||||
<div class="space-y-3">
|
||||
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
:id="scenario.id"
|
||||
:value="scenario.id"
|
||||
v-model="selectedScenario"
|
||||
class="mt-1 rounded-full"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<Label :for="scenario.id" class="block cursor-pointer font-medium">
|
||||
{{ scenario.name }}
|
||||
</Label>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="space-y-4 border-t pt-4">
|
||||
<h3 class="font-semibold">Options</h3>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
|
||||
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
|
||||
<Label for="checking-updates" class="cursor-pointer"
|
||||
>Show checking for updates loading state</Label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="space-y-2 border-t pt-4">
|
||||
<h3 class="font-semibold">Current Scenario Details</h3>
|
||||
<div class="space-y-1 font-mono text-sm">
|
||||
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
|
||||
<p>
|
||||
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Eligible:</span>
|
||||
{{ currentScenario?.updateResponse.isEligible }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Has SHA256:</span>
|
||||
{{ !!currentScenario?.updateResponse.sha256 }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Ignored Releases:</span>
|
||||
{{ ignoredReleases.join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Modal Button -->
|
||||
<div class="border-t pt-4">
|
||||
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal Component -->
|
||||
<CheckUpdateResponseModal
|
||||
:open="modalOpen"
|
||||
@update:open="
|
||||
(val: boolean) => {
|
||||
modalOpen = val;
|
||||
updateOsStore.setModalOpen(val);
|
||||
}
|
||||
"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Update Modal Test - Unraid Component Test</title>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<style>
|
||||
html {
|
||||
font-size: 10px;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.header {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
.back-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.back-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<a href="/test-pages/" class="back-link">← Back to Test Pages</a>
|
||||
<h1>🧪 Update Modal Test Scenarios</h1>
|
||||
</div>
|
||||
<div>
|
||||
<unraid-test-theme-switcher></unraid-test-theme-switcher>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mount the test component -->
|
||||
<unraid-test-update-modal></unraid-test-update-modal>
|
||||
|
||||
<!-- Mount the modals component which includes the changelog modal -->
|
||||
<unraid-modals></unraid-modals>
|
||||
|
||||
<!-- Load the manifest and inject resources -->
|
||||
<script src="/test-pages/load-manifest.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user