mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: add version check (#922)
* Add version endpoint * version indicator * refetch * * Translate strings * Handle error
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
|
"semver": "^7.7.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap-extension-global-drag-handle": "^0.1.18",
|
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||||
|
|||||||
@@ -351,5 +351,7 @@
|
|||||||
"Created at: {{time}}": "Created at: {{time}}",
|
"Created at: {{time}}": "Created at: {{time}}",
|
||||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
prefetchSsoProviders,
|
prefetchSsoProviders,
|
||||||
prefetchWorkspaceMembers,
|
prefetchWorkspaceMembers,
|
||||||
} from "@/components/settings/settings-queries.tsx";
|
} from "@/components/settings/settings-queries.tsx";
|
||||||
|
import AppVersion from "@/components/settings/app-version.tsx";
|
||||||
|
|
||||||
interface DataItem {
|
interface DataItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -205,19 +206,8 @@ export default function SettingsSidebar() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||||
{!isCloud() && (
|
|
||||||
<div className={classes.text}>
|
{!isCloud() && <AppVersion />}
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
c="dimmed"
|
|
||||||
component="a"
|
|
||||||
href="https://github.com/docmost/docmost/releases"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
v{APP_VERSION}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCloud() && (
|
{isCloud() && (
|
||||||
<div className={classes.text}>
|
<div className={classes.text}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
revokeInvitation,
|
revokeInvitation,
|
||||||
getWorkspace,
|
getWorkspace,
|
||||||
getWorkspacePublicData,
|
getWorkspacePublicData,
|
||||||
|
getAppVersion,
|
||||||
} from "@/features/workspace/services/workspace-service";
|
} from "@/features/workspace/services/workspace-service";
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
ICreateInvite,
|
ICreateInvite,
|
||||||
IInvitation,
|
IInvitation,
|
||||||
IPublicWorkspace,
|
IPublicWorkspace,
|
||||||
|
IVersion,
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
} from "@/features/workspace/types/workspace.types.ts";
|
} from "@/features/workspace/types/workspace.types.ts";
|
||||||
import { IUser } from "@/features/user/types/user.types.ts";
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
@@ -153,3 +155,15 @@ export function useGetInvitationQuery(
|
|||||||
enabled: !!invitationId,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
IAcceptInvite,
|
IAcceptInvite,
|
||||||
IPublicWorkspace,
|
IPublicWorkspace,
|
||||||
IInvitationLink,
|
IInvitationLink,
|
||||||
|
IVersion,
|
||||||
} from "../types/workspace.types";
|
} from "../types/workspace.types";
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
import { ISetupWorkspace } from "@/features/auth/types/auth.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: {
|
export async function resendInvitation(data: {
|
||||||
invitationId: string;
|
invitationId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
console.log(data);
|
|
||||||
await api.post("/workspace/invites/resend", data);
|
await api.post("/workspace/invites/resend", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +97,11 @@ export async function createWorkspace(
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAppVersion(): Promise<IVersion> {
|
||||||
|
const req = await api.post("/version");
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadLogo(file: File) {
|
export async function uploadLogo(file: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("type", "workspace-logo");
|
formData.append("type", "workspace-logo");
|
||||||
|
|||||||
@@ -57,3 +57,9 @@ export interface IPublicWorkspace {
|
|||||||
authProviders: IAuthProvider[];
|
authProviders: IAuthProvider[];
|
||||||
hasLicenseKey?: boolean;
|
hasLicenseKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVersion {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
releaseUrl: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { RobotsTxtController } from './robots.txt.controller';
|
import { RobotsTxtController } from './robots.txt.controller';
|
||||||
|
import { VersionController } from './version.controller';
|
||||||
|
import { VersionService } from './version.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [RobotsTxtController],
|
controllers: [RobotsTxtController, VersionController],
|
||||||
|
providers: [VersionService],
|
||||||
})
|
})
|
||||||
export class SecurityModule {}
|
export class SecurityModule {}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+11
-1
@@ -311,6 +311,9 @@ importers:
|
|||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.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:
|
socket.io-client:
|
||||||
specifier: ^4.8.1
|
specifier: ^4.8.1
|
||||||
version: 4.8.1
|
version: 4.8.1
|
||||||
@@ -8056,6 +8059,11 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.7.1:
|
||||||
|
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
serialize-javascript@6.0.2:
|
serialize-javascript@6.0.2:
|
||||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||||
|
|
||||||
@@ -13563,7 +13571,7 @@ snapshots:
|
|||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.4
|
minimatch: 9.0.4
|
||||||
semver: 7.6.3
|
semver: 7.7.1
|
||||||
ts-api-utils: 1.3.0(typescript@5.7.2)
|
ts-api-utils: 1.3.0(typescript@5.7.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
@@ -18024,6 +18032,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.6.3: {}
|
semver@7.6.3: {}
|
||||||
|
|
||||||
|
semver@7.7.1: {}
|
||||||
|
|
||||||
serialize-javascript@6.0.2:
|
serialize-javascript@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user