Skip to content

Commit a2d9d7a

Browse files
committed
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.
1 parent a4b4757 commit a2d9d7a

File tree

3 files changed

+76
-11
lines changed

3 files changed

+76
-11
lines changed

web/__test__/components/Activation/ActivationPluginsStep.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ describe('ActivationPluginsStep', () => {
6565

6666
const { wrapper, props } = mountComponent();
6767

68+
const checkboxes = wrapper.findAll('input[type="checkbox"]');
69+
for (const checkbox of checkboxes) {
70+
const input = checkbox.element as HTMLInputElement;
71+
input.checked = true;
72+
await checkbox.trigger('change');
73+
}
74+
await flushPromises();
75+
6876
const installButton = wrapper
6977
.findAll('[data-testid="brand-button"]')
7078
.find((button) => button.text().includes('Install'));
@@ -76,15 +84,33 @@ describe('ActivationPluginsStep', () => {
7684
const firstCallArgs = installPluginMock.mock.calls[0]?.[0];
7785
expect(firstCallArgs?.forced).toBe(true);
7886
expect(firstCallArgs?.url).toContain('community.applications');
79-
expect(props.onComplete).toHaveBeenCalled();
87+
expect(props.onComplete).not.toHaveBeenCalled();
8088
expect(wrapper.html()).toContain('installation started');
8189
expect(wrapper.html()).toContain('installed successfully');
90+
91+
const continueButton = wrapper
92+
.findAll('[data-testid="brand-button"]')
93+
.find((button) => button.text().includes('Continue'));
94+
expect(continueButton).toBeTruthy();
95+
const callsBeforeContinue = props.onComplete.mock.calls.length;
96+
await continueButton!.trigger('click');
97+
98+
expect(props.onComplete.mock.calls.length).toBeGreaterThanOrEqual(callsBeforeContinue + 1);
8299
});
83100

84101
it('shows error message when installation fails', async () => {
85102
installPluginMock.mockRejectedValueOnce(new Error('install failed'));
86103

87104
const { wrapper, props } = mountComponent();
105+
106+
const errorCheckboxes = wrapper.findAll('input[type="checkbox"]');
107+
for (const checkbox of errorCheckboxes) {
108+
const input = checkbox.element as HTMLInputElement;
109+
input.checked = true;
110+
await checkbox.trigger('change');
111+
}
112+
await flushPromises();
113+
88114
const installButton = wrapper
89115
.findAll('[data-testid="brand-button"]')
90116
.find((button) => button.text().includes('Install'));

web/src/components/Activation/ActivationPluginsStep.vue

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { ref } from 'vue';
2+
import { computed, ref } from 'vue';
33
import { useI18n } from 'vue-i18n';
44
55
import { BrandButton } from '@unraid/ui';
@@ -46,10 +46,11 @@ const availablePlugins: Plugin[] = [
4646
},
4747
];
4848
49-
const selectedPlugins = ref<Set<string>>(new Set(availablePlugins.map((p) => p.id)));
49+
const selectedPlugins = ref<Set<string>>(new Set());
5050
const isInstalling = ref(false);
5151
const error = ref<string | null>(null);
5252
const installationLogs = ref<string[]>([]);
53+
const installationFinished = ref(false);
5354
5455
const { installPlugin } = usePluginInstaller();
5556
@@ -61,6 +62,10 @@ const appendLogs = (lines: string[] | string) => {
6162
}
6263
};
6364
65+
const resetCompletionState = () => {
66+
installationFinished.value = false;
67+
};
68+
6469
const togglePlugin = (pluginId: string) => {
6570
const next = new Set(selectedPlugins.value);
6671
if (next.has(pluginId)) {
@@ -69,17 +74,20 @@ const togglePlugin = (pluginId: string) => {
6974
next.add(pluginId);
7075
}
7176
selectedPlugins.value = next;
77+
resetCompletionState();
7278
};
7379
7480
const handleInstall = async () => {
7581
if (selectedPlugins.value.size === 0) {
82+
installationFinished.value = true;
7683
props.onComplete();
7784
return;
7885
}
7986
8087
isInstalling.value = true;
8188
error.value = null;
8289
installationLogs.value = [];
90+
installationFinished.value = false;
8391
8492
try {
8593
const pluginsToInstall = availablePlugins.filter((p) => selectedPlugins.value.has(p.id));
@@ -105,10 +113,11 @@ const handleInstall = async () => {
105113
appendLogs(t('activation.pluginsStep.pluginInstalledMessage', { name: plugin.name }));
106114
}
107115
108-
props.onComplete();
116+
installationFinished.value = true;
109117
} catch (err) {
110118
error.value = t('activation.pluginsStep.installFailed');
111119
console.error('Failed to install plugins:', err);
120+
installationFinished.value = false;
112121
} finally {
113122
isInstalling.value = false;
114123
}
@@ -121,6 +130,39 @@ const handleSkip = () => {
121130
const handleBack = () => {
122131
props.onBack?.();
123132
};
133+
134+
const handlePrimaryAction = async () => {
135+
if (installationFinished.value || selectedPlugins.value.size === 0) {
136+
props.onComplete();
137+
return;
138+
}
139+
140+
if (!isInstalling.value) {
141+
await handleInstall();
142+
}
143+
};
144+
145+
const primaryButtonText = computed(() => {
146+
if (installationFinished.value) {
147+
return t('common.continue');
148+
}
149+
if (selectedPlugins.value.size > 0) {
150+
return t('activation.pluginsStep.installSelected');
151+
}
152+
return t('common.continue');
153+
});
154+
155+
const isPrimaryActionDisabled = computed(() => {
156+
if (isInstalling.value) {
157+
return true;
158+
}
159+
160+
if (installationFinished.value) {
161+
return false;
162+
}
163+
164+
return selectedPlugins.value.size === 0;
165+
});
124166
</script>
125167

126168
<template>
@@ -184,14 +226,10 @@ const handleBack = () => {
184226
@click="handleSkip"
185227
/>
186228
<BrandButton
187-
:text="
188-
selectedPlugins.size > 0
189-
? t('activation.pluginsStep.installAndContinue')
190-
: t('common.continue')
191-
"
192-
:disabled="isInstalling"
229+
:text="primaryButtonText"
230+
:disabled="isPrimaryActionDisabled"
193231
:loading="isInstalling"
194-
@click="handleInstall"
232+
@click="handlePrimaryAction"
195233
/>
196234
</div>
197235
</div>

web/src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"activation.activationSteps.unleashYourHardware": "Unleash Your Hardware",
1515
"activation.pluginsStep.addHelpfulPlugins": "Add helpful plugins",
1616
"activation.pluginsStep.installAndContinue": "Install & Continue",
17+
"activation.pluginsStep.installSelected": "Install Selected",
1718
"activation.pluginsStep.installEssentialPlugins": "Install Essential Plugins",
1819
"activation.pluginsStep.installFailed": "Failed to install plugins. Please try again.",
1920
"activation.pluginsStep.installingPluginMessage": "Installing {name}...",

0 commit comments

Comments
 (0)