This commit is contained in:
Philipinho
2026-03-09 00:51:14 +00:00
parent 78c3839ae7
commit ff01355ec3
19 changed files with 115 additions and 50 deletions
@@ -623,7 +623,7 @@
"Upgrade your plan": "Upgrade your plan",
"Available with a paid license": "Available with a paid license",
"Upgrade your license tier.": "Upgrade your license tier.",
"AI features require a paid plan. Visit docmost.com for more information.": "AI features require a paid plan. Visit docmost.com for more information.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"AI & MCP": "AI & MCP",
"AI": "AI",
"MCP": "MCP",
@@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
import { isCloud } from "@/lib/config.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import {
@@ -136,7 +136,7 @@ export default function SettingsSidebar() {
const [active, setActive] = useState(location.pathname);
const { goBack } = useSettingsNavigation();
const { isAdmin, isOwner } = useUserRole();
const [workspace] = useAtom(workspaceAtom);
const [entitlements] = useAtom(entitlementAtom);
const upgradeLabel = useUpgradeLabel();
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
@@ -146,7 +146,7 @@ export default function SettingsSidebar() {
}, [location.pathname]);
const hasFeature = (f: string) =>
workspace?.features?.includes(f) ?? false;
entitlements?.features?.includes(f) ?? false;
const canShowItem = (item: DataItem) => {
if (item.env === "cloud" && !isCloud()) return false;
@@ -191,7 +191,7 @@ export default function SettingsSidebar() {
prefetchHandler = prefetchBilling;
break;
case "License & Edition":
if (workspace?.hasLicenseKey) {
if (entitlements?.tier !== "free") {
prefetchHandler = prefetchLicense;
}
break;
+1 -1
View File
@@ -63,7 +63,7 @@ export default function AiSettings() {
mb="lg"
>
{t(
"AI features require a paid plan. Visit docmost.com for more information.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
)}
</Alert>
)}
+1 -1
View File
@@ -56,7 +56,7 @@ export default function SsoLogin() {
/>
)}
{(data.features?.length > 0) && (
{data.authProviders.length > 0 && (
<>
<Stack align="stretch" justify="center" gap="sm">
{data.authProviders.map((provider) => (
@@ -0,0 +1,5 @@
import { atom } from "jotai";
import type { Entitlements } from "./entitlement.types";
const initialValue: Entitlements | null = null;
export const entitlementAtom = atom(initialValue);
@@ -0,0 +1,7 @@
import api from "@/lib/api-client";
import { Entitlements } from "./entitlement.types";
export async function getEntitlements(): Promise<Entitlements> {
const req = await api.post<Entitlements>("/workspace/entitlements");
return req.data as Entitlements;
}
@@ -0,0 +1,7 @@
export type Tier = "free" | "standard" | "business" | "enterprise";
export type Entitlements = {
cloud: boolean;
tier: Tier;
features: string[];
};
@@ -0,0 +1,11 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { getEntitlements } from "./entitlement-service";
import { Entitlements } from "./entitlement.types";
export function useEntitlements(): UseQueryResult<Entitlements> {
return useQuery({
queryKey: ["entitlements"],
queryFn: getEntitlements,
staleTime: 5 * 60 * 1000,
});
}
+3 -8
View File
@@ -1,12 +1,7 @@
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
export const useHasFeature = (feature: string): boolean => {
const [workspace] = useAtom(workspaceAtom);
return workspace?.features?.includes(feature) ?? false;
};
export const useHasAnyFeature = (): boolean => {
const [workspace] = useAtom(workspaceAtom);
return (workspace?.features?.length ?? 0) > 0;
const [entitlements] = useAtom(entitlementAtom);
return entitlements?.features?.includes(feature) ?? false;
};
@@ -1,14 +1,14 @@
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import { isCloud } from "@/lib/config";
export function useUpgradeLabel(): string {
const { t } = useTranslation();
const [workspace] = useAtom(workspaceAtom);
const [entitlements] = useAtom(entitlementAtom);
if (!isCloud()) {
return workspace?.hasLicenseKey
return entitlements != null && entitlements.tier !== "free"
? t("Upgrade your license tier.")
: t("Available with a paid license");
}
@@ -7,21 +7,22 @@ import { useTranslation } from "react-i18next";
import { useActivateMutation } from "@/ee/licence/queries/license-query.ts";
import { useDisclosure } from "@mantine/hooks";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import RemoveLicense from "@/ee/licence/components/remove-license.tsx";
export default function ActivateLicense() {
const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false);
const [workspace] = useAtom(workspaceAtom);
const [entitlements] = useAtom(entitlementAtom);
const hasLicense = entitlements != null && entitlements.tier !== "free";
return (
<Group justify="flex-end" wrap="nowrap" mb="sm">
<Button onClick={open}>
{workspace?.hasLicenseKey ? t("Update license") : t("Add license")}
{hasLicense ? t("Update license") : t("Add license")}
</Button>
{workspace?.hasLicenseKey && <RemoveLicense />}
{hasLicense && <RemoveLicense />}
<Modal
size="550"
@@ -59,7 +60,7 @@ export function ActivateLicenseForm({ onClose }: ActivateLicenseFormProps) {
async function handleSubmit(data: { licenseKey: string }) {
await activateLicenseMutation.mutateAsync(data.licenseKey);
form.reset();
onClose();
onClose?.();
}
return (
+4 -3
View File
@@ -8,10 +8,11 @@ import ActivateLicenseForm from "@/ee/licence/components/activate-license-modal.
import InstallationDetails from "@/ee/licence/components/installation-details.tsx";
import OssDetails from "@/ee/licence/components/oss-details.tsx";
import { useAtom } from "jotai/index";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
export default function License() {
const [workspace] = useAtom(workspaceAtom);
const [entitlements] = useAtom(entitlementAtom);
const hasLicense = entitlements != null && entitlements.tier !== "free";
const { isAdmin } = useUserRole();
if (!isAdmin) {
@@ -29,7 +30,7 @@ export default function License() {
<InstallationDetails />
{workspace?.hasLicenseKey ? <LicenseDetails /> : <OssDetails />}
{hasLicense ? <LicenseDetails /> : <OssDetails />}
</>
);
}
@@ -31,6 +31,7 @@ export function useActivateMutation() {
queryKey: ["license"],
});
queryClient.refetchQueries({ queryKey: ["currentUser"] });
queryClient.refetchQueries({ queryKey: ["entitlements"] });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
@@ -47,6 +48,7 @@ export function useRemoveLicenseMutation() {
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["license"] });
queryClient.refetchQueries({ queryKey: ["currentUser"] });
queryClient.refetchQueries({ queryKey: ["entitlements"] });
},
});
}
@@ -1,4 +1,4 @@
import { useAtom } from "jotai";
import { useAtom, useSetAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import React, { useEffect } from "react";
import useCurrentUser from "@/features/user/hooks/use-current-user";
@@ -11,10 +11,14 @@ import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts";
import { useNotificationSocket } from "@/features/notification/hooks/use-notification-socket.ts";
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
import { Error404 } from "@/components/ui/error-404.tsx";
import { useEntitlements } from "@/ee/entitlement/use-entitlements";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
export function UserProvider({ children }: React.PropsWithChildren) {
const [, setCurrentUser] = useAtom(currentUserAtom);
const setEntitlements = useSetAtom(entitlementAtom);
const { data, isLoading, error, isError } = useCurrentUser();
const { data: entitlements } = useEntitlements();
const { i18n } = useTranslation();
const [, setSocket] = useAtom(socketAtom);
// fetch collab token on load
@@ -56,6 +60,12 @@ export function UserProvider({ children }: React.PropsWithChildren) {
}
}, [data, isLoading]);
useEffect(() => {
if (entitlements) {
setEntitlements(entitlements);
}
}, [entitlements]);
if (isLoading) return <></>;
if (isError && error?.["response"]?.status === 404) {
@@ -20,8 +20,6 @@ export interface IWorkspace {
emailDomains: string[];
memberCount?: number;
plan?: string;
hasLicenseKey?: boolean;
features?: string[];
enforceMfa?: boolean;
aiSearch?: boolean;
generativeAi?: boolean;
@@ -85,7 +83,6 @@ export interface IPublicWorkspace {
hostname: string;
enforceSso: boolean;
authProviders: IAuthProvider[];
features?: string[];
}
export interface IVersion {