diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index efad41cc..3efcdfec 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -495,5 +495,10 @@ "Page restored successfully": "Page restored successfully", "Deleted by": "Deleted by", "Deleted at": "Deleted at", - "Preview": "Preview" + "Preview": "Preview", + "Subpages": "Subpages", + "Failed to load subpages": "Failed to load subpages", + "No subpages": "No subpages", + "Subpages (Child pages)": "Subpages (Child pages)", + "List all subpages of the current page": "List all subpages of the current page" } diff --git a/apps/client/src/components/layouts/global/app-header.tsx b/apps/client/src/components/layouts/global/app-header.tsx index 09b95539..eb1ca74f 100644 --- a/apps/client/src/components/layouts/global/app-header.tsx +++ b/apps/client/src/components/layouts/global/app-header.tsx @@ -14,6 +14,14 @@ import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx"; import { useTranslation } from "react-i18next"; import useTrial from "@/ee/hooks/use-trial.tsx"; import { isCloud } from "@/lib/config.ts"; +import { + SearchControl, + SearchMobileControl, +} from "@/features/search/components/search-control.tsx"; +import { + searchSpotlight, + shareSearchSpotlight, +} from "@/features/search/constants.ts"; const links = [{ link: APP_ROUTE.HOME, label: "Home" }]; @@ -79,6 +87,15 @@ export function AppHeader() { +
+ + + + + + +
+ {isCloud() && isTrial && trialDaysLeft !== 0 && ( {isCloud() && } + ); } diff --git a/apps/client/src/components/ui/emoji-picker.tsx b/apps/client/src/components/ui/emoji-picker.tsx index 952c1171..112c2d9c 100644 --- a/apps/client/src/components/ui/emoji-picker.tsx +++ b/apps/client/src/components/ui/emoji-picker.tsx @@ -15,6 +15,11 @@ export interface EmojiPickerInterface { icon: ReactNode; removeEmojiAction: () => void; readOnly: boolean; + actionIconProps?: { + size?: string; + variant?: string; + c?: string; + }; } function EmojiPicker({ @@ -22,6 +27,7 @@ function EmojiPicker({ icon, removeEmojiAction, readOnly, + actionIconProps, }: EmojiPickerInterface) { const { t } = useTranslation(); const [opened, handlers] = useDisclosure(false); @@ -64,7 +70,12 @@ function EmojiPicker({ closeOnEscape={true} > - + {icon} diff --git a/apps/client/src/ee/components/ldap-login-modal.tsx b/apps/client/src/ee/components/ldap-login-modal.tsx new file mode 100644 index 00000000..9360651d --- /dev/null +++ b/apps/client/src/ee/components/ldap-login-modal.tsx @@ -0,0 +1,124 @@ +import React, { useState } from "react"; +import { Modal, TextInput, PasswordInput, Button, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zodResolver } from "mantine-form-zod-resolver"; +import { z } from "zod"; +import { notifications } from "@mantine/notifications"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { IAuthProvider } from "@/ee/security/types/security.types"; +import APP_ROUTE from "@/lib/app-route"; +import { ldapLogin } from "@/ee/security/services/ldap-auth-service"; + +const formSchema = z.object({ + username: z.string().min(1, { message: "Username is required" }), + password: z.string().min(1, { message: "Password is required" }), +}); + +interface LdapLoginModalProps { + opened: boolean; + onClose: () => void; + provider: IAuthProvider; + workspaceId: string; +} + +export function LdapLoginModal({ + opened, + onClose, + provider, + workspaceId, +}: LdapLoginModalProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + username: "", + password: "", + }, + }); + + const handleSubmit = async (values: { + username: string; + password: string; + }) => { + setIsLoading(true); + setError(null); + + try { + const response = await ldapLogin({ + username: values.username, + password: values.password, + providerId: provider.id, + workspaceId, + }); + + // Handle MFA like the regular login + if (response?.userHasMfa) { + onClose(); + navigate(APP_ROUTE.AUTH.MFA_CHALLENGE); + } else if (response?.requiresMfaSetup) { + onClose(); + navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED); + } else { + onClose(); + navigate(APP_ROUTE.HOME); + } + } catch (err: any) { + setIsLoading(false); + const errorMessage = + err.response?.data?.message || "Authentication failed"; + setError(errorMessage); + + notifications.show({ + message: errorMessage, + color: "red", + }); + } + }; + + const handleClose = () => { + form.reset(); + setError(null); + onClose(); + }; + + return ( + +
+ + + + + + + +
+
+ ); +} diff --git a/apps/client/src/ee/components/sso-login.tsx b/apps/client/src/ee/components/sso-login.tsx index 8de93c29..8c96d9c5 100644 --- a/apps/client/src/ee/components/sso-login.tsx +++ b/apps/client/src/ee/components/sso-login.tsx @@ -1,29 +1,62 @@ +import { useState } from "react"; import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts"; import { Button, Divider, Stack } from "@mantine/core"; -import { IconLock } from "@tabler/icons-react"; +import { IconLock, IconServer } from "@tabler/icons-react"; import { IAuthProvider } from "@/ee/security/types/security.types.ts"; import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts"; import { SSO_PROVIDER } from "@/ee/security/contants.ts"; import { GoogleIcon } from "@/components/icons/google-icon.tsx"; import { isCloud } from "@/lib/config.ts"; +import { LdapLoginModal } from "@/ee/components/ldap-login-modal.tsx"; export default function SsoLogin() { const { data, isLoading } = useWorkspacePublicDataQuery(); + const [ldapModalOpened, setLdapModalOpened] = useState(false); + const [selectedLdapProvider, setSelectedLdapProvider] = useState(null); if (!data?.authProviders || data?.authProviders?.length === 0) { return null; } const handleSsoLogin = (provider: IAuthProvider) => { - window.location.href = buildSsoLoginUrl({ - providerId: provider.id, - type: provider.type, - workspaceId: data.id, - }); + if (provider.type === SSO_PROVIDER.LDAP) { + // Open modal for LDAP instead of redirecting + setSelectedLdapProvider(provider); + setLdapModalOpened(true); + } else { + // Redirect for other SSO providers + window.location.href = buildSsoLoginUrl({ + providerId: provider.id, + type: provider.type, + workspaceId: data.id, + }); + } + }; + + const getProviderIcon = (provider: IAuthProvider) => { + if (provider.type === SSO_PROVIDER.GOOGLE) { + return ; + } else if (provider.type === SSO_PROVIDER.LDAP) { + return ; + } else { + return ; + } }; return ( <> + {selectedLdapProvider && ( + { + setLdapModalOpened(false); + setSelectedLdapProvider(null); + }} + provider={selectedLdapProvider} + workspaceId={data.id} + /> + )} + {(isCloud() || data.hasLicenseKey) && ( <> @@ -31,13 +64,7 @@ export default function SsoLogin() {