Show WebUI version and update action in the Web Applications list

This commit is contained in:
Steven RYDELL
2026-07-29 06:48:41 +02:00
parent 8fde771ff2
commit bd16e64e51
5 changed files with 274 additions and 13 deletions
+47
View File
@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import { describe, expect, it } from 'vitest';
import { compareVersions, isUpdateAvailable, normalizeVersion } from './versionCompare';
describe('normalizeVersion', () => {
it('strips a leading v prefix and whitespace', () => {
expect(normalizeVersion('v1.2.3')).toBe('1.2.3');
expect(normalizeVersion('V2.0')).toBe('2.0');
expect(normalizeVersion(' 1.0.6 ')).toBe('1.0.6');
});
});
describe('compareVersions', () => {
it('orders dotted numeric versions', () => {
expect(compareVersions('v1.0.7', '1.0.6')).toBeGreaterThan(0);
expect(compareVersions('1.0.6', 'v1.0.7')).toBeLessThan(0);
expect(compareVersions('1.2.0', '1.2')).toBe(0);
});
it('returns null for unparsable versions', () => {
expect(compareVersions('nightly', '1.0.6')).toBeNull();
});
});
describe('isUpdateAvailable', () => {
it('is false when latest equals current', () => {
expect(isUpdateAvailable('v1.0.6', '1.0.6')).toBe(false);
});
it('is true when latest is newer', () => {
expect(isUpdateAvailable('v1.1.0', '1.0.6')).toBe(true);
});
it('is false when latest is older', () => {
expect(isUpdateAvailable('v1.0.5', '1.0.6')).toBe(false);
});
it('falls back to string difference for unparsable tags', () => {
expect(isUpdateAvailable('nightly-2', '1.0.6')).toBe(true);
expect(isUpdateAvailable('v1.0.6', '1.0.6-beta')).toBe(true);
});
});
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
export function normalizeVersion(version: string): string {
return version.trim().replace(/^v/i, '');
}
// Compares two dotted numeric versions; returns null when either is unparsable.
export function compareVersions(a: string, b: string): number | null {
const pa = normalizeVersion(a).split('.').map(Number);
const pb = normalizeVersion(b).split('.').map(Number);
if (pa.some(Number.isNaN) || pb.some(Number.isNaN)) return null;
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
if (diff !== 0) return diff;
}
return 0;
}
// Falls back to a string difference when the versions are not dotted numerics,
// so an unexpected tag format still surfaces as an available update.
export function isUpdateAvailable(latestVersion: string, currentVersion: string): boolean {
const cmp = compareVersions(latestVersion, currentVersion);
if (cmp === null) return normalizeVersion(latestVersion) !== normalizeVersion(currentVersion);
return cmp > 0;
}