Skip to content

Commit 8619294

Browse files
committed
fix(onboarding): reuse confirm dialog in standalone boot flow
- Purpose: reuse the shared internal boot reboot/shutdown confirmation dialog in the standalone internal boot wizard. - Before: the onboarding Next Steps screen showed a confirmation modal, but the standalone internal boot wizard called reboot and shutdown immediately from its locked result state. - Problem: the same internal boot follow-up action behaved differently depending on which flow the user was in, which made the UX inconsistent and skipped the new licensing guidance in the standalone path. - Change: route standalone reboot and shutdown actions through InternalBootConfirmDialog before executing the power command. - How it works: the standalone wizard now tracks a pending power action, opens the shared dialog for reboot or shutdown, and only submits the matching helper after the dialog confirms. - Tests: updated the standalone internal boot wizard test to verify the shared dialog opens for both actions and that the reboot/shutdown helpers run only after confirmation.
1 parent cf56cad commit 8619294

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

web/__test__/components/Onboarding/OnboardingInternalBootStandalone.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const {
4444
submitInternalBootShutdownMock,
4545
cleanupOnboardingStorageMock,
4646
dialogPropsRef,
47+
internalBootConfirmDialogPropsRef,
4748
stepPropsRef,
4849
stepperPropsRef,
4950
} = vi.hoisted(() => ({
@@ -65,6 +66,7 @@ const {
6566
>(),
6667
cleanupOnboardingStorageMock: vi.fn(),
6768
dialogPropsRef: { value: null as Record<string, unknown> | null },
69+
internalBootConfirmDialogPropsRef: { value: null as Record<string, unknown> | null },
6870
stepPropsRef: { value: null as Record<string, unknown> | null },
6971
stepperPropsRef: { value: null as Record<string, unknown> | null },
7072
}));
@@ -103,6 +105,28 @@ vi.mock('@/components/Onboarding/components/OnboardingConsole.vue', () => ({
103105
},
104106
}));
105107

108+
vi.mock('@/components/Onboarding/components/InternalBootConfirmDialog.vue', () => ({
109+
default: {
110+
props: ['open', 'action', 'disabled'],
111+
emits: ['confirm', 'cancel'],
112+
setup(props: Record<string, unknown>) {
113+
internalBootConfirmDialogPropsRef.value = props;
114+
return { props };
115+
},
116+
template: `
117+
<div v-if="open" data-testid="internal-boot-confirm-dialog-stub">
118+
<div data-testid="internal-boot-confirm-dialog-action">{{ props.action }}</div>
119+
<button data-testid="internal-boot-confirm-dialog-confirm" @click="$emit('confirm')">
120+
Confirm
121+
</button>
122+
<button data-testid="internal-boot-confirm-dialog-cancel" @click="$emit('cancel')">
123+
Cancel
124+
</button>
125+
</div>
126+
`,
127+
},
128+
}));
129+
106130
vi.mock('@/components/Onboarding/OnboardingSteps.vue', () => ({
107131
default: {
108132
props: ['steps', 'activeStepIndex', 'onStepClick'],
@@ -231,6 +255,7 @@ describe('OnboardingInternalBoot.standalone.vue', () => {
231255
selection: null,
232256
};
233257
dialogPropsRef.value = null;
258+
internalBootConfirmDialogPropsRef.value = null;
234259
stepPropsRef.value = null;
235260
stepperPropsRef.value = null;
236261
applyInternalBootSelectionMock.mockResolvedValue({
@@ -596,7 +621,7 @@ describe('OnboardingInternalBoot.standalone.vue', () => {
596621
expect(wrapper.find('[data-testid="internal-boot-standalone-shutdown"]').exists()).toBe(true);
597622
});
598623

599-
it('calls reboot and shutdown helpers from the locked result actions', async () => {
624+
it('routes locked-result reboot and shutdown actions through the shared confirm dialog', async () => {
600625
configureDraftState.value = {
601626
bootMode: 'storage',
602627
skipped: false,
@@ -617,10 +642,25 @@ describe('OnboardingInternalBoot.standalone.vue', () => {
617642

618643
await wrapper.get('[data-testid="internal-boot-standalone-reboot"]').trigger('click');
619644
await flushPromises();
645+
expect(wrapper.find('[data-testid="internal-boot-confirm-dialog-stub"]').exists()).toBe(true);
646+
expect(internalBootConfirmDialogPropsRef.value).toMatchObject({
647+
open: true,
648+
action: 'reboot',
649+
});
650+
expect(submitInternalBootRebootMock).not.toHaveBeenCalled();
651+
await wrapper.get('[data-testid="internal-boot-confirm-dialog-confirm"]').trigger('click');
652+
await flushPromises();
620653
expect(submitInternalBootRebootMock).toHaveBeenCalledTimes(1);
621654

622655
await wrapper.get('[data-testid="internal-boot-standalone-shutdown"]').trigger('click');
623656
await flushPromises();
657+
expect(internalBootConfirmDialogPropsRef.value).toMatchObject({
658+
open: true,
659+
action: 'shutdown',
660+
});
661+
expect(submitInternalBootShutdownMock).not.toHaveBeenCalled();
662+
await wrapper.get('[data-testid="internal-boot-confirm-dialog-confirm"]').trigger('click');
663+
await flushPromises();
624664
expect(submitInternalBootShutdownMock).toHaveBeenCalledTimes(1);
625665
});
626666
});

web/src/components/Onboarding/standalone/OnboardingInternalBoot.standalone.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { XMarkIcon } from '@heroicons/vue/24/solid';
66
import { Dialog } from '@unraid/ui';
77
import { buildBootConfigurationSummaryViewModel } from '@/components/Onboarding/components/bootConfigurationSummary/buildBootConfigurationSummaryViewModel';
88
import OnboardingBootConfigurationSummary from '@/components/Onboarding/components/bootConfigurationSummary/OnboardingBootConfigurationSummary.vue';
9+
import InternalBootConfirmDialog from '@/components/Onboarding/components/InternalBootConfirmDialog.vue';
910
import OnboardingConsole from '@/components/Onboarding/components/OnboardingConsole.vue';
1011
import {
1112
applyInternalBootSelection,
@@ -39,6 +40,7 @@ const currentStep = ref<StepId>('CONFIGURE_BOOT');
3940
const confirmationState = ref<'idle' | 'saving' | 'result'>('idle');
4041
const isOpen = ref(true);
4142
const logs = ref<LogEntry[]>([]);
43+
const pendingPowerAction = ref<'reboot' | 'shutdown' | null>(null);
4244
const resultTitle = ref('');
4345
const resultMessage = ref('');
4446
const resultSeverity = ref<'success' | 'warning' | 'error'>('success');
@@ -62,7 +64,7 @@ const standaloneSummaryInvalidMessage = computed(() => summaryT('bootConfig.inva
6264
6365
const isLocked = computed(() => internalBootState.value.applyAttempted);
6466
65-
const handlePowerAction = (action: 'reboot' | 'shutdown') => {
67+
const executePowerAction = (action: 'reboot' | 'shutdown') => {
6668
cleanupOnboardingStorage();
6769
if (action === 'shutdown') {
6870
submitInternalBootShutdown();
@@ -71,6 +73,22 @@ const handlePowerAction = (action: 'reboot' | 'shutdown') => {
7173
}
7274
};
7375
76+
const handlePowerAction = (action: 'reboot' | 'shutdown') => {
77+
pendingPowerAction.value = action;
78+
};
79+
80+
const handleConfirmPowerAction = () => {
81+
const action = pendingPowerAction.value;
82+
pendingPowerAction.value = null;
83+
if (action) {
84+
executePowerAction(action);
85+
}
86+
};
87+
88+
const handleCancelPowerAction = () => {
89+
pendingPowerAction.value = null;
90+
};
91+
7492
const canReturnToConfigure = () =>
7593
!isLocked.value &&
7694
confirmationState.value === 'result' &&
@@ -618,5 +636,11 @@ onUnmounted(() => {
618636
</div>
619637
</div>
620638
</div>
639+
<InternalBootConfirmDialog
640+
:open="pendingPowerAction !== null"
641+
:action="pendingPowerAction ?? 'reboot'"
642+
@confirm="handleConfirmPowerAction"
643+
@cancel="handleCancelPowerAction"
644+
/>
621645
</Dialog>
622646
</template>

0 commit comments

Comments
 (0)