feat(FN-2974): style inline version update control and improve touch target
The merge adds an inline version refresh control to the Settings modal with improved touch targets for better mobile usability, replacing a separate duplicate-updates check button. Changes span the SettingsModal component, its CSS styling, and new test coverage for the control behavior. Fusion-Task-Id: FN-2974
This commit is contained in:
@@ -14,8 +14,7 @@
|
||||
/* Keep the GitHub Star and Help buttons at matching heights regardless of
|
||||
their differing icon sizes (provider icon 16px vs. HelpCircle 13px). */
|
||||
.settings-header-actions > .settings-github-star-btn,
|
||||
.settings-header-actions > .btn,
|
||||
.settings-header-actions > .settings-update-btn {
|
||||
.settings-header-actions > .btn {
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -23,8 +22,7 @@
|
||||
@media (max-width: 768px) {
|
||||
/* Keep settings header actions compact; override global mobile
|
||||
`.btn-icon { min-height/min-width: 36px; }` inflation. */
|
||||
.settings-header-actions > .btn-icon,
|
||||
.settings-header-actions > .settings-update-btn {
|
||||
.settings-header-actions > .btn-icon {
|
||||
min-height: 26px;
|
||||
min-width: 26px;
|
||||
}
|
||||
@@ -137,11 +135,42 @@
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.settings-update-btn {
|
||||
.settings-version-check-btn {
|
||||
--settings-inline-touch-target: calc(var(--space-lg) + var(--space-lg) + var(--space-xs));
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin: 0;
|
||||
min-height: var(--settings-inline-touch-target);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.settings-version-check-btn:hover:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.settings-version-check-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.settings-update-btn svg.spinning {
|
||||
.settings-version-check-btn:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.settings-version-check-btn svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.settings-version-check-btn svg.spinning {
|
||||
animation: settings-update-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -1897,42 +1897,11 @@ export function SettingsModal({
|
||||
<option value="weekly">Weekly</option>
|
||||
</select>
|
||||
<small>
|
||||
Controls how often the dashboard re-fetches the npm registry. The
|
||||
"Check Now" button below always triggers an immediate fetch
|
||||
regardless of this setting.
|
||||
Controls how often the dashboard re-fetches the npm registry.
|
||||
Use the version + refresh control in the header to trigger an
|
||||
immediate check at any time.
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Check Now</label>
|
||||
<div className="settings-update-check">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm settings-update-btn"
|
||||
onClick={() => {
|
||||
void handleCheckForUpdates();
|
||||
}}
|
||||
disabled={updateCheckLoading}
|
||||
>
|
||||
<RefreshCw className={updateCheckLoading ? "spinning" : undefined} size={14} />
|
||||
{updateCheckLoading ? "Checking…" : "Check for updates"}
|
||||
</button>
|
||||
{updateCheckResult && (
|
||||
<span
|
||||
aria-live="polite"
|
||||
className={`settings-update-result ${
|
||||
updateCheckResult.error
|
||||
? "settings-update-result--error"
|
||||
: updateCheckResult.updateAvailable
|
||||
? "settings-update-result--available"
|
||||
: "settings-update-result--up-to-date"
|
||||
}`}
|
||||
>
|
||||
{renderUpdateCheckResultContent()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<small>Manually check for the latest version right now.</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4675,7 +4644,21 @@ export function SettingsModal({
|
||||
<div className="settings-modal-heading">
|
||||
<h3>Settings</h3>
|
||||
<div className="settings-update-check">
|
||||
{appVersion && <p className="settings-modal-version">Version {appVersion}</p>}
|
||||
{appVersion && (
|
||||
<button
|
||||
type="button"
|
||||
className="settings-version-check-btn"
|
||||
onClick={() => {
|
||||
void handleCheckForUpdates();
|
||||
}}
|
||||
disabled={updateCheckLoading}
|
||||
aria-label="Check for updates"
|
||||
title="Check for updates"
|
||||
>
|
||||
<span className="settings-modal-version">Version {appVersion}</span>
|
||||
<RefreshCw size={12} className={updateCheckLoading ? "spinning" : undefined} />
|
||||
</button>
|
||||
)}
|
||||
{updateCheckResult && (
|
||||
<span
|
||||
aria-live="polite"
|
||||
@@ -4693,18 +4676,6 @@ export function SettingsModal({
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-header-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm settings-update-btn"
|
||||
onClick={() => {
|
||||
void handleCheckForUpdates();
|
||||
}}
|
||||
disabled={updateCheckLoading}
|
||||
aria-label="Check for updates"
|
||||
title="Check for updates"
|
||||
>
|
||||
<RefreshCw size={14} className={updateCheckLoading ? "spinning" : undefined} />
|
||||
</button>
|
||||
{form.showGitHubStarButton !== false && (
|
||||
<a
|
||||
href="https://github.com/Runfusion/Fusion"
|
||||
|
||||
@@ -515,6 +515,8 @@ describe("SettingsModal", () => {
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Check Now" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Manually check for the latest version right now.")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking check for updates shows up-to-date message", async () => {
|
||||
@@ -576,6 +578,55 @@ describe("SettingsModal", () => {
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking version text area triggers update check", async () => {
|
||||
mockCheckForUpdates.mockResolvedValueOnce({
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion: "1.2.3",
|
||||
updateAvailable: false,
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const inlineButton = screen.getByRole("button", { name: "Check for updates" });
|
||||
expect(within(inlineButton).getByText("Version 1.2.3")).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(within(inlineButton).getByText("Version 1.2.3"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("refresh icon has spinning class while loading", async () => {
|
||||
let resolveCheck: ((result: UpdateCheckResponse) => void) | undefined;
|
||||
const pendingCheck = new Promise<UpdateCheckResponse>((resolve) => {
|
||||
resolveCheck = resolve;
|
||||
});
|
||||
mockCheckForUpdates.mockReturnValueOnce(pendingCheck);
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const button = screen.getByRole("button", { name: "Check for updates" });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const spinningIcon = button.querySelector(".spinning");
|
||||
expect(spinningIcon).not.toBeNull();
|
||||
});
|
||||
|
||||
resolveCheck?.({
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion: "1.2.3",
|
||||
updateAvailable: false,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(button.querySelector(".spinning")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings export filename", () => {
|
||||
|
||||
Reference in New Issue
Block a user