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
|
/* Keep the GitHub Star and Help buttons at matching heights regardless of
|
||||||
their differing icon sizes (provider icon 16px vs. HelpCircle 13px). */
|
their differing icon sizes (provider icon 16px vs. HelpCircle 13px). */
|
||||||
.settings-header-actions > .settings-github-star-btn,
|
.settings-header-actions > .settings-github-star-btn,
|
||||||
.settings-header-actions > .btn,
|
.settings-header-actions > .btn {
|
||||||
.settings-header-actions > .settings-update-btn {
|
|
||||||
height: 26px;
|
height: 26px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -23,8 +22,7 @@
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Keep settings header actions compact; override global mobile
|
/* Keep settings header actions compact; override global mobile
|
||||||
`.btn-icon { min-height/min-width: 36px; }` inflation. */
|
`.btn-icon { min-height/min-width: 36px; }` inflation. */
|
||||||
.settings-header-actions > .btn-icon,
|
.settings-header-actions > .btn-icon {
|
||||||
.settings-header-actions > .settings-update-btn {
|
|
||||||
min-height: 26px;
|
min-height: 26px;
|
||||||
min-width: 26px;
|
min-width: 26px;
|
||||||
}
|
}
|
||||||
@@ -137,11 +135,42 @@
|
|||||||
gap: var(--space-sm);
|
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);
|
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;
|
animation: settings-update-spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1897,42 +1897,11 @@ export function SettingsModal({
|
|||||||
<option value="weekly">Weekly</option>
|
<option value="weekly">Weekly</option>
|
||||||
</select>
|
</select>
|
||||||
<small>
|
<small>
|
||||||
Controls how often the dashboard re-fetches the npm registry. The
|
Controls how often the dashboard re-fetches the npm registry.
|
||||||
"Check Now" button below always triggers an immediate fetch
|
Use the version + refresh control in the header to trigger an
|
||||||
regardless of this setting.
|
immediate check at any time.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</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">
|
<div className="settings-modal-heading">
|
||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
<div className="settings-update-check">
|
<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 && (
|
{updateCheckResult && (
|
||||||
<span
|
<span
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
@@ -4693,18 +4676,6 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-header-actions">
|
<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 && (
|
{form.showGitHubStarButton !== false && (
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Runfusion/Fusion"
|
href="https://github.com/Runfusion/Fusion"
|
||||||
|
|||||||
@@ -515,6 +515,8 @@ describe("SettingsModal", () => {
|
|||||||
await waitForSettingsModalReady();
|
await waitForSettingsModalReady();
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument();
|
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 () => {
|
it("clicking check for updates shows up-to-date message", async () => {
|
||||||
@@ -576,6 +578,55 @@ describe("SettingsModal", () => {
|
|||||||
expect(button).not.toBeDisabled();
|
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", () => {
|
describe("settings export filename", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user