mirror of
https://github.com/unraid/api.git
synced 2026-01-04 15:39:52 -06:00
feat(activation): enhance upgrade step handling in ActivationModal
- Updated the `ActivationModal` component to include a new method for completing pending upgrade steps, improving the onboarding experience. - Modified the `closeModal` function to ensure pending steps are marked as complete before closing the modal. - Enhanced tests to verify the correct behavior of upgrade step completion and modal interactions. - Updated documentation to reflect changes in upgrade onboarding behavior. This update improves the user experience by ensuring that all pending upgrade steps are properly handled during the activation process.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -111,7 +111,7 @@ const mockPurchaseStore = {
|
||||
|
||||
const mockStepDefinitions = [
|
||||
{
|
||||
id: 'timezone',
|
||||
id: 'TIMEZONE',
|
||||
required: true,
|
||||
completed: false,
|
||||
introducedIn: '7.0.0',
|
||||
@@ -120,7 +120,7 @@ const mockStepDefinitions = [
|
||||
icon: 'i-heroicons-clock',
|
||||
},
|
||||
{
|
||||
id: 'plugins',
|
||||
id: 'PLUGINS',
|
||||
required: false,
|
||||
completed: false,
|
||||
introducedIn: '7.0.0',
|
||||
@@ -129,7 +129,7 @@ const mockStepDefinitions = [
|
||||
icon: 'i-heroicons-puzzle-piece',
|
||||
},
|
||||
{
|
||||
id: 'activation',
|
||||
id: 'ACTIVATION',
|
||||
required: true,
|
||||
completed: false,
|
||||
introducedIn: '7.0.0',
|
||||
@@ -145,10 +145,17 @@ const mockUpgradeOnboardingStore = {
|
||||
allUpgradeSteps: ref(mockStepDefinitions),
|
||||
currentVersion: ref('7.0.0'),
|
||||
previousVersion: ref('6.12.0'),
|
||||
setIsHidden: vi.fn(),
|
||||
refetchActivationOnboarding: vi.fn(),
|
||||
refetchActivationOnboarding: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mutateMock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useMutation: () => ({
|
||||
mutate: mutateMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock all imports
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as typeof import('vue-i18n');
|
||||
@@ -216,6 +223,8 @@ window.addEventListener = vi.fn((event: string, handler: EventListenerOrEventLis
|
||||
describe('Activation/ActivationModal.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mutateMock.mockClear();
|
||||
mockUpgradeOnboardingStore.refetchActivationOnboarding.mockClear();
|
||||
|
||||
mockActivationCodeDataStore.partnerInfo.value = {
|
||||
hasPartnerLogo: false,
|
||||
@@ -231,6 +240,11 @@ describe('Activation/ActivationModal.vue', () => {
|
||||
});
|
||||
|
||||
handleKeydown = null;
|
||||
mockUpgradeOnboardingStore.shouldShowUpgradeOnboarding.value = false;
|
||||
mockUpgradeOnboardingStore.upgradeSteps.value = mockStepDefinitions.map((step) => ({ ...step }));
|
||||
mockUpgradeOnboardingStore.allUpgradeSteps.value = mockStepDefinitions.map((step) => ({
|
||||
...step,
|
||||
}));
|
||||
});
|
||||
|
||||
const mountComponent = () => {
|
||||
@@ -340,6 +354,37 @@ describe('Activation/ActivationModal.vue', () => {
|
||||
expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('marks pending upgrade steps complete when the modal is closed', async () => {
|
||||
mockUpgradeOnboardingStore.shouldShowUpgradeOnboarding.value = true;
|
||||
mockUpgradeOnboardingStore.upgradeSteps.value = [
|
||||
{
|
||||
id: 'TIMEZONE',
|
||||
required: true,
|
||||
completed: false,
|
||||
introducedIn: '7.0.0',
|
||||
},
|
||||
{
|
||||
id: 'PLUGINS',
|
||||
required: false,
|
||||
completed: false,
|
||||
introducedIn: '7.0.0',
|
||||
},
|
||||
];
|
||||
mockUpgradeOnboardingStore.allUpgradeSteps.value = mockUpgradeOnboardingStore.upgradeSteps.value;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
const dialog = wrapper.findComponent({ name: 'Dialog' });
|
||||
expect(dialog.exists()).toBe(true);
|
||||
|
||||
dialog.vm.$emit('update:modelValue', false);
|
||||
await flushPromises();
|
||||
|
||||
expect(mutateMock).toHaveBeenCalledTimes(2);
|
||||
expect(mutateMock).toHaveBeenNthCalledWith(1, { input: { stepId: 'TIMEZONE' } });
|
||||
expect(mutateMock).toHaveBeenNthCalledWith(2, { input: { stepId: 'PLUGINS' } });
|
||||
expect(mockUpgradeOnboardingStore.refetchActivationOnboarding).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders activation steps with correct active step', () => {
|
||||
const wrapper = mountComponent();
|
||||
|
||||
|
||||
@@ -120,24 +120,52 @@ const docsButtons = computed<BrandButtonProps[]>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
upgradeStore.setIsHidden(true);
|
||||
modalStore.setIsHidden(true);
|
||||
type MarkUpgradeStepOptions = {
|
||||
skipRefetch?: boolean;
|
||||
};
|
||||
|
||||
const { mutate: completeUpgradeStepMutation } = useMutation(COMPLETE_UPGRADE_STEP_MUTATION);
|
||||
|
||||
const markUpgradeStepCompleted = async (stepId: StepId | null) => {
|
||||
const markUpgradeStepCompleted = async (stepId: StepId | null, options: MarkUpgradeStepOptions = {}) => {
|
||||
if (!stepId) return;
|
||||
|
||||
try {
|
||||
await completeUpgradeStepMutation({ input: { stepId } });
|
||||
await refetchActivationOnboarding();
|
||||
if (!options.skipRefetch) {
|
||||
await refetchActivationOnboarding();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ActivationModal] Failed to mark upgrade step completed', error);
|
||||
}
|
||||
};
|
||||
|
||||
const completePendingUpgradeSteps = async () => {
|
||||
if (!shouldShowUpgradeOnboarding.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingSteps = upgradeSteps.value.map((step) => step.id as StepId);
|
||||
if (pendingSteps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const stepId of pendingSteps) {
|
||||
await markUpgradeStepCompleted(stepId, { skipRefetch: true });
|
||||
}
|
||||
await refetchActivationOnboarding();
|
||||
} catch (error) {
|
||||
console.error('[ActivationModal] Failed to complete pending upgrade steps', error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = async () => {
|
||||
if (shouldShowUpgradeOnboarding.value) {
|
||||
await completePendingUpgradeSteps();
|
||||
}
|
||||
modalStore.setIsHidden(true);
|
||||
};
|
||||
|
||||
const goToNextStep = async () => {
|
||||
if (availableSteps.value.length > 0) {
|
||||
// Only mark as completed if the current step is not already completed
|
||||
@@ -151,12 +179,12 @@ const goToNextStep = async () => {
|
||||
currentStepIndex.value++;
|
||||
} else {
|
||||
// If we're at the last step, close the modal
|
||||
closeModal();
|
||||
await closeModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
closeModal();
|
||||
await closeModal();
|
||||
};
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
@@ -286,7 +314,13 @@ watch(
|
||||
:show-close-button="isHidden === false || shouldShowUpgradeOnboarding"
|
||||
size="full"
|
||||
class="bg-background"
|
||||
@update:model-value="(value) => !value && closeModal()"
|
||||
@update:model-value="
|
||||
async (value) => {
|
||||
if (!value) {
|
||||
await closeModal();
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
<div v-if="partnerInfo?.hasPartnerLogo && !shouldShowUpgradeOnboarding">
|
||||
|
||||
@@ -52,7 +52,7 @@ This system shows contextual onboarding steps to users when they upgrade their U
|
||||
- Automatically detects which mode based on system state
|
||||
- Displays relevant steps for each mode
|
||||
- Reuses existing step components (timezone, plugins)
|
||||
- Persists "hidden" state per mode to session storage
|
||||
- Relies on recorded completion status from the onboarding tracker
|
||||
|
||||
## Adding New Steps
|
||||
|
||||
@@ -131,6 +131,6 @@ To test the upgrade flow:
|
||||
|
||||
- Fresh installs (no `lastTrackedVersion`) won't trigger upgrade onboarding until steps exist
|
||||
- The modal automatically switches between fresh install and upgrade modes
|
||||
- Each mode can be dismissed independently (stored in sessionStorage)
|
||||
- Dismissal is tracked through the onboarding mutations so all browsers stay in sync
|
||||
- Version comparison uses semver for reliable ordering
|
||||
- The same modal component handles both modes for consistency
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useQuery } from '@vue/apollo-composable';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
|
||||
import type { ActivationOnboardingQuery } from '~/composables/gql/graphql';
|
||||
|
||||
import { ACTIVATION_ONBOARDING_QUERY } from '~/components/Activation/activationOnboarding.query';
|
||||
|
||||
const UPGRADE_ONBOARDING_HIDDEN_KEY = 'upgrade-onboarding-hidden';
|
||||
|
||||
export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () => {
|
||||
const {
|
||||
result: activationOnboardingResult,
|
||||
@@ -16,8 +13,6 @@ export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () =>
|
||||
refetch,
|
||||
} = useQuery(ACTIVATION_ONBOARDING_QUERY, {}, { errorPolicy: 'all' });
|
||||
|
||||
const isHidden = useSessionStorage<boolean>(UPGRADE_ONBOARDING_HIDDEN_KEY, false);
|
||||
|
||||
const onboardingData = computed<ActivationOnboardingQuery['activationOnboarding'] | undefined>(
|
||||
() => activationOnboardingResult.value?.activationOnboarding
|
||||
);
|
||||
@@ -38,13 +33,9 @@ export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () =>
|
||||
);
|
||||
|
||||
const shouldShowUpgradeOnboarding = computed(() => {
|
||||
return !isHidden.value && (onboardingData.value?.hasPendingSteps ?? false);
|
||||
return onboardingData.value?.hasPendingSteps ?? false;
|
||||
});
|
||||
|
||||
const setIsHidden = (value: boolean) => {
|
||||
isHidden.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
loading: computed(() => activationOnboardingLoading.value),
|
||||
isUpgrade,
|
||||
@@ -54,8 +45,6 @@ export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () =>
|
||||
allUpgradeSteps,
|
||||
upgradeSteps,
|
||||
shouldShowUpgradeOnboarding,
|
||||
isHidden,
|
||||
setIsHidden,
|
||||
refetchActivationOnboarding: refetch,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user