diff --git a/Dockerfile b/Dockerfile index 02a35ffc..1f3b57df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine AS base +FROM node:22-slim AS base LABEL org.opencontainers.image.source="https://github.com/docmost/docmost" FROM base AS builder @@ -13,7 +13,9 @@ RUN pnpm build FROM base AS installer -RUN apk add --no-cache curl bash +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl bash \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/apps/client/index.html b/apps/client/index.html index c96058cb..28679e40 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -2,10 +2,18 @@ - - - + + + Docmost + + + + + + + + diff --git a/apps/client/package.json b/apps/client/package.json index 78306af7..db92ac24 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.22.2", + "version": "0.24.1", "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -17,6 +17,7 @@ "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "0.18.0-864353b", "@mantine/core": "^8.1.3", + "@mantine/dates": "^8.3.2", "@mantine/form": "^8.1.3", "@mantine/hooks": "^8.1.3", "@mantine/modals": "^8.1.3", @@ -26,7 +27,7 @@ "@tanstack/react-query": "^5.80.6", "@tiptap/extension-character-count": "^2.10.3", "alfaaz": "^1.1.0", - "axios": "^1.9.0", + "axios": "^1.13.2", "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "file-saver": "^2.0.5", @@ -40,7 +41,7 @@ "katex": "0.16.22", "lowlight": "^3.3.0", "mantine-form-zod-resolver": "^1.3.0", - "mermaid": "^11.6.0", + "mermaid": "^11.11.0", "mitt": "^3.0.1", "posthog-js": "^1.255.1", "react": "^18.3.1", @@ -56,7 +57,7 @@ "socket.io-client": "^4.8.1", "tippy.js": "^6.3.7", "tiptap-extension-global-drag-handle": "^0.1.18", - "zod": "^3.25.56" + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.16.0", @@ -64,10 +65,10 @@ "@types/file-saver": "^2.0.7", "@types/js-cookie": "^3.0.6", "@types/katex": "^0.16.7", - "@types/node": "22.10.0", + "@types/node": "22.19.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.4.1", + "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.15.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.1.0", @@ -80,6 +81,6 @@ "prettier": "^3.4.1", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", - "vite": "^6.3.5" + "vite": "^7.2.4" } } diff --git a/apps/client/public/favicon-16x16.png b/apps/client/public/favicon-16x16.png deleted file mode 100644 index 6298fe8a..00000000 Binary files a/apps/client/public/favicon-16x16.png and /dev/null differ diff --git a/apps/client/public/favicon-32x32.png b/apps/client/public/favicon-32x32.png deleted file mode 100644 index 40d6a30e..00000000 Binary files a/apps/client/public/favicon-32x32.png and /dev/null differ diff --git a/apps/client/public/icons/app-icon-192x192.png b/apps/client/public/icons/app-icon-192x192.png new file mode 100644 index 00000000..46bce9e5 Binary files /dev/null and b/apps/client/public/icons/app-icon-192x192.png differ diff --git a/apps/client/public/icons/app-icon-512x512.png b/apps/client/public/icons/app-icon-512x512.png new file mode 100644 index 00000000..65b91ed0 Binary files /dev/null and b/apps/client/public/icons/app-icon-512x512.png differ diff --git a/apps/client/public/icons/favicon-16x16.png b/apps/client/public/icons/favicon-16x16.png new file mode 100644 index 00000000..c8d2d56f Binary files /dev/null and b/apps/client/public/icons/favicon-16x16.png differ diff --git a/apps/client/public/icons/favicon-32x32.png b/apps/client/public/icons/favicon-32x32.png new file mode 100644 index 00000000..3ccc0fb0 Binary files /dev/null and b/apps/client/public/icons/favicon-32x32.png differ diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index 0199a502..1763e428 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -42,7 +42,7 @@ "Delete group": "Gruppe löschen", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.", "Description": "Beschreibung", - "Details": "Einzelheiten", + "Details": "Details", "e.g ACME": "z.B. ACME", "e.g ACME Inc": "z.B. ACME Inc.", "e.g Developers": "z.B. Entwickler", @@ -53,6 +53,7 @@ "e.g Space for product team": "z.B. Bereich für das Produktteam", "e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit", "Edit": "Bearbeiten", + "Read": "Lesen", "Edit group": "Gruppe bearbeiten", "Email": "E-Mail", "Enter a strong password": "Geben Sie ein starkes Passwort ein", @@ -233,9 +234,7 @@ "Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.", "Invite link": "Einladungslink", "Copy": "Kopieren", - "Copy to space": "In Raum kopieren", "Copied": "Kopiert", - "Duplicate": "Duplizieren", "Select a user": "Benutzer auswählen", "Select a group": "Gruppe auswählen", "Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.", @@ -495,5 +494,78 @@ "Page restored successfully": "Seite erfolgreich wiederhergestellt", "Deleted by": "Gelöscht von", "Deleted at": "Gelöscht am", - "Preview": "Vorschau" + "Preview": "Vorschau", + "Subpages": "Unterseiten", + "Failed to load subpages": "Fehler beim Laden von Unterseiten", + "No subpages": "Keine Unterseiten", + "Subpages (Child pages)": "Unterseiten (Untergeordnete Seiten)", + "List all subpages of the current page": "Alle Unterseiten der aktuellen Seite auflisten", + "Attachments": "Anhänge", + "All spaces": "Alle Bereiche", + "Unknown": "Unbekannt", + "Find a space": "Einen Bereich finden", + "Search in all your spaces": "In all deinen Bereichen suchen", + "Type": "Art", + "Enterprise": "Unternehmen", + "Download attachment": "Anhang herunterladen", + "Allowed email domains": "Erlaubte E-Mail-Domains", + "Only users with email addresses from these domains can signup via SSO.": "Nur Benutzer mit E-Mail-Adressen aus diesen Domains können sich über SSO registrieren.", + "Enter valid domain names separated by comma or space": "Geben Sie gültige Domainnamen ein, durch Kommas oder Leerzeichen getrennt", + "Enforce two-factor authentication": "Erzwingen der Zwei-Faktor-Authentifizierung", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Sobald es erzwungen wird, müssen alle Mitglieder die Zwei-Faktor-Authentifizierung aktivieren, um auf den Arbeitsbereich zugreifen zu können.", + "Toggle MFA enforcement": "Umschalten der MFA-Erzwingung", + "Display name": "Anzeigename", + "Allow signup": "Registrierung erlauben", + "Enabled": "Aktiviert", + "Advanced Settings": "Erweiterte Einstellungen", + "Enable TLS/SSL": "TLS/SSL aktivieren", + "Use secure connection to LDAP server": "Sichere Verbindung zum LDAP-Server verwenden", + "Group sync": "Gruppensynchronisation", + "No SSO providers found.": "Keine SSO-Anbieter gefunden.", + "Delete SSO provider": "SSO-Anbieter löschen", + "Are you sure you want to delete this SSO provider?": "Sind Sie sicher, dass Sie diesen SSO-Anbieter löschen möchten?", + "Action": "Aktion", + "{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration", + "Icon": "Icon", + "Upload image": "Bild hochladen", + "Remove image": "Bild entfernen", + "Failed to remove image": "Fehler beim Entfernen des Bildes", + "Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.", + "Image removed successfully": "Bild erfolgreich entfernt", + "API key": "API-Schlüssel", + "API key created successfully": "API-Schlüssel erfolgreich erstellt", + "API keys": "API-Schlüssel", + "API management": "API-Verwaltung", + "Are you sure you want to revoke this API key": "Sind Sie sicher, dass Sie diesen API-Schlüssel widerrufen möchten?", + "Create API Key": "API-Schlüssel erstellen", + "Custom expiration date": "Benutzerdefiniertes Ablaufdatum", + "Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein", + "Expiration": "Ablauf", + "Expired": "Abgelaufen", + "Expires": "Läuft ab", + "I've saved my API key": "Ich habe meinen API-Schlüssel gespeichert", + "Last use": "Zuletzt verwendet", + "No API keys found": "Keine API-Schlüssel gefunden", + "No expiration": "Kein Ablauf", + "Revoke API key": "API-Schlüssel widerrufen", + "Revoked successfully": "Erfolgreich widerrufen", + "Select expiration date": "Ablaufdatum wählen", + "This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.", + "Update API key": "API-Schlüssel aktualisieren", + "Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich", + "AI settings": "KI-Einstellungen", + "AI search": "KI-Suche", + "AI Answer": "KI-Antwort", + "Ask AI": "KI fragen", + "AI is thinking...": "Die KI überlegt...", + "Ask a question...": "Fragen stellen...", + "AI-powered search (Ask AI)": "KI-gestützte Suche (KI fragen)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Die KI-Suche verwendet Vektor-Einbettungen, um semantische Suchfunktionen in Ihrem Arbeitsbereich bereitzustellen.", + "Toggle AI search": "KI-Suche umschalten", + "Sources": "Quellen", + "Ask AI not available for attachments": "KI fragen nicht für Anhänge verfügbar", + "No answer available": "Keine Antwort verfügbar", + "Background color": "Hintergrundfarbe", + "Highlight color": "Hervorhebungsfarbe", + "Remove color": "Farbe entfernen" } diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index efad41cc..8cb33378 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "e.g Space for product team", "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate", "Edit": "Edit", + "Read": "Read", "Edit group": "Edit group", "Email": "Email", "Enter a strong password": "Enter a strong password", @@ -495,5 +496,78 @@ "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", + "Attachments": "Attachments", + "All spaces": "All spaces", + "Unknown": "Unknown", + "Find a space": "Find a space", + "Search in all your spaces": "Search in all your spaces", + "Type": "Type", + "Enterprise": "Enterprise", + "Download attachment": "Download attachment", + "Allowed email domains": "Allowed email domains", + "Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can signup via SSO.", + "Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space", + "Enforce two-factor authentication": "Enforce two-factor authentication", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.", + "Toggle MFA enforcement": "Toggle MFA enforcement", + "Display name": "Display name", + "Allow signup": "Allow signup", + "Enabled": "Enabled", + "Advanced Settings": "Advanced Settings", + "Enable TLS/SSL": "Enable TLS/SSL", + "Use secure connection to LDAP server": "Use secure connection to LDAP server", + "Group sync": "Group sync", + "No SSO providers found.": "No SSO providers found.", + "Delete SSO provider": "Delete SSO provider", + "Are you sure you want to delete this SSO provider?": "Are you sure you want to delete this SSO provider?", + "Action": "Action", + "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration", + "Icon": "Icon", + "Upload image": "Upload image", + "Remove image": "Remove image", + "Failed to remove image": "Failed to remove image", + "Image exceeds 10MB limit.": "Image exceeds 10MB limit.", + "Image removed successfully": "Image removed successfully", + "API key": "API key", + "API key created successfully": "API key created successfully", + "API keys": "API keys", + "API management": "API management", + "Are you sure you want to revoke this API key": "Are you sure you want to revoke this API key", + "Create API Key": "Create API Key", + "Custom expiration date": "Custom expiration date", + "Enter a descriptive token name": "Enter a descriptive token name", + "Expiration": "Expiration", + "Expired": "Expired", + "Expires": "Expires", + "I've saved my API key": "I've saved my API key", + "Last use": "Last Used", + "No API keys found": "No API keys found", + "No expiration": "No expiration", + "Revoke API key": "Revoke API key", + "Revoked successfully": "Revoked successfully", + "Select expiration date": "Select expiration date", + "This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.", + "Update API key": "Update API key", + "Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace", + "AI settings": "AI settings", + "AI search": "AI search", + "AI Answer": "AI Answer", + "Ask AI": "Ask AI", + "AI is thinking...": "AI is thinking...", + "Ask a question...": "Ask a question...", + "AI-powered search (Ask AI)": "AI-powered search (Ask AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.", + "Toggle AI search": "Toggle AI search", + "Sources": "Sources", + "Ask AI not available for attachments": "Ask AI not available for attachments", + "No answer available": "No answer available", + "Background color": "Background color", + "Highlight color": "Highlight color", + "Remove color": "Remove color" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index 407b9f14..f99e8541 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "ej: Espacio para el equipo de producto", "e.g Space for sales team to collaborate": "ej: Espacio para que el equipo de ventas colabore", "Edit": "Editar", + "Read": "Leer", "Edit group": "Editar grupo", "Email": "Correo electrónico", "Enter a strong password": "Introduce una contraseña fuerte", @@ -495,5 +496,78 @@ "Page restored successfully": "Página restaurada con éxito", "Deleted by": "Eliminado por", "Deleted at": "Eliminado en", - "Preview": "Vista previa" + "Preview": "Vista previa", + "Subpages": "Subpáginas", + "Failed to load subpages": "Error al cargar subpáginas", + "No subpages": "Sin subpáginas", + "Subpages (Child pages)": "Subpáginas (Páginas hijas)", + "List all subpages of the current page": "Listar todas las subpáginas de la página actual", + "Attachments": "Adjuntos", + "All spaces": "Todos los espacios", + "Unknown": "Desconocido", + "Find a space": "Encontrar un espacio", + "Search in all your spaces": "Buscar en todos tus espacios", + "Type": "Tipo", + "Enterprise": "Empresa", + "Download attachment": "Descargar adjunto", + "Allowed email domains": "Dominios de correo electrónico permitidos", + "Only users with email addresses from these domains can signup via SSO.": "Solo los usuarios con direcciones de correo electrónico de estos dominios pueden registrarse a través de SSO.", + "Enter valid domain names separated by comma or space": "Introduce nombres de dominio válidos separados por coma o espacio", + "Enforce two-factor authentication": "Aplicar autenticación de dos factores", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Una vez aplicada, todos los miembros deben habilitar la autenticación de dos factores para acceder al espacio de trabajo.", + "Toggle MFA enforcement": "Alternar la aplicación de MFA", + "Display name": "Nombre para mostrar", + "Allow signup": "Permitir registro", + "Enabled": "Habilitado", + "Advanced Settings": "Configuración avanzada", + "Enable TLS/SSL": "Habilitar TLS/SSL", + "Use secure connection to LDAP server": "Usar conexión segura al servidor LDAP", + "Group sync": "Sincronización de grupos", + "No SSO providers found.": "No se encontraron proveedores de SSO.", + "Delete SSO provider": "Eliminar proveedor de SSO", + "Are you sure you want to delete this SSO provider?": "¿Está seguro de que desea eliminar este proveedor de SSO?", + "Action": "Acción", + "{{ssoProviderType}} configuration": "Configuración de {{ssoProviderType}}", + "Icon": "Icono", + "Upload image": "Subir imagen", + "Remove image": "Eliminar imagen", + "Failed to remove image": "No se ha podido eliminar la imagen", + "Image exceeds 10MB limit.": "La imagen excede del límite de 10 MB", + "Image removed successfully": "Imagen eliminada correctamente", + "API key": "Clave API", + "API key created successfully": "Clave API creada correctamente", + "API keys": "Claves API", + "API management": "Gestión de API", + "Are you sure you want to revoke this API key": "¿Está seguro de que desea revocar esta clave API? ", + "Create API Key": "Crear clave API", + "Custom expiration date": "Fecha de vencimiento personalizada", + "Enter a descriptive token name": "Introduce un nombre descriptivo del token", + "Expiration": "Vencimiento", + "Expired": "Vencido", + "Expires": "Vence", + "I've saved my API key": "He guardado mi clave API", + "Last use": "Último uso", + "No API keys found": "No se han encontrado claves API", + "No expiration": "Sin vencimiento", + "Revoke API key": "Revocar clave API", + "Revoked successfully": "Revocada correctamente", + "Select expiration date": "Seleccionar fecha de vencimiento", + "This action cannot be undone. Any applications using this API key will stop working.": "Esta acción no se puede deshacer. Las aplicaciones que utilicen esta clave API dejarán de funcionar.", + "Update API key": "Actualizar clave API", + "Manage API keys for all users in the workspace": "Gestionar claves API para todos los usuarios en el espacio de trabajo", + "AI settings": "Configuración de IA", + "AI search": "Búsqueda de IA", + "AI Answer": "Respuesta de IA", + "Ask AI": "Preguntar a IA", + "AI is thinking...": "IA está pensando...", + "Ask a question...": "Haz una pregunta...", + "AI-powered search (Ask AI)": "Búsqueda impulsada por IA (Preguntar a IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La búsqueda de IA utiliza incrustaciones vectoriales para proporcionar capacidades de búsqueda semántica en todo el contenido de su espacio de trabajo.", + "Toggle AI search": "Alternar búsqueda de IA", + "Sources": "Fuentes", + "Ask AI not available for attachments": "Preguntar a IA no está disponible para adjuntos", + "No answer available": "No hay respuesta disponible", + "Background color": "Color de fondo", + "Highlight color": "Color de resaltado", + "Remove color": "Eliminar color" } diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index a502af11..5644d719 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "par ex. Espace pour l'équipe produit", "e.g Space for sales team to collaborate": "par ex. Espace pour l'équipe de vente pour collaborer", "Edit": "Modifier", + "Read": "Lire", "Edit group": "Modifier groupe", "Email": "Email", "Enter a strong password": "Entrez un mot de passe fort", @@ -495,5 +496,78 @@ "Page restored successfully": "Page restaurée avec succès", "Deleted by": "Supprimé par", "Deleted at": "Supprimé à", - "Preview": "Aperçu" + "Preview": "Aperçu", + "Subpages": "Sous-pages", + "Failed to load subpages": "Échec du chargement des sous-pages", + "No subpages": "Pas de sous-pages", + "Subpages (Child pages)": "Sous-pages (Pages enfants)", + "List all subpages of the current page": "Lister toutes les sous-pages de la page actuelle", + "Attachments": "Pièces jointes", + "All spaces": "Tous les espaces", + "Unknown": "Inconnu", + "Find a space": "Trouver un espace", + "Search in all your spaces": "Rechercher dans tous vos espaces", + "Type": "Type", + "Enterprise": "Entreprise", + "Download attachment": "Télécharger la pièce jointe", + "Allowed email domains": "Domaines de messagerie autorisés", + "Only users with email addresses from these domains can signup via SSO.": "Seuls les utilisateurs possédant des adresses e-mail provenant de ces domaines peuvent s'inscrire via SSO.", + "Enter valid domain names separated by comma or space": "Entrez des noms de domaine valides séparés par une virgule ou un espace", + "Enforce two-factor authentication": "Imposer l'authentification à deux facteurs", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Une fois appliquée, tous les membres doivent activer l'authentification à deux facteurs pour accéder à l'espace de travail.", + "Toggle MFA enforcement": "Basculer l'application de l'AMF", + "Display name": "Nom d'affichage", + "Allow signup": "Autoriser l'inscription", + "Enabled": "Activé", + "Advanced Settings": "Paramètres avancés", + "Enable TLS/SSL": "Activer TLS/SSL", + "Use secure connection to LDAP server": "Utiliser une connexion sécurisée au serveur LDAP", + "Group sync": "Synchronisation de groupe", + "No SSO providers found.": "Aucun fournisseur SSO trouvé.", + "Delete SSO provider": "Supprimer le fournisseur SSO", + "Are you sure you want to delete this SSO provider?": "Êtes-vous sûr de vouloir supprimer ce fournisseur SSO ?", + "Action": "Action", + "{{ssoProviderType}} configuration": "Configuration {{ssoProviderType}}", + "Icon": "Icône", + "Upload image": "Téléverser une image", + "Remove image": "Supprimer l'image", + "Failed to remove image": "Échec de la suppression de l'image", + "Image exceeds 10MB limit.": "L'image dépasse la limite de 10 Mo.", + "Image removed successfully": "Image supprimée avec succès", + "API key": "Clé API", + "API key created successfully": "Clé API créée avec succès", + "API keys": "Clés API", + "API management": "Gestion des API", + "Are you sure you want to revoke this API key": "Êtes-vous sûr de vouloir révoquer cette clé API", + "Create API Key": "Créer une clé API", + "Custom expiration date": "Date d'expiration personnalisée", + "Enter a descriptive token name": "Entrez un nom descriptif pour le jeton", + "Expiration": "Expiration", + "Expired": "Expiré(e)", + "Expires": "Expire", + "I've saved my API key": "J'ai enregistré ma clé API", + "Last use": "Dernière utilisation", + "No API keys found": "Aucune clé API trouvée", + "No expiration": "Pas d'expiration", + "Revoke API key": "Révoquer la clé API", + "Revoked successfully": "Révoqué(e) avec succès", + "Select expiration date": "Sélectionnez la date d'expiration", + "This action cannot be undone. Any applications using this API key will stop working.": "Cette action ne peut pas être annulée. Toutes les applications utilisant cette clé API cesseront de fonctionner.", + "Update API key": "Mettre à jour la clé API", + "Manage API keys for all users in the workspace": "Gérer les clés API pour tous les utilisateurs dans l'espace de travail", + "AI settings": "Paramètres de l'IA", + "AI search": "Recherche IA", + "AI Answer": "Réponse IA", + "Ask AI": "Demander à l'IA", + "AI is thinking...": "L'IA réfléchit...", + "Ask a question...": "Posez une question...", + "AI-powered search (Ask AI)": "Recherche assistée par l'IA (Demander à l'IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La recherche IA utilise des incorporations vectorielles pour fournir des capacités de recherche sémantique à travers le contenu de votre espace de travail.", + "Toggle AI search": "Basculer la recherche IA", + "Sources": "Sources", + "Ask AI not available for attachments": "Demande à l'IA non disponible pour les pièces jointes", + "No answer available": "Pas de réponse disponible", + "Background color": "Couleur de fond", + "Highlight color": "Couleur de surbrillance", + "Remove color": "Supprimer la couleur" } diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index f72c58c7..8d00f451 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "es. Spazio per il team di prodotto", "e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team di vendita", "Edit": "Modifica", + "Read": "Leggi", "Edit group": "Modifica gruppo", "Email": "Email", "Enter a strong password": "Inserisci una password sicura", @@ -495,5 +496,78 @@ "Page restored successfully": "Pagina ripristinata con successo", "Deleted by": "Eliminato da", "Deleted at": "Eliminato il", - "Preview": "Anteprima" + "Preview": "Anteprima", + "Subpages": "Sottopagine", + "Failed to load subpages": "Caricamento delle sottopagine non riuscito", + "No subpages": "Nessuna sottopagina", + "Subpages (Child pages)": "Sottopagine (Pagine figlie)", + "List all subpages of the current page": "Elenca tutte le sottopagine della pagina corrente", + "Attachments": "Allegati", + "All spaces": "Tutti gli spazi", + "Unknown": "Sconosciuto", + "Find a space": "Trova uno spazio", + "Search in all your spaces": "Cerca in tutti i tuoi spazi", + "Type": "Tipo", + "Enterprise": "Impresa", + "Download attachment": "Scarica allegato", + "Allowed email domains": "Domini email consentiti", + "Only users with email addresses from these domains can signup via SSO.": "Solo gli utenti con indirizzi email provenienti da questi domini possono registrarsi tramite SSO.", + "Enter valid domain names separated by comma or space": "Inserisci nomi di dominio validi separati da virgole o spazi", + "Enforce two-factor authentication": "Imponi l'autenticazione a due fattori", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Una volta impostata, tutti i membri devono abilitare l'autenticazione a due fattori per accedere all'area di lavoro.", + "Toggle MFA enforcement": "Attiva disattiva l'applicazione MFA", + "Display name": "Nome visualizzato", + "Allow signup": "Consenti iscrizione", + "Enabled": "Abilitato", + "Advanced Settings": "Impostazioni avanzate", + "Enable TLS/SSL": "Abilita TLS/SSL", + "Use secure connection to LDAP server": "Usa connessione sicura al server LDAP", + "Group sync": "Sincronizzazione gruppi", + "No SSO providers found.": "Nessun provider SSO trovato.", + "Delete SSO provider": "Elimina provider SSO", + "Are you sure you want to delete this SSO provider?": "Sei sicuro di voler eliminare questo provider SSO?", + "Action": "Azione", + "{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}", + "Icon": "Icona", + "Upload image": "Carica immagine", + "Remove image": "Rimuovi immagine", + "Failed to remove image": "Rimozione immagine fallita", + "Image exceeds 10MB limit.": "L'immagine supera il limite di 10MB.", + "Image removed successfully": "Immagine rimossa con successo", + "API key": "Chiave API", + "API key created successfully": "Chiave API creata con successo", + "API keys": "Chiavi API", + "API management": "Gestione API", + "Are you sure you want to revoke this API key": "Sei sicuro di voler revocare questa chiave API", + "Create API Key": "Crea Chiave API", + "Custom expiration date": "Data di scadenza personalizzata", + "Enter a descriptive token name": "Inserisci un nome descrittivo del token", + "Expiration": "Scadenza", + "Expired": "Scaduto", + "Expires": "Scade", + "I've saved my API key": "Ho salvato la mia chiave API", + "Last use": "Ultimo utilizzo", + "No API keys found": "Nessuna chiave API trovata", + "No expiration": "Nessuna scadenza", + "Revoke API key": "Revoca chiave API", + "Revoked successfully": "Revocata con successo", + "Select expiration date": "Seleziona la data di scadenza", + "This action cannot be undone. Any applications using this API key will stop working.": "Questa azione non può essere annullata. Qualsiasi applicazione che utilizza questa chiave API smetterà di funzionare.", + "Update API key": "Aggiorna chiave API", + "Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro", + "AI settings": "Impostazioni AI", + "AI search": "Ricerca AI", + "AI Answer": "Risposta AI", + "Ask AI": "Chiedi all'AI", + "AI is thinking...": "L'AI sta pensando...", + "Ask a question...": "Fai una domanda...", + "AI-powered search (Ask AI)": "Ricerca potenziata dall'AI (Chiedi all'AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La ricerca AI utilizza embeddings vettoriali per fornire capacità di ricerca semantica nel contenuto della tua area di lavoro.", + "Toggle AI search": "Attiva/disattiva ricerca AI", + "Sources": "Fonti", + "Ask AI not available for attachments": "Chiedere all'AI non è disponibile per gli allegati", + "No answer available": "Nessuna risposta disponibile", + "Background color": "Colore di sfondo", + "Highlight color": "Colore evidenziato", + "Remove color": "Rimuovi colore" } diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index 3e7950db..6ea006d7 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "例: 製品チームのスペース", "e.g Space for sales team to collaborate": "例: 営業チーム連携用スペース", "Edit": "編集", + "Read": "読む", "Edit group": "グループを編集", "Email": "メールアドレス", "Enter a strong password": "強力なパスワードを入力してください", @@ -495,5 +496,78 @@ "Page restored successfully": "ページが正常に復元されました", "Deleted by": "削除者", "Deleted at": "削除日時", - "Preview": "プレビュー" + "Preview": "プレビュー", + "Subpages": "サブページ", + "Failed to load subpages": "サブページの読み込みに失敗しました", + "No subpages": "サブページがありません", + "Subpages (Child pages)": "サブページ(子ページ)", + "List all subpages of the current page": "現在のページのすべてのサブページをリスト", + "Attachments": "添付ファイル", + "All spaces": "すべてのスペース", + "Unknown": "不明", + "Find a space": "スペースを探す", + "Search in all your spaces": "あなたのすべてのスペースで検索", + "Type": "タイプ", + "Enterprise": "エンタープライズ", + "Download attachment": "添付ファイルをダウンロード", + "Allowed email domains": "許可されたメールドメイン", + "Only users with email addresses from these domains can signup via SSO.": "これらのドメインからのメールアドレスを持つユーザーのみがSSOで登録できます。", + "Enter valid domain names separated by comma or space": "コンマまたはスペースで区切って有効なドメイン名を入力してください", + "Enforce two-factor authentication": "二要素認証を強制する", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "一度強制されると、すべてのメンバーはワークスペースにアクセスするために二要素認証を有効にする必要があります。", + "Toggle MFA enforcement": "MFAの強制を切り替える", + "Display name": "表示名", + "Allow signup": "登録を許可する", + "Enabled": "有効", + "Advanced Settings": "詳細設定", + "Enable TLS/SSL": "TLS/SSLを有効にする", + "Use secure connection to LDAP server": "LDAPサーバーへの安全な接続を使用する", + "Group sync": "グループ同期", + "No SSO providers found.": "SSOプロバイダーが見つかりませんでした。", + "Delete SSO provider": "SSOプロバイダーを削除する", + "Are you sure you want to delete this SSO provider?": "このSSOプロバイダーを削除してもよろしいですか?", + "Action": "アクション", + "{{ssoProviderType}} configuration": "{{ssoProviderType}}の構成", + "Icon": "アイコン", + "Upload image": "画像をアップロード", + "Remove image": "画像を削除", + "Failed to remove image": "画像の削除に失敗しました", + "Image exceeds 10MB limit.": "画像が10MBの制限を超えています。", + "Image removed successfully": "画像が正常に削除されました", + "API key": "APIキー", + "API key created successfully": "APIキーが正常に作成されました", + "API keys": "APIキー", + "API management": "API管理", + "Are you sure you want to revoke this API key": "このAPIキーを無効にしてもよろしいですか", + "Create API Key": "APIキーを作成", + "Custom expiration date": "カスタム有効期限", + "Enter a descriptive token name": "説明的なトークン名を入力してください", + "Expiration": "有効期限", + "Expired": "期限切れ", + "Expires": "期限が切れます", + "I've saved my API key": "APIキーを保存しました", + "Last use": "最終使用", + "No API keys found": "APIキーが見つかりません", + "No expiration": "期限なし", + "Revoke API key": "APIキーを無効にする", + "Revoked successfully": "正常に無効化されました", + "Select expiration date": "有効期限を選択してください", + "This action cannot be undone. Any applications using this API key will stop working.": "この操作は元に戻せません。このAPIキーを使用しているアプリケーションは動作を停止します。", + "Update API key": "APIキーを更新", + "Manage API keys for all users in the workspace": "ワークスペース内のすべてのユーザーのAPIキーを管理", + "AI settings": "AI設定", + "AI search": "AI検索", + "AI Answer": "AI回答", + "Ask AI": "AIに質問する", + "AI is thinking...": "AIが考え中...", + "Ask a question...": "質問を入力...", + "AI-powered search (Ask AI)": "AIによる検索(AIに質問)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI検索はベクター埋め込みを使用して、ワークスペースコンテンツ全体にわたって意味検索機能を提供します。", + "Toggle AI search": "AI検索を切り替え", + "Sources": "ソース", + "Ask AI not available for attachments": "添付ファイルにはAI質問は利用できません", + "No answer available": "回答がありません", + "Background color": "背景色", + "Highlight color": "ハイライト色", + "Remove color": "色を削除" } diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index fdf35b72..6e1f5b24 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "예: 제품 팀을 위한 Space", "e.g Space for sales team to collaborate": "예: 영업 팀의 Space", "Edit": "편집", + "Read": "읽기", "Edit group": "팀 편집", "Email": "이메일", "Enter a strong password": "강력한 비밀번호를 입력하세요", @@ -495,5 +496,78 @@ "Page restored successfully": "페이지가 성공적으로 복구되었습니다", "Deleted by": "삭제자", "Deleted at": "삭제 시간", - "Preview": "미리보기" + "Preview": "미리보기", + "Subpages": "하위 페이지", + "Failed to load subpages": "하위 페이지 로드 실패", + "No subpages": "하위 페이지 없음", + "Subpages (Child pages)": "하위 페이지 (자식 페이지)", + "List all subpages of the current page": "현재 페이지의 모든 하위 페이지 목록", + "Attachments": "첨부 파일", + "All spaces": "전체 공간", + "Unknown": "알 수 없음", + "Find a space": "공간 찾기", + "Search in all your spaces": "모든 공간에서 검색", + "Type": "유형", + "Enterprise": "기업", + "Download attachment": "첨부 파일 다운로드", + "Allowed email domains": "허용된 이메일 도메인", + "Only users with email addresses from these domains can signup via SSO.": "이 도메인의 이메일 주소를 가진 사용자만 SSO를 통해 가입할 수 있습니다.", + "Enter valid domain names separated by comma or space": "콤마 또는 공백으로 구분하여 유효한 도메인 이름 입력", + "Enforce two-factor authentication": "이중 인증 시행", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "시행되면 모든 멤버가 작업 공간에 액세스하기 위해 이중 인증을 활성화해야 합니다.", + "Toggle MFA enforcement": "MFA 시행 전환", + "Display name": "표시 이름", + "Allow signup": "가입 허용", + "Enabled": "활성화됨", + "Advanced Settings": "고급 설정", + "Enable TLS/SSL": "TLS\\/SSL 활성화", + "Use secure connection to LDAP server": "LDAP 서버에 안전한 연결 사용", + "Group sync": "그룹 동기화", + "No SSO providers found.": "SSO 제공자를 찾을 수 없습니다.", + "Delete SSO provider": "SSO 제공자 삭제", + "Are you sure you want to delete this SSO provider?": "이 SSO 제공자를 삭제하시겠습니까?", + "Action": "작업", + "{{ssoProviderType}} configuration": "{{ssoProviderType}} 구성", + "Icon": "아이콘", + "Upload image": "이미지 업로드", + "Remove image": "이미지 제거", + "Failed to remove image": "이미지 제거 실패", + "Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.", + "Image removed successfully": "이미지가 성공적으로 제거되었습니다", + "API key": "API 키", + "API key created successfully": "API 키 생성 완료", + "API keys": "API 키", + "API management": "API 관리", + "Are you sure you want to revoke this API key": "이 API 키를 취소하시겠습니까?", + "Create API Key": "API 키 생성", + "Custom expiration date": "사용자 정의 만료일", + "Enter a descriptive token name": "토큰 이름을 입력하세요", + "Expiration": "만료", + "Expired": "만료됨", + "Expires": "만료일", + "I've saved my API key": "API 키를 저장했습니다", + "Last use": "최근 사용", + "No API keys found": "API 키를 찾을 수 없습니다", + "No expiration": "유효기간 없음", + "Revoke API key": "API 키 취소", + "Revoked successfully": "성공적으로 취소되었습니다", + "Select expiration date": "만료일 선택", + "This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.", + "Update API key": "API 키 갱신", + "Manage API keys for all users in the workspace": "워크스페이스 내 모든 사용자의 API 키 관리", + "AI settings": "AI 설정", + "AI search": "AI 검색", + "AI Answer": "AI 답변", + "Ask AI": "AI에게 묻기", + "AI is thinking...": "AI가 생각 중입니다...", + "Ask a question...": "질문하세요...", + "AI-powered search (Ask AI)": "AI 지원 검색 (AI에게 묻기)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI 검색은 벡터 임베딩을 사용하여 작업공간 콘텐츠에 대한 의미 검색 기능을 제공합니다.", + "Toggle AI search": "AI 검색 전환", + "Sources": "출처", + "Ask AI not available for attachments": "AI에게 묻기 기능은 첨부 파일에 대해 사용할 수 없습니다", + "No answer available": "답변을 제공할 수 없습니다", + "Background color": "배경 색", + "Highlight color": "강조 색", + "Remove color": "색 제거" } diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 1757f92e..7db6836d 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -34,7 +34,7 @@ "Create group": "Groep aanmaken", "Create page": "Pagina aanmaken", "Create space": "Ruimte aanmaken", - "Create workspace": "Wwerkruimte aanmaken", + "Create workspace": "Werkruimte aanmaken", "Current password": "Huidig wachtwoord", "Dark": "Donker", "Date": "Datum", @@ -53,6 +53,7 @@ "e.g Space for product team": "bijv. Ruimte voor productteam", "e.g Space for sales team to collaborate": "bijv. Ruimte voor verkoopteam om samen te werken", "Edit": "Bewerken", + "Read": "Lezen", "Edit group": "Groep bewerken", "Email": "E-mailadres", "Enter a strong password": "Voer een sterk wachtwoord in", @@ -90,7 +91,7 @@ "Invite by email": "Uitnodigen via e-mail", "Invite members": "Leden uitnodigen", "Invite new members": "Nieuwe leden uitnodigen", - "Invited members who are yet to accept their invitation will appear here.": "Uigenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.", + "Invited members who are yet to accept their invitation will appear here.": "Uitgenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.", "Invited members will be granted access to spaces the groups can access": "Uitgenodigde leden wordt toegang gegeven tot ruimtes de groepen toegang toe heeft", "Join the workspace": "Word lid van de werkruimte", "Language": "Taal", @@ -495,5 +496,78 @@ "Page restored successfully": "Pagina succesvol hersteld", "Deleted by": "Verwijderd door", "Deleted at": "Verwijderd op", - "Preview": "Voorbeeld" + "Preview": "Voorbeeld", + "Subpages": "Subpagina's", + "Failed to load subpages": "Laden van subpagina's mislukt", + "No subpages": "Geen subpagina's", + "Subpages (Child pages)": "Subpagina's (Kindpagina's)", + "List all subpages of the current page": "Lijst van alle subpagina's van de huidige pagina", + "Attachments": "Bijlagen", + "All spaces": "Alle ruimtes", + "Unknown": "Onbekend", + "Find a space": "Vind een ruimte", + "Search in all your spaces": "Zoek in al je ruimtes", + "Type": "Type", + "Enterprise": "Onderneming", + "Download attachment": "Bijlage downloaden", + "Allowed email domains": "Toegestane e-maildomeinen", + "Only users with email addresses from these domains can signup via SSO.": "Alleen gebruikers met e-mailadressen van deze domeinen kunnen zich aanmelden via SSO.", + "Enter valid domain names separated by comma or space": "Voer geldige domeinnamen in, gescheiden door komma of spatie", + "Enforce two-factor authentication": "Handhaaf tweefactorauthenticatie", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Na handhaving moeten alle leden tweefactorauthenticatie inschakelen om toegang te krijgen tot de werkomgeving.", + "Toggle MFA enforcement": "Schakel MFA-handhaving in of uit", + "Display name": "Weergavenaam", + "Allow signup": "Aanmelden toestaan", + "Enabled": "Ingeschakeld", + "Advanced Settings": "Geavanceerde instellingen", + "Enable TLS/SSL": "TLS/SSL inschakelen", + "Use secure connection to LDAP server": "Gebruik een beveiligde verbinding met de LDAP-server", + "Group sync": "Groepssynchronisatie", + "No SSO providers found.": "Geen SSO-providers gevonden.", + "Delete SSO provider": "Verwijder SSO-provider", + "Are you sure you want to delete this SSO provider?": "Weet u zeker dat u deze SSO-provider wilt verwijderen?", + "Action": "Actie", + "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuratie", + "Icon": "Icoon", + "Upload image": "Afbeelding uploaden", + "Remove image": "Afbeelding verwijderen", + "Failed to remove image": "Afbeelding verwijderen mislukt", + "Image exceeds 10MB limit.": "Afbeelding overschrijdt de limiet van 10MB.", + "Image removed successfully": "Afbeelding succesvol verwijderd", + "API key": "API-sleutel", + "API key created successfully": "API-sleutel succesvol aangemaakt", + "API keys": "API-sleutels", + "API management": "API-beheer", + "Are you sure you want to revoke this API key": "Weet u zeker dat u deze API-sleutel wilt intrekken", + "Create API Key": "API-sleutel aanmaken", + "Custom expiration date": "Aangepaste vervaldatum", + "Enter a descriptive token name": "Voer een beschrijvende tokennaam in", + "Expiration": "Vervaldatum", + "Expired": "Verlopen", + "Expires": "Verloopt", + "I've saved my API key": "Ik heb mijn API-sleutel opgeslagen", + "Last use": "Laatst gebruikt", + "No API keys found": "Geen API-sleutels gevonden", + "No expiration": "Geen vervaldatum", + "Revoke API key": "API-sleutel intrekken", + "Revoked successfully": "Succesvol ingetrokken", + "Select expiration date": "Selecteer vervaldatum", + "This action cannot be undone. Any applications using this API key will stop working.": "Deze actie kan niet ongedaan worden gemaakt. Alle toepassingen die deze API-sleutel gebruiken, zullen niet meer werken.", + "Update API key": "API-sleutel bijwerken", + "Manage API keys for all users in the workspace": "Beheer API-sleutels voor alle gebruikers in de werkruimte", + "AI settings": "AI-instellingen", + "AI search": "AI-zoekopdracht", + "AI Answer": "AI Antwoord", + "Ask AI": "Vraag AI", + "AI is thinking...": "AI is aan het nadenken...", + "Ask a question...": "Stel een vraag...", + "AI-powered search (Ask AI)": "AI-ondersteunde zoekopdracht (Vraag AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI-zoekopdracht maakt gebruik van vectorembeddings om semantische zoekmogelijkheden te bieden in uw werkruimte-inhoud.", + "Toggle AI search": "Schakel AI-zoekopdracht in/uit", + "Sources": "Bronnen", + "Ask AI not available for attachments": "Vraag AI is niet beschikbaar voor bijlages", + "No answer available": "Geen antwoord beschikbaar", + "Background color": "Achtergrondkleur", + "Highlight color": "Markeerkleur", + "Remove color": "Kleur verwijderen" } diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index 1ed2c6e5..5d11ec7a 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "ex.: Espaço para a equipe de produto", "e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar", "Edit": "Editar", + "Read": "Ler", "Edit group": "Editar grupo", "Email": "Email", "Enter a strong password": "Insira uma senha forte", @@ -495,5 +496,78 @@ "Page restored successfully": "Página restaurada com sucesso", "Deleted by": "Excluído por", "Deleted at": "Excluído em", - "Preview": "Visualização" + "Preview": "Visualização", + "Subpages": "Subpáginas", + "Failed to load subpages": "Falha ao carregar subpáginas", + "No subpages": "Sem subpáginas", + "Subpages (Child pages)": "Subpáginas (Páginas filhas)", + "List all subpages of the current page": "Listar todas as subpáginas da página atual", + "Attachments": "Anexos", + "All spaces": "Todos os espaços", + "Unknown": "Desconhecido", + "Find a space": "Encontrar um espaço", + "Search in all your spaces": "Pesquisar em todos os seus espaços", + "Type": "Tipo", + "Enterprise": "Empresa", + "Download attachment": "Baixar anexo", + "Allowed email domains": "Domínios de email permitidos", + "Only users with email addresses from these domains can signup via SSO.": "Apenas usuários com endereços de email desses domínios podem se inscrever via SSO.", + "Enter valid domain names separated by comma or space": "Insira nomes de domínio válidos separados por vírgula ou espaço", + "Enforce two-factor authentication": "Impor autenticação de dois fatores", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Uma vez imposto, todos os membros devem habilitar a autenticação de dois fatores para acessar o espaço de trabalho.", + "Toggle MFA enforcement": "Alternar imposição de MFA", + "Display name": "Nome de exibição", + "Allow signup": "Permitir inscrição", + "Enabled": "Habilitado", + "Advanced Settings": "Configurações Avançadas", + "Enable TLS/SSL": "Habilitar TLS/SSL", + "Use secure connection to LDAP server": "Usar conexão segura com o servidor LDAP", + "Group sync": "Sincronização de grupo", + "No SSO providers found.": "Nenhum provedor de SSO encontrado.", + "Delete SSO provider": "Excluir provedor de SSO", + "Are you sure you want to delete this SSO provider?": "Tem certeza de que deseja excluir este provedor de SSO?", + "Action": "Ação", + "{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}", + "Icon": "Ícone", + "Upload image": "Fazer upload da imagem", + "Remove image": "Remover imagem", + "Failed to remove image": "Falha ao remover imagem", + "Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.", + "Image removed successfully": "Imagem removida com sucesso", + "API key": "Chave API", + "API key created successfully": "Chave API criada com sucesso", + "API keys": "Chaves API", + "API management": "Gestão de API", + "Are you sure you want to revoke this API key": "Tem certeza de que deseja revogar esta chave API", + "Create API Key": "Criar Chave API", + "Custom expiration date": "Data de expiração personalizada", + "Enter a descriptive token name": "Insira um nome descritivo para o token", + "Expiration": "Expiração", + "Expired": "Expirado", + "Expires": "Expira", + "I've saved my API key": "Salvei minha chave API", + "Last use": "Último uso", + "No API keys found": "Nenhuma chave API encontrada", + "No expiration": "Sem expiração", + "Revoke API key": "Revogar chave API", + "Revoked successfully": "Revogada com sucesso", + "Select expiration date": "Selecionar data de expiração", + "This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.", + "Update API key": "Atualizar chave API", + "Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho", + "AI settings": "Configurações de IA", + "AI search": "Pesquisa IA", + "AI Answer": "Resposta de IA", + "Ask AI": "Pergunte à IA", + "AI is thinking...": "IA está pensando...", + "Ask a question...": "Faça uma pergunta...", + "AI-powered search (Ask AI)": "Pesquisa com IA (Pergunte à IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "A pesquisa IA usa vetores de incorporação para fornecer capacidades de pesquisa semântica em todo o conteúdo do seu espaço de trabalho.", + "Toggle AI search": "Alternar pesquisa de IA", + "Sources": "Fontes", + "Ask AI not available for attachments": "Perguntar à IA não está disponível para anexos", + "No answer available": "Nenhuma resposta disponível", + "Background color": "Cor de fundo", + "Highlight color": "Cor de destaque", + "Remove color": "Remover cor" } diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index 3ad0607f..f1a9cd85 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "например, Пространство для продуктовой команды", "e.g Space for sales team to collaborate": "например, Пространство для совместной работы команды продаж", "Edit": "Редактировать", + "Read": "Читать", "Edit group": "Редактировать группу", "Email": "Электронная почта", "Enter a strong password": "Введите надёжный пароль", @@ -495,5 +496,78 @@ "Page restored successfully": "Страница успешно восстановлена", "Deleted by": "Удалено пользователем", "Deleted at": "Удалено в", - "Preview": "Предпросмотр" + "Preview": "Предпросмотр", + "Subpages": "Подстраницы", + "Failed to load subpages": "Не удалось загрузить под страницы", + "No subpages": "Нет подстраниц", + "Subpages (Child pages)": "Подстраницы (вложенные страницы)", + "List all subpages of the current page": "Показать все под страницы", + "Attachments": "Вложения", + "All spaces": "Все пространства", + "Unknown": "Неизвестно", + "Find a space": "Найти пространство", + "Search in all your spaces": "Поиск во всех ваших пространствах", + "Type": "Тип", + "Enterprise": "Предприятие", + "Download attachment": "Скачать вложение", + "Allowed email domains": "Разрешенные домены электронной почты", + "Only users with email addresses from these domains can signup via SSO.": "Только пользователи с электронными адресами из этих доменов могут зарегистрироваться через SSO.", + "Enter valid domain names separated by comma or space": "Введите допустимые доменные имена, разделённые запятыми или пробелами", + "Enforce two-factor authentication": "Обязательная двухфакторная аутентификация", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "После введения обязательности все участники должны будут включить двухфакторную аутентификацию для доступа к рабочему пространству.", + "Toggle MFA enforcement": "Переключить обязательность MFA", + "Display name": "Отображаемое имя", + "Allow signup": "Разрешить регистрацию", + "Enabled": "Включено", + "Advanced Settings": "Расширенные настройки", + "Enable TLS/SSL": "Включить TLS/SSL", + "Use secure connection to LDAP server": "Использовать защищённое соединение с сервером LDAP", + "Group sync": "Синхронизация группы", + "No SSO providers found.": "Поставщики SSO не найдены.", + "Delete SSO provider": "Удалить поставщика SSO", + "Are you sure you want to delete this SSO provider?": "Вы уверены, что хотите удалить этого поставщика SSO?", + "Action": "Действие", + "{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}", + "Icon": "Иконка", + "Upload image": "Загрузить изображение", + "Remove image": "Удалить изображение", + "Failed to remove image": "Не удалось удалить изображение", + "Image exceeds 10MB limit.": "Изображение превышает предел 10MB.", + "Image removed successfully": "Изображение успешно удалено", + "API key": "API ключ", + "API key created successfully": "API ключ успешно создан", + "API keys": "API ключи", + "API management": "Управление API", + "Are you sure you want to revoke this API key": "Вы уверены, что хотите отозвать этот API ключ", + "Create API Key": "Создать API ключ", + "Custom expiration date": "Пользовательская дата срока действия", + "Enter a descriptive token name": "Введите понятное имя токена", + "Expiration": "Срок действия", + "Expired": "Истек", + "Expires": "Истекает", + "I've saved my API key": "Я сохранил мой API ключ", + "Last use": "Последнее использование", + "No API keys found": "API ключи не найдены", + "No expiration": "Не истекает", + "Revoke API key": "Отозвать API ключ", + "Revoked successfully": "Отозван успешно", + "Select expiration date": "Выберете срок действия", + "This action cannot be undone. Any applications using this API key will stop working.": "Это действие необратимо. Любые приложения, использующие этот API ключ, перестанут работать.", + "Update API key": "Обновить API ключ", + "Manage API keys for all users in the workspace": "Управлять API ключами для всех пользователей в рабочей области", + "AI settings": "Настройки ИИ", + "AI search": "Поиск ИИ", + "AI Answer": "Ответ ИИ", + "Ask AI": "Спросить ИИ", + "AI is thinking...": "ИИ обрабатывает запрос...", + "Ask a question...": "Задайте вопрос...", + "AI-powered search (Ask AI)": "Поиск на базе ИИ (Спросить ИИ)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Поиск ИИ использует векторные встраивания для обеспечения семантического поиска по содержимому вашего рабочего пространства.", + "Toggle AI search": "Переключить поиск ИИ", + "Sources": "Источники", + "Ask AI not available for attachments": "Функция \"Спросить ИИ\" недоступна для вложений", + "No answer available": "Ответ недоступен", + "Background color": "Цвет фона", + "Highlight color": "Цвет выделения", + "Remove color": "Удалить цвет" } diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index e3f359a3..2fb44ad1 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "наприклад, Простір для продуктової команди", "e.g Space for sales team to collaborate": "наприклад, Простір для спільної роботи команди продажів", "Edit": "Редагувати", + "Read": "Читати", "Edit group": "Редагувати групу", "Email": "Електронна пошта", "Enter a strong password": "Введіть надійний пароль", @@ -495,5 +496,78 @@ "Page restored successfully": "Сторінку успішно відновлено", "Deleted by": "Видалено", "Deleted at": "Видалено о", - "Preview": "Попередній перегляд" + "Preview": "Попередній перегляд", + "Subpages": "Підсторінки", + "Failed to load subpages": "Не вдалося завантажити підсторінки", + "No subpages": "Немає підсторінок", + "Subpages (Child pages)": "Підсторінки (дочірні сторінки)", + "List all subpages of the current page": "Перелік всіх підсторінок поточної сторінки", + "Attachments": "Вкладення", + "All spaces": "Усі простори", + "Unknown": "Невідомо", + "Find a space": "Знайти простір", + "Search in all your spaces": "Шукати у всіх ваших просторах", + "Type": "Тип", + "Enterprise": "Підприємство", + "Download attachment": "Завантажити вкладення", + "Allowed email domains": "Дозволені домени електронної пошти", + "Only users with email addresses from these domains can signup via SSO.": "Лише користувачі з адресами електронної пошти з цих доменів можуть реєструватися через SSO.", + "Enter valid domain names separated by comma or space": "Введіть дійсні доменні імена, розділені комою або пробілом", + "Enforce two-factor authentication": "Вимагати двофакторну автентифікацію", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "Після увімкнення всі учасники повинні ввімкнути двофакторну автентифікацію для доступу до робочого простору.", + "Toggle MFA enforcement": "Перемикання вимоги MFA", + "Display name": "Відображуване ім'я", + "Allow signup": "Дозволити реєстрацію", + "Enabled": "Увімкнено", + "Advanced Settings": "Розширені налаштування", + "Enable TLS/SSL": "Увімкнути TLS/SSL", + "Use secure connection to LDAP server": "Використовувати захищене з'єднання з сервером LDAP", + "Group sync": "Синхронізація групи", + "No SSO providers found.": "Постачальників SSO не знайдено.", + "Delete SSO provider": "Видалити постачальника SSO", + "Are you sure you want to delete this SSO provider?": "Ви впевнені, що хочете видалити цього постачальника SSO?", + "Action": "Дія", + "{{ssoProviderType}} configuration": "Конфігурація {{ssoProviderType}}", + "Icon": "Іконка", + "Upload image": "Завантажити зображення", + "Remove image": "Видалити зображення", + "Failed to remove image": "Не вдалося видалити зображення", + "Image exceeds 10MB limit.": "Зображення має займати менше, ніж 10 МБ.", + "Image removed successfully": "Зображення видалено", + "API key": "Ключ API", + "API key created successfully": "Ключ API успішно створено", + "API keys": "Ключі API", + "API management": "Управління API", + "Are you sure you want to revoke this API key": "Ви впевнені, що хочете відкликати цей ключ API", + "Create API Key": "Створити ключ API", + "Custom expiration date": "Користувацька дата закінчення", + "Enter a descriptive token name": "Введіть описову назву токена", + "Expiration": "Термін дії", + "Expired": "Закінчився", + "Expires": "Закінчується", + "I've saved my API key": "Я зберіг свій ключ API", + "Last use": "Останнє використання", + "No API keys found": "Ключі API не знайдено", + "No expiration": "Без терміну дії", + "Revoke API key": "Відкликати ключ API", + "Revoked successfully": "Успішно відкликано", + "Select expiration date": "Виберіть дату закінчення", + "This action cannot be undone. Any applications using this API key will stop working.": "Цю дію не можна скасувати. Будь-які додатки, що використовують цей ключ API, перестануть працювати.", + "Update API key": "Оновити ключ API", + "Manage API keys for all users in the workspace": "Керувати ключами API для всіх користувачів у робочій області", + "AI settings": "Налаштування ШІ", + "AI search": "Пошук з ШІ", + "AI Answer": "Відповідь ШІ", + "Ask AI": "Запитати ШІ", + "AI is thinking...": "ШІ думає...", + "Ask a question...": "Задайте питання...", + "AI-powered search (Ask AI)": "Пошук на базі ШІ (Запитати ШІ)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Пошук з ШІ використовує векторні вбудовування для надання можливостей семантичного пошуку у вашому робочому вмісті.", + "Toggle AI search": "Переключити пошук з ШІ", + "Sources": "Джерела", + "Ask AI not available for attachments": "Запитати ШІ недоступно для вкладень", + "No answer available": "Відповідь недоступна", + "Background color": "Колір фону", + "Highlight color": "Колір підсвічування", + "Remove color": "Видалити колір" } diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index 56c7ec85..d4b25deb 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -53,6 +53,7 @@ "e.g Space for product team": "例如:产品团队的空间", "e.g Space for sales team to collaborate": "例如:销售团队协作的空间", "Edit": "编辑", + "Read": "阅读", "Edit group": "编辑群组", "Email": "电子邮箱", "Enter a strong password": "输入一个强密码", @@ -495,5 +496,78 @@ "Page restored successfully": "页面恢复成功", "Deleted by": "删除人", "Deleted at": "删除时间", - "Preview": "预览" + "Preview": "预览", + "Subpages": "子页面", + "Failed to load subpages": "加载子页面失败", + "No subpages": "没有子页面", + "Subpages (Child pages)": "子页面(子页面)", + "List all subpages of the current page": "列出当前页面的所有子页面", + "Attachments": "附件", + "All spaces": "所有空间", + "Unknown": "未知", + "Find a space": "查找空间", + "Search in all your spaces": "在您的所有空间中搜索", + "Type": "类型", + "Enterprise": "企业", + "Download attachment": "下载附件", + "Allowed email domains": "允许的电子邮件域", + "Only users with email addresses from these domains can signup via SSO.": "只有来自这些域的电子邮件地址的用户才能通过SSO注册。", + "Enter valid domain names separated by comma or space": "输入用逗号或空格分隔的有效域名", + "Enforce two-factor authentication": "强制实施双因素认证", + "Once enforced, all members must enable two-factor authentication to access the workspace.": "一旦实施,所有成员必须启用双因素认证才能访问工作区。", + "Toggle MFA enforcement": "切换多因素认证实施", + "Display name": "显示名称", + "Allow signup": "允许注册", + "Enabled": "已启用", + "Advanced Settings": "高级设置", + "Enable TLS/SSL": "启用TLS/SSL", + "Use secure connection to LDAP server": "使用安全连接到LDAP服务器", + "Group sync": "组同步", + "No SSO providers found.": "未找到SSO提供商。", + "Delete SSO provider": "删除SSO提供商", + "Are you sure you want to delete this SSO provider?": "您确定要删除此SSO提供商吗?", + "Action": "操作", + "{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置", + "Icon": "图标", + "Upload image": "上传图片", + "Remove image": "删除图片", + "Failed to remove image": "无法删除图片", + "Image exceeds 10MB limit.": "图片超过10MB限制。", + "Image removed successfully": "图片删除成功", + "API key": "API密钥", + "API key created successfully": "API密钥创建成功", + "API keys": "API密钥", + "API management": "API管理", + "Are you sure you want to revoke this API key": "确定要撤销此API密钥吗", + "Create API Key": "创建API密钥", + "Custom expiration date": "自定义到期日期", + "Enter a descriptive token name": "输入描述性令牌名称", + "Expiration": "到期", + "Expired": "已过期", + "Expires": "到期", + "I've saved my API key": "我已保存我的API密钥", + "Last use": "上次使用", + "No API keys found": "找不到API密钥", + "No expiration": "无到期", + "Revoke API key": "撤销API密钥", + "Revoked successfully": "撤销成功", + "Select expiration date": "选择到期日期", + "This action cannot be undone. Any applications using this API key will stop working.": "此操作无法撤销。使用此API密钥的任何应用程序将停止工作。", + "Update API key": "更新API密钥", + "Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥", + "AI settings": "AI设置", + "AI search": "AI搜索", + "AI Answer": "AI回答", + "Ask AI": "询问AI", + "AI is thinking...": "AI正在思考...", + "Ask a question...": "提问...", + "AI-powered search (Ask AI)": "AI驱动的搜索(询问AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI搜索使用向量嵌入提供跨工作空间内容的语义搜索功能。", + "Toggle AI search": "切换AI搜索", + "Sources": "来源", + "Ask AI not available for attachments": "附件不支持询问AI", + "No answer available": "无可用答案", + "Background color": "背景颜色", + "Highlight color": "突出显示颜色", + "Remove color": "移除颜色" } diff --git a/apps/client/public/manifest.json b/apps/client/public/manifest.json new file mode 100644 index 00000000..3e4b35dd --- /dev/null +++ b/apps/client/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "Docmost", + "short_name": "Docmost", + "start_url": "/", + "display": "standalone", + "background_color": "#222", + "theme_color": "#222", + "icons": [ + { + "src": "icons/favicon-16x16.png", + "type": "image/png", + "sizes": "16x16" + }, + { + "src": "icons/favicon-32x32.png", + "type": "image/png", + "sizes": "32x32" + }, + { + "src": "icons/app-icon-192x192.png", + "type": "image/png", + "sizes": "180x180 192x192" + }, + { + "src": "icons/app-icon-512x512.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 3995191d..e0df67a7 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -35,6 +35,9 @@ import SpacesPage from "@/pages/spaces/spaces.tsx"; import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page"; import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page"; import SpaceTrash from "@/pages/space/space-trash.tsx"; +import UserApiKeys from "@/ee/api-key/pages/user-api-keys"; +import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys"; +import AiSettings from "@/ee/ai/pages/ai-settings.tsx"; export default function App() { const { t } = useTranslation(); @@ -96,13 +99,16 @@ export default function App() { path={"account/preferences"} element={} /> + } /> } /> } /> + } /> } /> } /> } /> } /> } /> + } /> {!isCloud() && } />} {isCloud() && } />} diff --git a/apps/client/src/components/common/avatar-uploader.tsx b/apps/client/src/components/common/avatar-uploader.tsx new file mode 100644 index 00000000..0c83411c --- /dev/null +++ b/apps/client/src/components/common/avatar-uploader.tsx @@ -0,0 +1,165 @@ +import React, { useRef } from "react"; +import { Menu, Box, Loader } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { IconTrash, IconUpload } from "@tabler/icons-react"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; +import { notifications } from "@mantine/notifications"; + +interface AvatarUploaderProps { + currentImageUrl?: string | null; + fallbackName?: string; + radius?: string | number; + size?: string | number; + variant?: string; + type: AvatarIconType; + onUpload: (file: File) => Promise; + onRemove: () => Promise; + isLoading?: boolean; + disabled?: boolean; +} + +export default function AvatarUploader({ + currentImageUrl, + fallbackName, + radius, + variant, + size, + type, + onUpload, + onRemove, + isLoading = false, + disabled = false, +}: AvatarUploaderProps) { + const { t } = useTranslation(); + const fileInputRef = useRef(null); + + const handleFileInputChange = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file || disabled) { + return; + } + + // Validate file size (max 10MB) + const maxSizeInBytes = 10 * 1024 * 1024; + if (file.size > maxSizeInBytes) { + notifications.show({ + message: t("Image exceeds 10MB limit."), + color: "red", + }); + // Reset the input + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + return; + } + + try { + await onUpload(file); + } catch (error) { + console.error(error); + notifications.show({ + message: t("Failed to upload image"), + color: "red", + }); + } + + // Reset the input so the same file can be selected again + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleUploadClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } else { + console.error("File input ref is null!"); + } + }; + + const handleRemove = async () => { + if (disabled) return; + + try { + await onRemove(); + notifications.show({ + message: t("Image removed successfully"), + }); + } catch (error) { + console.error(error); + notifications.show({ + message: t("Failed to remove image"), + color: "red", + }); + } + }; + + return ( + + + + + + + + {isLoading && ( + + + + )} + + + + + } + disabled={isLoading || disabled} + onClick={handleUploadClick} + > + {t("Upload image")} + + + {currentImageUrl && ( + } + color="red" + onClick={handleRemove} + disabled={isLoading || disabled} + > + {t("Remove image")} + + )} + + + + ); +} diff --git a/apps/client/src/components/common/no-table-results.tsx b/apps/client/src/components/common/no-table-results.tsx index 124bbb9b..0f34fa2f 100644 --- a/apps/client/src/components/common/no-table-results.tsx +++ b/apps/client/src/components/common/no-table-results.tsx @@ -4,14 +4,15 @@ import { useTranslation } from "react-i18next"; interface NoTableResultsProps { colSpan: number; + text?: string; } -export default function NoTableResults({ colSpan }: NoTableResultsProps) { +export default function NoTableResults({ colSpan, text }: NoTableResultsProps) { const { t } = useTranslation(); return ( - {t("No results found...")} + {text || t("No results found...")} 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/layouts/global/top-menu.tsx b/apps/client/src/components/layouts/global/top-menu.tsx index d3a89ecc..84925080 100644 --- a/apps/client/src/components/layouts/global/top-menu.tsx +++ b/apps/client/src/components/layouts/global/top-menu.tsx @@ -1,8 +1,8 @@ import { Group, Menu, - UnstyledButton, Text, + UnstyledButton, useMantineColorScheme, } from "@mantine/core"; import { @@ -10,7 +10,6 @@ import { IconBrush, IconCheck, IconChevronDown, - IconChevronRight, IconDeviceDesktop, IconLogout, IconMoon, @@ -26,6 +25,7 @@ import APP_ROUTE from "@/lib/app-route.ts"; import useAuth from "@/features/auth/hooks/use-auth.ts"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { useTranslation } from "react-i18next"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; export default function TopMenu() { const { t } = useTranslation(); @@ -50,6 +50,7 @@ export default function TopMenu() { name={workspace?.name} variant="filled" size="sm" + type={AvatarIconType.WORKSPACE_ICON} /> {workspace?.name} diff --git a/apps/client/src/components/settings/app-version.tsx b/apps/client/src/components/settings/app-version.tsx index cb332478..5ba9ce2a 100644 --- a/apps/client/src/components/settings/app-version.tsx +++ b/apps/client/src/components/settings/app-version.tsx @@ -50,7 +50,7 @@ export default function AppVersion() { href="https://github.com/docmost/docmost/releases" target="_blank" > - v{APP_VERSION} + {appVersion?.currentVersion && <>v{appVersion?.currentVersion}} diff --git a/apps/client/src/components/settings/settings-queries.tsx b/apps/client/src/components/settings/settings-queries.tsx index 2f3b46bd..bc528466 100644 --- a/apps/client/src/components/settings/settings-queries.tsx +++ b/apps/client/src/components/settings/settings-queries.tsx @@ -10,6 +10,7 @@ import { getWorkspaceMembers } from "@/features/workspace/services/workspace-ser import { getLicenseInfo } from "@/ee/licence/services/license-service.ts"; import { getSsoProviders } from "@/ee/security/services/security-service.ts"; import { getShares } from "@/features/share/services/share-service.ts"; +import { getApiKeys } from "@/ee/api-key"; export const prefetchWorkspaceMembers = () => { const params = { limit: 100, page: 1, query: "" } as QueryParams; @@ -65,3 +66,17 @@ export const prefetchShares = () => { queryFn: () => getShares({ page: 1, limit: 100 }), }); }; + +export const prefetchApiKeys = () => { + queryClient.prefetchQuery({ + queryKey: ["api-key-list", { page: 1 }], + queryFn: () => getApiKeys({ page: 1 }), + }); +}; + +export const prefetchApiKeyManagement = () => { + queryClient.prefetchQuery({ + queryKey: ["api-key-list", { page: 1 }], + queryFn: () => getApiKeys({ page: 1, adminView: true }), + }); +}; diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx index fe0c7e88..75e5a7af 100644 --- a/apps/client/src/components/settings/settings-sidebar.tsx +++ b/apps/client/src/components/settings/settings-sidebar.tsx @@ -12,15 +12,18 @@ import { IconLock, IconKey, IconWorld, + IconSparkles, } from "@tabler/icons-react"; import { Link, useLocation } from "react-router-dom"; import classes from "./settings.module.css"; import { useTranslation } from "react-i18next"; import { isCloud } from "@/lib/config.ts"; import useUserRole from "@/hooks/use-user-role.tsx"; -import { useAtom } from "jotai/index"; +import { useAtom } from "jotai"; import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { + prefetchApiKeyManagement, + prefetchApiKeys, prefetchBilling, prefetchGroups, prefetchLicense, @@ -60,6 +63,14 @@ const groupedData: DataGroup[] = [ icon: IconBrush, path: "/settings/account/preferences", }, + { + label: "API keys", + icon: IconKey, + path: "/settings/account/api-keys", + isCloud: true, + isEnterprise: true, + showDisabledInNonEE: true, + }, ], }, { @@ -90,6 +101,22 @@ const groupedData: DataGroup[] = [ { label: "Groups", icon: IconUsersGroup, path: "/settings/groups" }, { label: "Spaces", icon: IconSpaces, path: "/settings/spaces" }, { label: "Public sharing", icon: IconWorld, path: "/settings/sharing" }, + { + label: "API management", + icon: IconKey, + path: "/settings/api-keys", + isCloud: true, + isEnterprise: true, + isAdmin: true, + showDisabledInNonEE: true, + }, + { + label: "AI settings", + icon: IconSparkles, + path: "/settings/ai", + isAdmin: true, + isSelfhosted: true, + }, ], }, { @@ -195,6 +222,12 @@ export default function SettingsSidebar() { case "Public sharing": prefetchHandler = prefetchShares; break; + case "API keys": + prefetchHandler = prefetchApiKeys; + break; + case "API management": + prefetchHandler = prefetchApiKeyManagement; + break; default: break; } diff --git a/apps/client/src/components/ui/custom-avatar.tsx b/apps/client/src/components/ui/custom-avatar.tsx index de4456e5..54730127 100644 --- a/apps/client/src/components/ui/custom-avatar.tsx +++ b/apps/client/src/components/ui/custom-avatar.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Avatar } from "@mantine/core"; import { getAvatarUrl } from "@/lib/config.ts"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; interface CustomAvatarProps { avatarUrl: string; @@ -11,13 +12,15 @@ interface CustomAvatarProps { variant?: string; style?: any; component?: any; + type?: AvatarIconType; + mt?: string | number; } export const CustomAvatar = React.forwardRef< HTMLInputElement, CustomAvatarProps ->(({ avatarUrl, name, ...props }: CustomAvatarProps, ref) => { - const avatarLink = getAvatarUrl(avatarUrl); +>(({ avatarUrl, name, type, ...props }: CustomAvatarProps, ref) => { + const avatarLink = getAvatarUrl(avatarUrl, type); return ( 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/components/ui/responsive-settings-row.tsx b/apps/client/src/components/ui/responsive-settings-row.tsx new file mode 100644 index 00000000..ec3f65f7 --- /dev/null +++ b/apps/client/src/components/ui/responsive-settings-row.tsx @@ -0,0 +1,47 @@ +import { Box } from "@mantine/core"; +import React from "react"; + +interface ResponsiveSettingsRowProps { + children: React.ReactNode; +} + +export function ResponsiveSettingsRow({ children }: ResponsiveSettingsRowProps) { + return ( + + {children} + + ); +} + +interface ResponsiveSettingsContentProps { + children: React.ReactNode; +} + +export function ResponsiveSettingsContent({ children }: ResponsiveSettingsContentProps) { + return ( + + {children} + + ); +} + +interface ResponsiveSettingsControlProps { + children: React.ReactNode; +} + +export function ResponsiveSettingsControl({ children }: ResponsiveSettingsControlProps) { + return ( + + {children} + + ); +} diff --git a/apps/client/src/ee/ai/components/ai-search-result.tsx b/apps/client/src/ee/ai/components/ai-search-result.tsx new file mode 100644 index 00000000..f082f25a --- /dev/null +++ b/apps/client/src/ee/ai/components/ai-search-result.tsx @@ -0,0 +1,113 @@ +import React, { useMemo } from "react"; +import { Paper, Text, Group, Stack, Loader, Box } from "@mantine/core"; +import { IconSparkles, IconFileText } from "@tabler/icons-react"; +import { Link } from "react-router-dom"; +import { IAiSearchResponse } from "../services/ai-search-service.ts"; +import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { markdownToHtml } from "@docmost/editor-ext"; +import DOMPurify from "dompurify"; +import { useTranslation } from "react-i18next"; + +interface AiSearchResultProps { + result?: IAiSearchResponse; + isLoading?: boolean; + streamingAnswer?: string; + streamingSources?: any[]; +} + +export function AiSearchResult({ + result, + isLoading, + streamingAnswer = "", + streamingSources = [], +}: AiSearchResultProps) { + const { t } = useTranslation(); + + // Use streaming data if available, otherwise fall back to result + const answer = streamingAnswer || result?.answer || ""; + const sources = + streamingSources.length > 0 ? streamingSources : result?.sources || []; + + // Deduplicate sources by pageId, keeping the one with highest similarity + const deduplicatedSources = useMemo(() => { + if (!sources || sources.length === 0) return []; + + const pageMap = new Map(); + sources.forEach((source) => { + const existing = pageMap.get(source.pageId); + if (!existing || source.similarity > existing.similarity) { + pageMap.set(source.pageId, source); + } + }); + + return Array.from(pageMap.values()); + }, [sources]); + + if (isLoading && !answer) { + return ( + + + + {t("AI is thinking...")} + + + ); + } + + if (!answer && !isLoading) { + return null; + } + + return ( + + + + + + {t("AI Answer")} + + {isLoading && } + +
+ + + {deduplicatedSources.length > 0 && ( + + + {t("Sources")} + + {deduplicatedSources.map((source) => ( + + + + + + {source.title} + + + + + ))} + + )} + + ); +} diff --git a/apps/client/src/ee/ai/components/enable-ai-search.tsx b/apps/client/src/ee/ai/components/enable-ai-search.tsx new file mode 100644 index 00000000..53b0a9bd --- /dev/null +++ b/apps/client/src/ee/ai/components/enable-ai-search.tsx @@ -0,0 +1,69 @@ +import { Group, Text, Switch, MantineSize, Title } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { isCloud } from "@/lib/config.ts"; +import useLicense from "@/ee/hooks/use-license.tsx"; + +export default function EnableAiSearch() { + const { t } = useTranslation(); + + return ( + <> + +
+ {t("AI-powered search (Ask AI)")} + + {t( + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.", + )} + +
+ + +
+ + ); +} + +interface AiSearchToggleProps { + size?: MantineSize; + label?: string; +} +export function AiSearchToggle({ size, label }: AiSearchToggleProps) { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState(workspace?.settings?.ai?.search); + const { hasLicenseKey } = useLicense(); + + const hasAccess = isCloud() || (!isCloud() && hasLicenseKey); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ aiSearch: value }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + ); +} diff --git a/apps/client/src/ee/ai/hooks/use-ai-search.ts b/apps/client/src/ee/ai/hooks/use-ai-search.ts new file mode 100644 index 00000000..f9c5aa88 --- /dev/null +++ b/apps/client/src/ee/ai/hooks/use-ai-search.ts @@ -0,0 +1,46 @@ +import { useMutation, UseMutationResult } from "@tanstack/react-query"; +import { useState, useCallback } from "react"; +import { askAi, IAiSearchResponse } from "@/ee/ai/services/ai-search-service.ts"; +import { IPageSearchParams } from "@/features/search/types/search.types.ts"; + +// @ts-ignore +interface UseAiSearchResult extends UseMutationResult { + streamingAnswer: string; + streamingSources: any[]; + clearStreaming: () => void; +} + +export function useAiSearch(): UseAiSearchResult { + const [streamingAnswer, setStreamingAnswer] = useState(""); + const [streamingSources, setStreamingSources] = useState([]); + + const clearStreaming = useCallback(() => { + setStreamingAnswer(""); + setStreamingSources([]); + }, []); + + const mutation = useMutation({ + mutationFn: async (params: IPageSearchParams & { contentType?: string }) => { + setStreamingAnswer(""); + setStreamingSources([]); + + const { contentType, ...apiParams } = params; + + return await askAi(apiParams, (chunk) => { + if (chunk.content) { + setStreamingAnswer((prev) => prev + chunk.content); + } + if (chunk.sources) { + setStreamingSources(chunk.sources); + } + }); + }, + }); + + return { + ...mutation, + streamingAnswer, + streamingSources, + clearStreaming, + }; +} diff --git a/apps/client/src/ee/ai/hooks/use-ai.ts b/apps/client/src/ee/ai/hooks/use-ai.ts new file mode 100644 index 00000000..40c1ca12 --- /dev/null +++ b/apps/client/src/ee/ai/hooks/use-ai.ts @@ -0,0 +1,61 @@ +import { useState, useCallback, useRef } from "react"; +import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts"; +import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts"; + +export function useAiStream() { + const [content, setContent] = useState(""); + const [isStreaming, setIsStreaming] = useState(false); + const abortControllerRef = useRef(null); + const mutation = useAiGenerateStreamMutation(); + + const startStream = useCallback( + async (data: AiGenerateDto) => { + setContent(""); + setIsStreaming(true); + + try { + const controller = await mutation.mutateAsync({ + ...data, + onChunk: (chunk) => { + setContent((prev) => prev + chunk.content); + }, + onError: (error) => { + console.error("AI stream error:", error); + setIsStreaming(false); + }, + onComplete: () => { + setIsStreaming(false); + }, + }); + + abortControllerRef.current = controller; + } catch (error) { + console.error("Failed to start stream:", error); + setIsStreaming(false); + } + }, + [mutation] + ); + + const stopStream = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + setIsStreaming(false); + } + }, []); + + const resetContent = useCallback(() => { + setContent(""); + }, []); + + return { + content, + isStreaming, + startStream, + stopStream, + resetContent, + isLoading: mutation.isPending, + error: mutation.error, + }; +} \ No newline at end of file diff --git a/apps/client/src/ee/ai/pages/ai-settings.tsx b/apps/client/src/ee/ai/pages/ai-settings.tsx new file mode 100644 index 00000000..b9ab516d --- /dev/null +++ b/apps/client/src/ee/ai/pages/ai-settings.tsx @@ -0,0 +1,46 @@ +import { Helmet } from "react-helmet-async"; +import { getAppName, isCloud } from "@/lib/config.ts"; +import SettingsTitle from "@/components/settings/settings-title.tsx"; +import React from "react"; +import useUserRole from "@/hooks/use-user-role.tsx"; +import { useTranslation } from "react-i18next"; +import useLicense from "@/ee/hooks/use-license.tsx"; +import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx"; +import { Alert } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; + +export default function AiSettings() { + const { t } = useTranslation(); + const { isAdmin } = useUserRole(); + const { hasLicenseKey } = useLicense(); + + if (!isAdmin) { + return null; + } + + const hasAccess = isCloud() || (!isCloud() && hasLicenseKey); + + return ( + <> + + AI - {getAppName()} + + + + {!hasAccess && ( + } + title={t("Enterprise feature")} + color="blue" + mb="lg" + > + {t( + "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.", + )} + + )} + + + + ); +} diff --git a/apps/client/src/ee/ai/queries/ai-query.ts b/apps/client/src/ee/ai/queries/ai-query.ts new file mode 100644 index 00000000..076de9c7 --- /dev/null +++ b/apps/client/src/ee/ai/queries/ai-query.ts @@ -0,0 +1,44 @@ +import { + useMutation, + UseMutationResult, + useQuery, + UseQueryResult, +} from "@tanstack/react-query"; +import { + generateAiContent, + generateAiContentStream, +} from "@/ee/ai/services/ai-service.ts"; +import { + AiConfigResponse, + AiContentResponse, + AiGenerateDto, + AiStreamChunk, + AiStreamError, +} from "@/ee/ai/types/ai.types.ts"; + +export function useAiGenerateMutation(): UseMutationResult< + AiContentResponse, + Error, + AiGenerateDto +> { + return useMutation({ + mutationFn: (data: AiGenerateDto) => generateAiContent(data), + }); +} + +interface StreamCallbacks { + onChunk: (chunk: AiStreamChunk) => void; + onError?: (error: AiStreamError) => void; + onComplete?: () => void; +} + +export function useAiGenerateStreamMutation(): UseMutationResult< + AbortController, + Error, + AiGenerateDto & StreamCallbacks +> { + return useMutation({ + mutationFn: ({ onChunk, onError, onComplete, ...data }) => + generateAiContentStream(data, onChunk, onError, onComplete), + }); +} diff --git a/apps/client/src/ee/ai/services/ai-search-service.ts b/apps/client/src/ee/ai/services/ai-search-service.ts new file mode 100644 index 00000000..759a104a --- /dev/null +++ b/apps/client/src/ee/ai/services/ai-search-service.ts @@ -0,0 +1,83 @@ +import api from "@/lib/api-client.ts"; +import { IPageSearchParams } from "@/features/search/types/search.types.ts"; + +export interface IAiSearchResponse { + answer: string; + sources?: Array<{ + pageId: string; + title: string; + slugId: string; + spaceSlug: string; + similarity: number; + distance: number; + chunkIndex: number; + excerpt: string; + }>; +} + +export async function askAi( + params: IPageSearchParams, + onChunk?: (chunk: { content?: string; sources?: any[] }) => void, +): Promise { + const response = await fetch("/api/ai/ask", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + let answer = ""; + let sources: any[] = []; + let buffer = ""; + + if (reader) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + + // Keep the last incomplete line in the buffer + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") break; + + try { + const parsed = JSON.parse(data); + if (parsed.error) { + throw new Error(parsed.error); + } + if (parsed.content) { + answer += parsed.content; + onChunk?.({ content: parsed.content }); + } + if (parsed.sources) { + sources = parsed.sources; + onChunk?.({ sources: parsed.sources }); + } + } catch (e) { + if (e instanceof Error) { + throw e; + } + // Skip invalid JSON + } + } + } + } + } + + return { answer, sources }; +} diff --git a/apps/client/src/ee/ai/services/ai-service.ts b/apps/client/src/ee/ai/services/ai-service.ts new file mode 100644 index 00000000..f3634d59 --- /dev/null +++ b/apps/client/src/ee/ai/services/ai-service.ts @@ -0,0 +1,89 @@ +import api from "@/lib/api-client.ts"; +import { + AiGenerateDto, + AiContentResponse, + AiStreamChunk, + AiStreamError, +} from "@/ee/ai/types/ai.types.ts"; + +export async function generateAiContent( + data: AiGenerateDto, +): Promise { + const req = await api.post("/ai/generate", data); + return req.data; +} + +export async function generateAiContentStream( + data: AiGenerateDto, + onChunk: (chunk: AiStreamChunk) => void, + onError?: (error: AiStreamError) => void, + onComplete?: () => void, +): Promise { + const abortController = new AbortController(); + try { + const response = await fetch("/api/ai/generate/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + signal: abortController.signal, + credentials: "include", // This ensures cookies are sent, matching axios withCredentials + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error("Response body is not readable"); + } + + const processStream = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") { + onComplete?.(); + return; + } + try { + const parsed = JSON.parse(data); + if (parsed.error) { + onError?.(parsed); + } else { + onChunk(parsed); + } + } catch (e) { + // Ignore parse errors for incomplete chunks + } + } + } + } + } catch (error) { + if (error.name !== "AbortError") { + onError?.({ error: error.message }); + } + } finally { + reader.releaseLock(); + } + }; + + processStream(); + } catch (error) { + onError?.({ error: error.message }); + } + + return abortController; +} diff --git a/apps/client/src/ee/ai/types/ai.types.ts b/apps/client/src/ee/ai/types/ai.types.ts new file mode 100644 index 00000000..a5fbc253 --- /dev/null +++ b/apps/client/src/ee/ai/types/ai.types.ts @@ -0,0 +1,40 @@ +export enum AiAction { + IMPROVE_WRITING = "improve_writing", + FIX_SPELLING_GRAMMAR = "fix_spelling_grammar", + MAKE_SHORTER = "make_shorter", + MAKE_LONGER = "make_longer", + SIMPLIFY = "simplify", + CHANGE_TONE = "change_tone", + SUMMARIZE = "summarize", + CONTINUE_WRITING = "continue_writing", + TRANSLATE = "translate", + CUSTOM = "custom", +} + +export interface AiGenerateDto { + action?: AiAction; + content: string; + prompt?: string; +} + +export interface AiContentResponse { + content: string; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; +} + +export interface AiConfigResponse { + configured: boolean; + availableActions: AiAction[]; +} + +export interface AiStreamChunk { + content: string; +} + +export interface AiStreamError { + error: string; +} diff --git a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx new file mode 100644 index 00000000..6a01ee3c --- /dev/null +++ b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx @@ -0,0 +1,72 @@ +import { + Modal, + Text, + Stack, + Alert, + Group, + Button, + TextInput, +} from "@mantine/core"; +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { IApiKey } from "@/ee/api-key"; +import CopyTextButton from "@/components/common/copy.tsx"; + +interface ApiKeyCreatedModalProps { + opened: boolean; + onClose: () => void; + apiKey: IApiKey; +} + +export function ApiKeyCreatedModal({ + opened, + onClose, + apiKey, +}: ApiKeyCreatedModalProps) { + const { t } = useTranslation(); + + if (!apiKey) return null; + + return ( + + + } + title={t("Important")} + color="red" + > + {t( + "Make sure to copy your API key now. You won't be able to see it again!", + )} + + +
+ + {t("API key")} + + + + + + +
+ + +
+
+ ); +} diff --git a/apps/client/src/ee/api-key/components/api-key-table.tsx b/apps/client/src/ee/api-key/components/api-key-table.tsx new file mode 100644 index 00000000..48757acc --- /dev/null +++ b/apps/client/src/ee/api-key/components/api-key-table.tsx @@ -0,0 +1,143 @@ +import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core"; +import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react"; +import { format } from "date-fns"; +import { useTranslation } from "react-i18next"; +import { IApiKey } from "@/ee/api-key"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import React from "react"; +import NoTableResults from "@/components/common/no-table-results"; + +interface ApiKeyTableProps { + apiKeys: IApiKey[]; + isLoading?: boolean; + showUserColumn?: boolean; + onUpdate?: (apiKey: IApiKey) => void; + onRevoke?: (apiKey: IApiKey) => void; +} + +export function ApiKeyTable({ + apiKeys, + isLoading, + showUserColumn = false, + onUpdate, + onRevoke, +}: ApiKeyTableProps) { + const { t } = useTranslation(); + + const formatDate = (date: Date | string | null) => { + if (!date) return t("Never"); + return format(new Date(date), "MMM dd, yyyy"); + }; + + const isExpired = (expiresAt: string | null) => { + if (!expiresAt) return false; + return new Date(expiresAt) < new Date(); + }; + + return ( + + + + + {t("Name")} + {showUserColumn && {t("User")}} + {t("Last used")} + {t("Expires")} + {t("Created")} + + + + + + {apiKeys && apiKeys.length > 0 ? ( + apiKeys.map((apiKey: IApiKey, index: number) => ( + + + + {apiKey.name} + + + + {showUserColumn && apiKey.creator && ( + + + + + {apiKey.creator.name} + + + + )} + + + + {formatDate(apiKey.lastUsedAt)} + + + + + {apiKey.expiresAt ? ( + isExpired(apiKey.expiresAt) ? ( + + {t("Expired")} + + ) : ( + + {formatDate(apiKey.expiresAt)} + + ) + ) : ( + + {t("Never")} + + )} + + + + + {formatDate(apiKey.createdAt)} + + + + + + + + + + + + {onUpdate && ( + } + onClick={() => onUpdate(apiKey)} + > + {t("Rename")} + + )} + {onRevoke && ( + } + color="red" + onClick={() => onRevoke(apiKey)} + > + {t("Revoke")} + + )} + + + + + )) + ) : ( + + )} + +
+
+ ); +} diff --git a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx new file mode 100644 index 00000000..cade36e8 --- /dev/null +++ b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx @@ -0,0 +1,153 @@ +import { lazy, Suspense, useState } from "react"; +import { Modal, TextInput, Button, Group, Stack, Select } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zodResolver } from "mantine-form-zod-resolver"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useCreateApiKeyMutation } from "@/ee/api-key/queries/api-key-query"; +import { IconCalendar } from "@tabler/icons-react"; +import { IApiKey } from "@/ee/api-key"; + +const DateInput = lazy(() => + import("@mantine/dates").then((module) => ({ + default: module.DateInput, + })), +); + +interface CreateApiKeyModalProps { + opened: boolean; + onClose: () => void; + onSuccess: (response: IApiKey) => void; +} + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + expiresAt: z.string().optional(), +}); +type FormValues = z.infer; + +export function CreateApiKeyModal({ + opened, + onClose, + onSuccess, +}: CreateApiKeyModalProps) { + const { t } = useTranslation(); + const [expirationOption, setExpirationOption] = useState("30"); + const createApiKeyMutation = useCreateApiKeyMutation(); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + name: "", + expiresAt: "", + }, + }); + + const getExpirationDate = (): string | undefined => { + if (expirationOption === "never") { + return undefined; + } + if (expirationOption === "custom") { + return form.values.expiresAt; + } + const days = parseInt(expirationOption); + const date = new Date(); + date.setDate(date.getDate() + days); + return date.toISOString(); + }; + + const getExpirationLabel = (days: number) => { + const date = new Date(); + date.setDate(date.getDate() + days); + const formatted = date.toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + return `${days} days (${formatted})`; + }; + + const expirationOptions = [ + { value: "30", label: getExpirationLabel(30) }, + { value: "60", label: getExpirationLabel(60) }, + { value: "90", label: getExpirationLabel(90) }, + { value: "365", label: getExpirationLabel(365) }, + { value: "custom", label: t("Custom") }, + { value: "never", label: t("No expiration") }, + ]; + + const handleSubmit = async (data: { + name?: string; + expiresAt?: string | Date; + }) => { + const groupData = { + name: data.name, + expiresAt: getExpirationDate(), + }; + + try { + const createdKey = await createApiKeyMutation.mutateAsync(groupData); + onSuccess(createdKey); + form.reset(); + onClose(); + } catch (err) { + // + } + }; + + const handleClose = () => { + form.reset(); + setExpirationOption("30"); + onClose(); + }; + + return ( + +
handleSubmit(values))}> + + + +