feat(activation): enhance plugin installation flow and UI feedback

- Updated the `ActivationPluginsStep` component to improve the handling of plugin installations, including better state management for installation completion.
- Introduced a new primary action button that dynamically updates based on installation status and selected plugins.
- Enhanced unit tests to cover new checkbox interactions and verify the correct behavior of the installation process.
- Updated localization files to include new messages related to the installation process.

This update significantly improves user experience by providing clearer feedback during plugin installations and ensuring proper state management throughout the process.
This commit is contained in:
Eli Bosley
2025-10-15 17:46:10 -04:00
parent da381b5f95
commit 2decc234b8
3 changed files with 76 additions and 11 deletions

View File

@@ -65,6 +65,14 @@ describe('ActivationPluginsStep', () => {
const { wrapper, props } = mountComponent();
const checkboxes = wrapper.findAll('input[type="checkbox"]');
for (const checkbox of checkboxes) {
const input = checkbox.element as HTMLInputElement;
input.checked = true;
await checkbox.trigger('change');
}
await flushPromises();
const installButton = wrapper
.findAll('[data-testid="brand-button"]')
.find((button) => button.text().includes('Install'));
@@ -76,15 +84,33 @@ describe('ActivationPluginsStep', () => {
const firstCallArgs = installPluginMock.mock.calls[0]?.[0];
expect(firstCallArgs?.forced).toBe(true);
expect(firstCallArgs?.url).toContain('community.applications');
expect(props.onComplete).toHaveBeenCalled();
expect(props.onComplete).not.toHaveBeenCalled();
expect(wrapper.html()).toContain('installation started');
expect(wrapper.html()).toContain('installed successfully');
const continueButton = wrapper
.findAll('[data-testid="brand-button"]')
.find((button) => button.text().includes('Continue'));
expect(continueButton).toBeTruthy();
const callsBeforeContinue = props.onComplete.mock.calls.length;
await continueButton!.trigger('click');
expect(props.onComplete.mock.calls.length).toBeGreaterThanOrEqual(callsBeforeContinue + 1);
});
it('shows error message when installation fails', async () => {
installPluginMock.mockRejectedValueOnce(new Error('install failed'));
const { wrapper, props } = mountComponent();
const errorCheckboxes = wrapper.findAll('input[type="checkbox"]');
for (const checkbox of errorCheckboxes) {
const input = checkbox.element as HTMLInputElement;
input.checked = true;
await checkbox.trigger('change');
}
await flushPromises();
const installButton = wrapper
.findAll('[data-testid="brand-button"]')
.find((button) => button.text().includes('Install'));

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { BrandButton } from '@unraid/ui';
@@ -46,10 +46,11 @@ const availablePlugins: Plugin[] = [
},
];
const selectedPlugins = ref<Set<string>>(new Set(availablePlugins.map((p) => p.id)));
const selectedPlugins = ref<Set<string>>(new Set());
const isInstalling = ref(false);
const error = ref<string | null>(null);
const installationLogs = ref<string[]>([]);
const installationFinished = ref(false);
const { installPlugin } = usePluginInstaller();
@@ -61,6 +62,10 @@ const appendLogs = (lines: string[] | string) => {
}
};
const resetCompletionState = () => {
installationFinished.value = false;
};
const togglePlugin = (pluginId: string) => {
const next = new Set(selectedPlugins.value);
if (next.has(pluginId)) {
@@ -69,10 +74,12 @@ const togglePlugin = (pluginId: string) => {
next.add(pluginId);
}
selectedPlugins.value = next;
resetCompletionState();
};
const handleInstall = async () => {
if (selectedPlugins.value.size === 0) {
installationFinished.value = true;
props.onComplete();
return;
}
@@ -80,6 +87,7 @@ const handleInstall = async () => {
isInstalling.value = true;
error.value = null;
installationLogs.value = [];
installationFinished.value = false;
try {
const pluginsToInstall = availablePlugins.filter((p) => selectedPlugins.value.has(p.id));
@@ -105,10 +113,11 @@ const handleInstall = async () => {
appendLogs(t('activation.pluginsStep.pluginInstalledMessage', { name: plugin.name }));
}
props.onComplete();
installationFinished.value = true;
} catch (err) {
error.value = t('activation.pluginsStep.installFailed');
console.error('Failed to install plugins:', err);
installationFinished.value = false;
} finally {
isInstalling.value = false;
}
@@ -121,6 +130,39 @@ const handleSkip = () => {
const handleBack = () => {
props.onBack?.();
};
const handlePrimaryAction = async () => {
if (installationFinished.value || selectedPlugins.value.size === 0) {
props.onComplete();
return;
}
if (!isInstalling.value) {
await handleInstall();
}
};
const primaryButtonText = computed(() => {
if (installationFinished.value) {
return t('common.continue');
}
if (selectedPlugins.value.size > 0) {
return t('activation.pluginsStep.installSelected');
}
return t('common.continue');
});
const isPrimaryActionDisabled = computed(() => {
if (isInstalling.value) {
return true;
}
if (installationFinished.value) {
return false;
}
return selectedPlugins.value.size === 0;
});
</script>
<template>
@@ -184,14 +226,10 @@ const handleBack = () => {
@click="handleSkip"
/>
<BrandButton
:text="
selectedPlugins.size > 0
? t('activation.pluginsStep.installAndContinue')
: t('common.continue')
"
:disabled="isInstalling"
:text="primaryButtonText"
:disabled="isPrimaryActionDisabled"
:loading="isInstalling"
@click="handleInstall"
@click="handlePrimaryAction"
/>
</div>
</div>

View File

@@ -14,6 +14,7 @@
"activation.activationSteps.unleashYourHardware": "Unleash Your Hardware",
"activation.pluginsStep.addHelpfulPlugins": "Add helpful plugins",
"activation.pluginsStep.installAndContinue": "Install & Continue",
"activation.pluginsStep.installSelected": "Install Selected",
"activation.pluginsStep.installEssentialPlugins": "Install Essential Plugins",
"activation.pluginsStep.installFailed": "Failed to install plugins. Please try again.",
"activation.pluginsStep.installingPluginMessage": "Installing {name}...",