feat: add version check (#922)

* Add version endpoint

* version indicator

* refetch

* * Translate strings
* Handle error
This commit is contained in:
Philip Okugbe 2025-03-22 15:29:10 +00:00 committed by GitHub
parent c824b5b570
commit f8ce160906
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 156 additions and 17 deletions

View File

@ -47,6 +47,7 @@
"react-helmet-async": "^2.0.5",
"react-i18next": "^15.0.1",
"react-router-dom": "^7.0.1",
"semver": "^7.7.1",
"socket.io-client": "^4.8.1",
"tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.18",

View File

@ -351,5 +351,7 @@
"Created at: {{time}}": "Created at: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
"Character count: {{characterCount}}": "Character count: {{characterCount}}",
"New update": "New update",
"{{latestVersion}} is available": "{{latestVersion}} is available"
}

View File

@ -0,0 +1,53 @@
import { useAppVersion } from "@/features/workspace/queries/workspace-query.ts";
import { isCloud } from "@/lib/config.ts";
import classes from "@/components/settings/settings.module.css";
import { Indicator, Text, Tooltip } from "@mantine/core";
import React from "react";
import semverGt from "semver/functions/gt";
import { useTranslation } from "react-i18next";
export default function AppVersion() {
const { t } = useTranslation();
const { data: appVersion } = useAppVersion(!isCloud());
let hasUpdate = false;
try {
hasUpdate =
appVersion &&
parseFloat(appVersion.latestVersion) > 0 &&
semverGt(appVersion.latestVersion, appVersion.currentVersion);
} catch (err) {
console.error(err);
}
return (
<div className={classes.text}>
<Tooltip
label={t("{{latestVersion}} is available", {
latestVersion: `v${appVersion?.latestVersion}`,
})}
disabled={!hasUpdate}
>
<Indicator
label={t("New update")}
color="gray"
inline
size={16}
position="middle-end"
style={{ cursor: "pointer" }}
disabled={!hasUpdate}
>
<Text
size="sm"
c="dimmed"
component="a"
mr={45}
href="https://github.com/docmost/docmost/releases"
target="_blank"
>
v{APP_VERSION}
</Text>
</Indicator>
</Tooltip>
</div>
);
}

View File

@ -27,6 +27,7 @@ import {
prefetchSsoProviders,
prefetchWorkspaceMembers,
} from "@/components/settings/settings-queries.tsx";
import AppVersion from "@/components/settings/app-version.tsx";
interface DataItem {
label: string;
@ -205,19 +206,8 @@ export default function SettingsSidebar() {
</Group>
<ScrollArea w="100%">{menuItems}</ScrollArea>
{!isCloud() && (
<div className={classes.text}>
<Text
size="sm"
c="dimmed"
component="a"
href="https://github.com/docmost/docmost/releases"
target="_blank"
>
v{APP_VERSION}
</Text>
</div>
)}
{!isCloud() && <AppVersion />}
{isCloud() && (
<div className={classes.text}>

View File

@ -15,6 +15,7 @@ import {
revokeInvitation,
getWorkspace,
getWorkspacePublicData,
getAppVersion,
} from "@/features/workspace/services/workspace-service";
import { IPagination, QueryParams } from "@/lib/types.ts";
import { notifications } from "@mantine/notifications";
@ -22,6 +23,7 @@ import {
ICreateInvite,
IInvitation,
IPublicWorkspace,
IVersion,
IWorkspace,
} from "@/features/workspace/types/workspace.types.ts";
import { IUser } from "@/features/user/types/user.types.ts";
@ -153,3 +155,15 @@ export function useGetInvitationQuery(
enabled: !!invitationId,
});
}
export function useAppVersion(
isEnabled: boolean,
): UseQueryResult<IVersion, Error> {
return useQuery({
queryKey: ["version"],
queryFn: () => getAppVersion(),
staleTime: 60 * 60 * 1000, // 1 hr
enabled: isEnabled,
refetchOnMount: true,
});
}

View File

@ -7,6 +7,7 @@ import {
IAcceptInvite,
IPublicWorkspace,
IInvitationLink,
IVersion,
} from "../types/workspace.types";
import { IPagination, QueryParams } from "@/lib/types.ts";
import { ISetupWorkspace } from "@/features/auth/types/auth.types.ts";
@ -73,7 +74,6 @@ export async function getInviteLink(data: {
export async function resendInvitation(data: {
invitationId: string;
}): Promise<void> {
console.log(data);
await api.post("/workspace/invites/resend", data);
}
@ -97,6 +97,11 @@ export async function createWorkspace(
return req.data;
}
export async function getAppVersion(): Promise<IVersion> {
const req = await api.post("/version");
return req.data;
}
export async function uploadLogo(file: File) {
const formData = new FormData();
formData.append("type", "workspace-logo");

View File

@ -57,3 +57,9 @@ export interface IPublicWorkspace {
authProviders: IAuthProvider[];
hasLicenseKey?: boolean;
}
export interface IVersion {
currentVersion: string;
latestVersion: string;
releaseUrl: string;
}

View File

@ -1,7 +1,10 @@
import { Module } from '@nestjs/common';
import { RobotsTxtController } from './robots.txt.controller';
import { VersionController } from './version.controller';
import { VersionService } from './version.service';
@Module({
controllers: [RobotsTxtController],
controllers: [RobotsTxtController, VersionController],
providers: [VersionService],
})
export class SecurityModule {}

View File

@ -0,0 +1,27 @@
import {
Controller,
HttpCode,
HttpStatus,
NotFoundException,
Post,
UseGuards,
} from '@nestjs/common';
import { VersionService } from './version.service';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { EnvironmentService } from '../environment/environment.service';
@UseGuards(JwtAuthGuard)
@Controller('version')
export class VersionController {
constructor(
private readonly versionService: VersionService,
private readonly environmentService: EnvironmentService,
) {}
@HttpCode(HttpStatus.OK)
@Post()
async getVersion() {
if (this.environmentService.isCloud()) throw new NotFoundException();
return this.versionService.getVersion();
}
}

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const packageJson = require('./../../../package.json');
@Injectable()
export class VersionService {
constructor() {}
async getVersion() {
const url = `https://api.github.com/repos/docmost/docmost/releases/latest`;
let latestVersion = 0;
try {
const response = await fetch(url);
if (!response.ok) return;
const data = await response.json();
latestVersion = data?.tag_name?.replace('v', '');
} catch (err) {
/* empty */
}
return {
currentVersion: packageJson?.version,
latestVersion: latestVersion,
releaseUrl: 'https://github.com/docmost/docmost/releases',
};
}
}

12
pnpm-lock.yaml generated
View File

@ -311,6 +311,9 @@ importers:
react-router-dom:
specifier: ^7.0.1
version: 7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
semver:
specifier: ^7.7.1
version: 7.7.1
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
@ -8056,6 +8059,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.1:
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'}
hasBin: true
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@ -13563,7 +13571,7 @@ snapshots:
fast-glob: 3.3.2
is-glob: 4.0.3
minimatch: 9.0.4
semver: 7.6.3
semver: 7.7.1
ts-api-utils: 1.3.0(typescript@5.7.2)
optionalDependencies:
typescript: 5.7.2
@ -18024,6 +18032,8 @@ snapshots:
semver@7.6.3: {}
semver@7.7.1: {}
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0