diff --git a/.dockerignore b/.dockerignore index 6528bce76..f828e1f65 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ -node_modules -.git -dist +**/node_modules +**/.git +**/dist /data -.env* -.nx +**/.env* +**/.nx diff --git a/.env.example b/.env.example index b218bdb84..6c5756fad 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ JWT_TOKEN_EXPIRES_IN=30d DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public" REDIS_URL=redis://127.0.0.1:6379 -# options: local | s3 +# options: local | s3 | azure STORAGE_DRIVER=local # S3 driver config @@ -21,6 +21,11 @@ AWS_S3_BUCKET= AWS_S3_ENDPOINT= AWS_S3_FORCE_PATH_STYLE= +# Azure Blob Storage driver config +AZURE_STORAGE_ACCOUNT_NAME= +AZURE_STORAGE_ACCOUNT_KEY= +AZURE_STORAGE_CONTAINER= + # default: 50mb FILE_UPLOAD_SIZE_LIMIT= @@ -48,6 +53,13 @@ GOTENBERG_URL= DISABLE_TELEMETRY=false +# Allow other sites to embed Docmost in an iframe. +IFRAME_EMBED_ALLOWED=false + +# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed. +# Example: https://intranet.example.com,https://portal.example.com +IFRAME_ALLOWED_ORIGINS= + # Enable debug logging in production (default: false) DEBUG_MODE=false diff --git a/.gitignore b/.gitignore index 862c9e2c8..883b23ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .env.prod data # compiled output -/dist +dist /node_modules # Logs diff --git a/Dockerfile b/Dockerfile index d665e254b..85d39981e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,8 @@ COPY --from=builder /app/apps/server/package.json /app/apps/server/package.json # Copy packages COPY --from=builder /app/packages/editor-ext/dist /app/packages/editor-ext/dist COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-ext/package.json +COPY --from=builder /app/packages/base-formula/dist /app/packages/base-formula/dist +COPY --from=builder /app/packages/base-formula/package.json /app/packages/base-formula/package.json # Copy root package files COPY --from=builder /app/package.json /app/package.json diff --git a/apps/client/package.json b/apps/client/package.json index c769f6090..46c076520 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.80.1", + "version": "0.90.1", "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -18,21 +18,23 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0", "@atlaskit/pragmatic-drag-and-drop-live-region": "1.3.4", "@casl/react": "5.0.1", + "@docmost/base-formula": "workspace:*", "@docmost/editor-ext": "workspace:*", "@excalidraw/excalidraw": "0.18.0-3a5ef40", - "@mantine/core": "8.3.18", - "@mantine/dates": "8.3.18", - "@mantine/form": "8.3.18", - "@mantine/hooks": "8.3.18", - "@mantine/modals": "8.3.18", - "@mantine/notifications": "8.3.18", - "@mantine/spotlight": "8.3.18", + "@mantine/core": "9.3.2", + "@mantine/dates": "9.3.2", + "@mantine/form": "9.3.2", + "@mantine/hooks": "9.3.2", + "@mantine/modals": "9.3.2", + "@mantine/notifications": "9.3.2", + "@mantine/spotlight": "9.3.2", "@slidoapp/emoji-mart": "5.8.7", "@slidoapp/emoji-mart-data": "1.2.4", "@slidoapp/emoji-mart-react": "1.1.5", "@tabler/icons-react": "3.40.0", "@tanstack/react-query": "5.90.17", - "@tanstack/react-virtual": "3.13.24", + "@tanstack/react-table": "8.21.3", + "@tanstack/react-virtual": "3.14.3", "alfaaz": "1.1.0", "axios": "1.16.0", "blueimp-load-image": "5.16.0", @@ -41,24 +43,25 @@ "highlightjs-sap-abap": "0.3.0", "i18next": "25.10.1", "i18next-http-backend": "3.0.6", - "jotai": "2.18.1", + "jotai": "2.20.1", "jotai-optics": "0.4.0", - "js-cookie": "3.0.5", + "js-cookie": "3.0.7", "jwt-decode": "4.0.0", "katex": "0.16.40", "lowlight": "3.3.0", "mantine-form-zod-resolver": "1.3.0", "mermaid": "11.15.0", "mitt": "3.0.1", - "posthog-js": "1.372.2", - "react": "18.3.1", + "nanoid": "3.3.8", + "posthog-js": "1.391.2", + "react": "19.2.7", "react-clear-modal": "^2.0.18", - "react-dom": "^18.3.1", + "react-dom": "19.2.7", "react-drawio": "1.0.7", "react-error-boundary": "6.1.1", "react-helmet-async": "3.0.0", "react-i18next": "16.5.8", - "react-router-dom": "7.13.1", + "react-router-dom": "7.18.0", "semver": "7.7.4", "socket.io-client": "4.8.3", "zod": "4.3.6" @@ -73,8 +76,8 @@ "@types/js-cookie": "3.0.6", "@types/katex": "0.16.8", "@types/node": "22.19.1", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.1", "eslint": "9.28.0", "eslint-plugin-react": "7.37.5", @@ -89,7 +92,7 @@ "prettier": "3.8.1", "typescript": "5.9.3", "typescript-eslint": "8.57.1", - "vite": "8.0.5", + "vite": "8.0.16", "vitest": "4.1.6" } } diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index 5c9e2d44c..0368ad429 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -71,6 +71,7 @@ "Export": "Exportieren", "Failed to create page": "Erstellung der Seite fehlgeschlagen", "Failed to delete page": "Löschen der Seite fehlgeschlagen", + "Failed to restore page": "Seite konnte nicht wiederhergestellt werden", "Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten", "Failed to import pages": "Import der Seiten fehlgeschlagen", "Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.", @@ -111,7 +112,7 @@ "Member": "Mitglied", "members": "Mitglieder", "Members": "Mitglieder", - "My preferences": "Meine Voreinstellungen", + "My preferences": "Meine Einstellungen", "My Profile": "Mein Profil", "My profile": "Mein Profil", "Name": "Name", @@ -139,7 +140,7 @@ "People": "Personen", "Pending": "Ausstehend", "Please confirm your action": "Bitte bestätigen Sie Ihre Aktion", - "Preferences": "Vorlieben", + "Preferences": "Einstellungen", "Print PDF": "PDF drucken", "Profile": "Profil", "Recently updated": "Kürzlich aktualisiert", @@ -276,6 +277,9 @@ "Align left": "Links ausrichten", "Align right": "Rechts ausrichten", "Align center": "Zentrieren", + "Alt text": "Alternativtext", + "Describe this for accessibility.": "Beschreiben Sie dies für die Barrierefreiheit.", + "Add a description": "Beschreibung hinzufügen", "Justify": "Blocksatz", "Merge cells": "Zellen zusammenführen", "Split cell": "Zelle teilen", @@ -286,6 +290,19 @@ "Add row above": "Zeile oben hinzufügen", "Add row below": "Zeile unten hinzufügen", "Delete table": "Tabelle löschen", + "Add column left": "Spalte links hinzufügen", + "Add column right": "Spalte rechts hinzufügen", + "Clear cell": "Zelle leeren", + "Clear cells": "Zellen leeren", + "Toggle header cell": "Kopfzelle umschalten", + "Toggle header column": "Kopfspalte umschalten", + "Toggle header row": "Kopfzeile umschalten", + "Move column left": "Spalte nach links verschieben", + "Move column right": "Spalte nach rechts verschieben", + "Move row down": "Zeile nach unten verschieben", + "Move row up": "Zeile nach oben verschieben", + "Sort A → Z": "A → Z sortieren", + "Sort Z → A": "Z → A sortieren", "Info": "Info", "Note": "Hinweis", "Success": "Erfolg", @@ -348,6 +365,8 @@ "Create block quote.": "Erstellen Sie ein Blockzitat.", "Insert code snippet.": "Code-Snippet einfügen.", "Insert horizontal rule divider": "Horizontale Trennlinie einfügen", + "Page break": "Seitenumbruch", + "Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.", "Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.", "Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.", @@ -392,6 +411,10 @@ "Write...": "\"Schreiben...\"", "Column count": "Spaltenanzahl", "{{count}} Columns": "{{count}} Spalten", + "{{count}} command available_one": "1 Befehl verfügbar", + "{{count}} command available_other": "{{count}} Befehle verfügbar", + "{{count}} result available_one": "1 Ergebnis verfügbar", + "{{count}} result available_other": "{{count}} Ergebnisse verfügbar", "Equal columns": "Gleich breite Spalten", "Left sidebar": "Linke Seitenleiste", "Right sidebar": "Rechte Seitenleiste", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} ist verfügbar", "Default page edit mode": "Standard-Bearbeitungsmodus für Seiten", "Choose your preferred page edit mode. Avoid accidental edits.": "Wählen Sie Ihren bevorzugten Seitenbearbeitungsmodus. Vermeiden Sie versehentliche Bearbeitungen.", + "Choose {{format}} file": "{{format}}-Datei auswählen", "Reading": "Lesen", "Delete member": "Mitglied löschen", "Member deleted successfully": "Mitglied erfolgreich gelöscht", @@ -565,6 +589,8 @@ "Move to trash": "In den Papierkorb verschieben", "Move this page to trash?": "Diese Seite in den Papierkorb verschieben?", "Restore page": "Seite wiederherstellen", + "Permanently delete": "Endgültig löschen", + "{{name}} moved this page to Trash {{time}}.": "{{name}} hat diese Seite {{time}} in den Papierkorb verschoben.", "Page moved to trash": "Seite in den Papierkorb verschoben", "Page restored successfully": "Seite erfolgreich wiederhergestellt", "Deleted by": "Gelöscht von", @@ -608,25 +634,21 @@ "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", + "Update": "Aktualisieren", + "Update {{credential}}": "{{credential}} aktualisieren", "Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich", "Restrict API key creation to admins": "API-Schlüsselerstellung auf Administratoren beschränken", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Nur Administratoren und Eigentümer können neue API-Schlüssel erstellen. Bestehende Mitgliederschlüssel funktionieren weiterhin.", @@ -858,9 +880,12 @@ "AI Chat": "KI-Chat", "Analyze for insights": "Für Erkenntnisse analysieren", "Ask anything...": "Fragen Sie irgendetwas...", + "Assistant said:": "Assistent sagte:", "Chat history": "Chatverlauf", "Chat name": "Chatname", + "Chat transcript": "Chatprotokoll", "Close": "Schließen", + "Copy assistant response": "Antwort des Assistenten kopieren", "Docmost AI": "Docmost KI", "Failed to load chat. An error occurred.": "Chat konnte nicht geladen werden. Ein Fehler ist aufgetreten.", "Failed to render this message.": "Diese Nachricht konnte nicht dargestellt werden.", @@ -870,9 +895,17 @@ "No chats found": "Keine Chats gefunden", "No conversations yet": "Noch keine Unterhaltungen", "Open full page": "Ganze Seite öffnen", + "Scroll to bottom": "Nach unten scrollen", + "You said:": "Sie sagten:", "Previous 7 days": "Letzte 7 Tage", "Previous 30 days": "Letzte 30 Tage", "Search chats...": "Chats durchsuchen...", + "Search chats": "Chats durchsuchen", + "Ask anything... Use @ to mention pages": "Frag etwas ... Verwende @, um Seiten zu erwähnen", + "Ask anything or search your workspace": "Fragen Sie etwas oder durchsuchen Sie Ihren Workspace", + "Welcome to {{name}}": "Willkommen bei {{name}}", + "Add files": "Dateien hinzufügen", + "Mention a page": "Eine Seite einfügen", "Start a new chat to see it here.": "Starten Sie einen neuen Chat, damit er hier angezeigt wird.", "Summarize this page": "Diese Seite zusammenfassen", "Toggle AI Chat": "KI-Chat umschalten", @@ -880,5 +913,176 @@ "Try a different search term.": "Versuchen Sie einen anderen Suchbegriff.", "Try again": "Erneut versuchen", "Untitled chat": "Chat ohne Titel", - "What can I help you with?": "Womit kann ich Ihnen helfen?" + "What can I help you with?": "Womit kann ich Ihnen helfen?", + "Are you sure you want to revoke this {{credential}}": "Sind Sie sicher, dass Sie diese(n) {{credential}} widerrufen möchten?", + "Automatically provision users and groups from your identity provider via SCIM.": "Stellen Sie Benutzer und Gruppen automatisch über SCIM von Ihrem Identitätsanbieter bereit.", + "Configure your identity provider with this URL to provision users and groups.": "Konfigurieren Sie Ihren Identitätsanbieter mit dieser URL, um Benutzer und Gruppen bereitzustellen.", + "Create {{credential}}": "{{credential}} erstellen", + "{{credential}} created": "{{credential}} erstellt", + "{{credential}} created successfully": "{{credential}} erfolgreich erstellt", + "Created by": "Erstellt von", + "Custom": "Benutzerdefiniert", + "Enable SCIM": "SCIM aktivieren", + "Enter a descriptive name": "Geben Sie einen beschreibenden Namen ein", + "I've saved my {{credential}}": "Ich habe meine(n) {{credential}} gespeichert", + "Important": "Wichtig", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Stellen Sie sicher, dass Sie Ihre(n) {{credential}} jetzt kopieren. Sie können sie/ihn später nicht erneut anzeigen!", + "Never": "Nie", + "Revoke {{credential}}": "{{credential}} widerrufen", + "SCIM endpoint URL": "SCIM-Endpunkt-URL", + "SCIM provisioning": "SCIM-Bereitstellung", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM hat Vorrang vor der SSO-Gruppensynchronisierung, solange es aktiviert ist.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Sie haben die maximale Anzahl von {{max}} SCIM-Token erreicht. Löschen Sie ein vorhandenes Token, um ein neues zu erstellen.", + "SCIM token": "SCIM-Token", + "SCIM tokens": "SCIM-Token", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Diese Aktion kann nicht rückgängig gemacht werden. Ihr Identitätsanbieter wird die Synchronisierung sofort beenden.", + "Toggle SCIM provisioning": "SCIM-Bereitstellung umschalten", + "Token": "Token", + "Page menu": "Seitenmenü", + "Expand": "Erweitern", + "Collapse": "Reduzieren", + "Comment menu": "Kommentarmenü", + "Group menu": "Gruppenmenü", + "Show hidden breadcrumbs": "Ausgeblendete Breadcrumbs anzeigen", + "Breadcrumbs": "Navigationspfade", + "Page actions": "Seitenaktionen", + "Pick emoji": "Emoji auswählen", + "Template menu": "Vorlagenmenü", + "Use": "Verwenden", + "Use template": "Vorlage verwenden", + "Preview template: {{title}}": "Vorlage anzeigen: {{title}}", + "Use a template": "Eine Vorlage verwenden", + "Search templates...": "Vorlagen suchen...", + "Search spaces...": "Bereiche suchen...", + "No templates found": "Keine Vorlagen gefunden", + "No spaces found": "Keine Bereiche gefunden", + "Browse all templates": "Alle Vorlagen durchsuchen", + "This space": "Dieser Bereich", + "All templates": "Alle Vorlagen", + "Global": "Global", + "New template": "Neue Vorlage", + "Edit template": "Vorlage bearbeiten", + "Are you sure you want to delete this template?": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten?", + "Template scope updated": "Vorlagenbereich aktualisiert", + "Choose which space this template belongs to": "Wählen Sie den Bereich aus, zu dem diese Vorlage gehört", + "Scope": "Bereich", + "Select scope": "Bereich auswählen", + "Title": "Titel", + "Saving...": "Wird gespeichert...", + "Saved": "Gespeichert", + "Save failed. Retry": "Speichern fehlgeschlagen. Erneut versuchen", + "By {{name}}": "Von {{name}}", + "Updated {{time}}": "Aktualisiert {{time}}", + "Choose destination": "Ziel auswählen", + "Search pages and spaces...": "Seiten und Bereiche suchen...", + "No results found": "Keine Ergebnisse gefunden", + "You don't have permission to create pages here": "Sie haben hier keine Berechtigung, Seiten zu erstellen", + "Chat menu": "Chatmenü", + "API key menu": "API-Schlüssel-Menü", + "Jump to comment selection": "Zur Kommentarauswahl springen", + "Slash commands": "Slash-Befehle", + "Mention suggestions": "Erwähnungsvorschläge", + "Link suggestions": "Linkvorschläge", + "Diagram editor": "Diagrammeditor", + "Add comment": "Kommentar hinzufügen", + "Find and replace": "Suchen und ersetzen", + "Main navigation": "Hauptnavigation", + "Space navigation": "Bereichsnavigation", + "Settings navigation": "Einstellungsnavigation", + "AI navigation": "KI-Navigation", + "Breadcrumb": "Navigationspfad", + "Synced block": "Synchronisierter Block", + "Create a block that stays in sync across pages.": "Erstellt einen Block der über mehrere Seiten synchronisiert wird", + "Editing original": "Original bearbeiten", + "Copy synced block": "Synchronisierten Block kopieren", + "Unsync": "Synchronisierung aufheben", + "Delete synced block": "Synchronisierten Block löschen", + "Synced to {{count}} other page_one": "Mit {{count}} anderer Seite synchronisiert", + "Synced to {{count}} other page_other": "Mit {{count}} anderen Seiten synchronisiert", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "DIESE SEITE", + "No pages": "Keine Seiten", + "The original synced block no longer exists": "Der originale synchronisierte Block existiert nicht mehr", + "You don't have access to this synced block": "Sie haben keinen Zugriff auf diesen synchronisierten Block", + "Failed to load this synced block": "Dieser synchronisierte Block konnte nicht geladen werden", + "Fixed editor toolbar": "Fixierte Editor-Symbolleiste", + "Show a formatting toolbar above the editor with quick access to common actions.": "Anzeige einer Formatierungs-Symbolleiste über dem Editor für schnellen Zugriff auf Aktionen.", + "Toggle fixed editor toolbar": "Fixierte Editor-Symbolleiste ein/aus", + "Normal text": "Normaler Text", + "More inline formatting": "Weitere Formatierung", + "Subscript": "Tiefgestellt", + "Superscript": "Hochgestellt", + "Inline code": "Inline-Code", + "Insert media": "Medien einfügen", + "Mention": "Erwähnung", + "Emoji": "Emoji", + "Columns": "Spalten", + "More inserts": "Weiteren Inhalt einfügen", + "Embeds": "Einbettungen", + "Diagrams": "Diagramme", + "Advanced": "Erweitert", + "Utility": "Dienstprogramme", + "Decrease indent": "Einzug verkleinern", + "Increase indent": "Einzug vergrößern", + "Clear formatting": "Formatierung zurücksetzen", + "Code block": "Codeblock", + "Experimental": "Experimentell", + "Strikethrough": "Durchgestrichen", + "Undo": "Rückgängig", + "Redo": "Wiederholen", + "Backlinks": "Rückverweise", + "Last updated by": "Zuletzt aktualisiert von", + "Last updated": "Zuletzt aktualisiert", + "Stats": "Statistiken", + "Word count": "Wörter", + "Characters": "Zeichen", + "Incoming links": "Eingehende Links", + "Outgoing links": "Ausgehende Links", + "Incoming links ({{count}})": "Eingehende Links ({{count}})", + "Outgoing links ({{count}})": "Ausgehende Links ({{count}})", + "No pages link here yet.": "Aktuell verlinken keine Seiten hierher.", + "This page doesn't link to other pages yet.": "Diese Seite verlinkt noch nicht auf andere Seiten.", + "Verified until {{date}}": "Verifiziert bis zum {{date}}", + "Labels": "Beschriftungen", + "Add label": "Beschriftung hinzufügen", + "No labels yet": "Noch keine Beschriftungen", + "Already added": "Bereits hinzugefügt", + "Invalid label name": "Ungültiger Beschriftungsname", + "No matches": "Keine Treffer", + "Search or create…": "Suchen oder erstellen…", + "Remove label {{name}}": "Beschriftung {{name}} entfernen", + "Failed to add label": "Beschriftung konnte nicht hinzugefügt werden", + "Failed to remove label": "Beschriftung konnte nicht entfernt werden", + "No pages with this label": "Keine Seiten mit dieser Beschriftung", + "Pages tagged with this label will appear here.": "Hier werden Seiten angezeigt, die mit dieser Beschriftung versehen sind.", + "No pages match your search.": "Es konnten keine Seiten gefunden werden, die mit Ihrer Suche übereinstimmen.", + "Updated {{date}}": "Aktualisiert am {{date}}", + "Cell actions": "Zellaktionen", + "Column actions": "Spaltenaktionen", + "Row actions": "Zeilenaktionen", + "Filter": "Filter", + "Page title": "Seitentitel", + "Page content": "Seiteninhalt", + "Member actions": "Mitgliederaktionen", + "Toggle password visibility": "Passwortsichtbarkeit umschalten", + "Send comment": "Kommentar senden", + "Token actions": "Token-Aktionen", + "Template settings": "Vorlageneinstellungen", + "Edit diagram": "Diagramm bearbeiten", + "Edit embed": "Einbettung bearbeiten", + "Edit drawing": "Zeichnung bearbeiten", + "Delete equation": "Gleichung löschen", + "Invite actions": "Einladungsaktionen", + "Get started": "Erste Schritte", + "* indicates required fields": "* kennzeichnet Pflichtfelder", + "List of spaces in this workspace": "Liste der Bereiche in diesem Workspace", + "Active sessions": "Aktive Sitzungen", + "Add {{name}} to favorites": "{{name}} zu Favoriten hinzufügen", + "Remove {{name}} from favorites": "{{name}} aus Favoriten entfernen", + "Added to favorites": "Zu Favoriten hinzugefügt", + "Removed from favorites": "Aus Favoriten entfernt", + "Added {{name}} to favorites": "{{name}} zu Favoriten hinzugefügt", + "Removed {{name}} from favorites": "{{name}} aus Favoriten entfernt", + "Page menu for {{name}}": "Seitenmenü für {{name}}", + "Create subpage of {{name}}": "Unterseite von {{name}} erstellen" } diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index da4c9271a..efa361797 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -44,6 +44,8 @@ "Dark": "Dark", "Date": "Date", "Delete": "Delete", + "Remove from page": "Remove from page", + "Base options": "Base options", "Delete group": "Delete group", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.", "Description": "Description", @@ -83,6 +85,24 @@ "Failed to import pages": "Failed to import pages", "Failed to load page. An error occurred.": "Failed to load page. An error occurred.", "Failed to update data": "Failed to update data", + "Failed to create base": "Failed to create base", + "Failed to update base": "Failed to update base", + "Failed to delete base": "Failed to delete base", + "Failed to create property": "Failed to create property", + "Failed to update property": "Failed to update property", + "Failed to delete property": "Failed to delete property", + "Failed to reorder property": "Failed to reorder property", + "Failed to create view": "Failed to create view", + "Failed to update view": "Failed to update view", + "Failed to delete view": "Failed to delete view", + "Failed to create row": "Failed to create row", + "Failed to update row": "Failed to update row", + "Failed to delete row": "Failed to delete row", + "Failed to delete rows": "Failed to delete rows", + "Failed to reorder row": "Failed to reorder row", + "Failed to move card": "Failed to move card", + "Failed to add card": "Failed to add card", + "Failed to export CSV": "Failed to export CSV", "Favorite spaces": "Favorite spaces", "Favorite spaces appear here": "Favorite spaces appear here", "Favorites": "Favorites", @@ -293,6 +313,9 @@ "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", + "Alt text": "Alt text", + "Describe this for accessibility.": "Describe this for accessibility.", + "Add a description": "Add a description", "Justify": "Justify", "Merge cells": "Merge cells", "Split cell": "Split cell", @@ -411,6 +434,8 @@ "Insert mermaid diagram": "Insert mermaid diagram", "Insert and design Drawio diagrams": "Insert and design Drawio diagrams", "Insert current date": "Insert current date", + "Time": "Time", + "Insert current time": "Insert current time", "Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams", "Multiple": "Multiple", "Turn into": "Turn into", @@ -424,6 +449,10 @@ "Write...": "Write...", "Column count": "Column count", "{{count}} Columns": "{{count}} Columns", + "{{count}} command available_one": "1 command available", + "{{count}} command available_other": "{{count}} commands available", + "{{count}} result available_one": "1 result available", + "{{count}} result available_other": "{{count}} results available", "Equal columns": "Equal columns", "Left sidebar": "Left sidebar", "Right sidebar": "Right sidebar", @@ -433,6 +462,7 @@ "Names do not match": "Names do not match", "Today, {{time}}": "Today, {{time}}", "Yesterday, {{time}}": "Yesterday, {{time}}", + "now": "now", "Space created successfully": "Space created successfully", "Space updated successfully": "Space updated successfully", "Space deleted successfully": "Space deleted successfully", @@ -605,6 +635,8 @@ "Deleted by": "Deleted by", "Deleted at": "Deleted at", "Preview": "Preview", + "Base preview unavailable": "Base preview unavailable", + "Restore this base to view its contents.": "Restore this base to view its contents.", "Subpages": "Subpages", "Failed to load subpages": "Failed to load subpages", "No subpages": "No subpages", @@ -889,9 +921,12 @@ "AI Chat": "AI Chat", "Analyze for insights": "Analyze for insights", "Ask anything...": "Ask anything...", + "Assistant said:": "Assistant said:", "Chat history": "Chat history", "Chat name": "Chat name", + "Chat transcript": "Chat transcript", "Close": "Close", + "Copy assistant response": "Copy assistant response", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Failed to load chat. An error occurred.", "Failed to render this message.": "Failed to render this message.", @@ -901,6 +936,8 @@ "No chats found": "No chats found", "No conversations yet": "No conversations yet", "Open full page": "Open full page", + "Scroll to bottom": "Scroll to bottom", + "You said:": "You said:", "Previous 7 days": "Previous 7 days", "Previous 30 days": "Previous 30 days", "Search chats...": "Search chats...", @@ -952,7 +989,36 @@ "Page actions": "Page actions", "Pick emoji": "Pick emoji", "Template menu": "Template menu", - "Chat menu": "Chat menu", + "Use": "Use", + "Use template": "Use template", + "Preview template: {{title}}": "Preview template: {{title}}", + "Use a template": "Use a template", + "Search templates...": "Search templates...", + "Search spaces...": "Search spaces...", + "No templates found": "No templates found", + "No spaces found": "No spaces found", + "Browse all templates": "Browse all templates", + "This space": "This space", + "All templates": "All templates", + "Global": "Global", + "New template": "New template", + "Edit template": "Edit template", + "Are you sure you want to delete this template?": "Are you sure you want to delete this template?", + "Template scope updated": "Template scope updated", + "Choose which space this template belongs to": "Choose which space this template belongs to", + "Scope": "Scope", + "Select scope": "Select scope", + "Title": "Title", + "Saving...": "Saving...", + "Saved": "Saved", + "Save failed. Retry": "Save failed. Retry", + "By {{name}}": "By {{name}}", + "Updated {{time}}": "Updated {{time}}", + "Choose destination": "Choose destination", + "Search pages and spaces...": "Search pages and spaces...", + "No results found": "No results found", + "You don't have permission to create pages here": "You don't have permission to create pages here", + "Chat menu for {{title}}": "Chat menu for {{title}}", "API key menu": "API key menu", "Jump to comment selection": "Jump to comment selection", "Slash commands": "Slash commands", @@ -1034,5 +1100,72 @@ "Updated {{date}}": "Updated {{date}}", "Cell actions": "Cell actions", "Column actions": "Column actions", - "Row actions": "Row actions" + "Row actions": "Row actions", + "Filter": "Filter", + "Page title": "Page title", + "Page content": "Page content", + "Member actions for {{name}}": "Member actions for {{name}}", + "Toggle password visibility": "Toggle password visibility", + "Send comment": "Send comment", + "Token actions": "Token actions", + "Template settings": "Template settings", + "Edit diagram": "Edit diagram", + "Edit embed": "Edit embed", + "Edit drawing": "Edit drawing", + "Delete equation": "Delete equation", + "Invite actions": "Invite actions", + "Get started": "Get started", + "* indicates required fields": "* indicates required fields", + "List of spaces in this workspace": "List of spaces in this workspace", + "Active sessions": "Active sessions", + "Add {{name}} to favorites": "Add {{name}} to favorites", + "Remove {{name}} from favorites": "Remove {{name}} from favorites", + "Added to favorites": "Added to favorites", + "Removed from favorites": "Removed from favorites", + "Added {{name}} to favorites": "Added {{name}} to favorites", + "Removed {{name}} from favorites": "Removed {{name}} from favorites", + "Page menu for {{name}}": "Page menu for {{name}}", + "Create subpage of {{name}}": "Create subpage of {{name}}", + "Allow personal spaces": "Allow personal spaces", + "Members can create their own personal space.": "Members can create their own personal space.", + "Toggle allow personal spaces": "Toggle allow personal spaces", + "Create personal space": "Create personal space", + "Personal space": "Personal space", + "{{name}}'s space": "{{name}}'s space", + "Apply": "Apply", + "Cells that aren't already a page reference will be cleared.": "Cells that aren't already a page reference will be cleared.", + "Cells that aren't a valid URL will be cleared.": "Cells that aren't a valid URL will be cleared.", + "Cells that aren't a valid email address will be cleared.": "Cells that aren't a valid email address will be cleared.", + "Cells that can't be parsed as a date will be cleared.": "Cells that can't be parsed as a date will be cleared.", + "Cells that can't be parsed as a number will be cleared.": "Cells that can't be parsed as a number will be cleared.", + "Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).", + "Cells will be reinterpreted under the new type.": "Cells will be reinterpreted under the new type.", + "Cells will be replaced with a comma-separated list of file names.": "Cells will be replaced with a comma-separated list of file names.", + "Cells will be replaced with a comma-separated list of option names.": "Cells will be replaced with a comma-separated list of option names.", + "Cells will be replaced with the option name.": "Cells will be replaced with the option name.", + "Cells will be replaced with the page title.": "Cells will be replaced with the page title.", + "Cells will be replaced with the person's name.": "Cells will be replaced with the person's name.", + "Change type": "Change type", + "Change type to {{label}}?": "Change type to {{label}}?", + "Converting…": "Converting…", + "Existing values become single-item lists. No data is lost.": "Existing values become single-item lists. No data is lost.", + "Only the first selected item per row will be kept; the rest will be discarded.": "Only the first selected item per row will be kept; the rest will be discarded.", + "Previous record": "Previous record", + "Next record": "Next record", + "Record actions": "Record actions", + "Delete record": "Delete record", + "Delete record?": "Delete record?", + "This action cannot be undone.": "This action cannot be undone.", + "to navigate": "to navigate", + "to close": "to close", + "Expand row {{number}}": "Expand row {{number}}", + "Saving…": "Saving…", + "Read-only": "Read-only", + "Loading…": "Loading…", + "Updated {{when}}": "Updated {{when}}", + "Add property": "Add property", + "Create property": "Create property", + "Hide properties": "Hide properties", + "Find a property type": "Find a property type", + "Properties": "Properties" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index e9ceacf98..131977738 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -71,6 +71,7 @@ "Export": "Exportar", "Failed to create page": "No se pudo crear la página", "Failed to delete page": "No se pudo eliminar la página", + "Failed to restore page": "No se pudo restaurar la página", "Failed to fetch recent pages": "Error al obtener las páginas recientes", "Failed to import pages": "No se pudieron importar las páginas", "Failed to load page. An error occurred.": "Error al cargar la página. Se produjo un error.", @@ -276,6 +277,9 @@ "Align left": "Alinear a la izquierda", "Align right": "Alinear a la derecha", "Align center": "Alinear al centro", + "Alt text": "Texto alternativo", + "Describe this for accessibility.": "Describe esto para la accesibilidad.", + "Add a description": "Agregar una descripción", "Justify": "Justificar", "Merge cells": "Combinar celdas", "Split cell": "Dividir celda", @@ -286,6 +290,19 @@ "Add row above": "Agregar fila arriba", "Add row below": "Agregar fila debajo", "Delete table": "Eliminar tabla", + "Add column left": "Agregar columna a la izquierda", + "Add column right": "Agregar columna a la derecha", + "Clear cell": "Borrar celda", + "Clear cells": "Borrar celdas", + "Toggle header cell": "Alternar celda de encabezado", + "Toggle header column": "Alternar columna de encabezado", + "Toggle header row": "Alternar fila de encabezado", + "Move column left": "Mover columna a la izquierda", + "Move column right": "Mover columna a la derecha", + "Move row down": "Mover fila hacia abajo", + "Move row up": "Mover fila hacia arriba", + "Sort A → Z": "Ordenar de A → Z", + "Sort Z → A": "Ordenar de Z → A", "Info": "Información", "Note": "Nota", "Success": "Satisfactorio", @@ -348,6 +365,8 @@ "Create block quote.": "Crear una cita en bloque.", "Insert code snippet.": "Insertar fragmento de código.", "Insert horizontal rule divider": "Insertar regla horizontal", + "Page break": "Salto de página", + "Insert a page break for printing.": "Inserta un salto de página para imprimir.", "Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.", "Upload any video from your device.": "Sube cualquier video desde tu dispositivo.", "Upload any audio from your device.": "Sube cualquier audio desde tu dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Escribe...", "Column count": "Número de columnas", "{{count}} Columns": "{count, plural, one {# columna} other {# columnas}}", + "{{count}} command available_one": "1 comando disponible", + "{{count}} command available_other": "{{count}} comandos disponibles", + "{{count}} result available_one": "1 resultado disponible", + "{{count}} result available_other": "{{count}} resultados disponibles", "Equal columns": "Columnas iguales", "Left sidebar": "Barra lateral izquierda", "Right sidebar": "Barra lateral derecha", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} está disponible", "Default page edit mode": "Modo de edición predeterminado de la página", "Choose your preferred page edit mode. Avoid accidental edits.": "Elige tu modo de edición de página preferido. Evita ediciones accidentales.", + "Choose {{format}} file": "Elegir archivo {{format}}", "Reading": "Lectura", "Delete member": "Eliminar miembro", "Member deleted successfully": "Miembro eliminado correctamente", @@ -565,6 +589,8 @@ "Move to trash": "Mover a la papelera", "Move this page to trash?": "¿Mover esta página a la papelera?", "Restore page": "Restaurar página", + "Permanently delete": "Eliminar permanentemente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} movió esta página a la Papelera {{time}}.", "Page moved to trash": "Página movida a la papelera", "Page restored successfully": "Página restaurada correctamente", "Deleted by": "Eliminado por", @@ -608,25 +634,21 @@ "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", + "Update": "Actualizar", + "Update {{credential}}": "Actualizar {{credential}}", "Manage API keys for all users in the workspace": "Gestionar claves API para todos los usuarios en el espacio de trabajo", "Restrict API key creation to admins": "Restringir la creación de claves API a administradores", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Solo los administradores y propietarios pueden crear nuevas claves API. Las claves de miembros existentes seguirán funcionando.", @@ -858,9 +880,12 @@ "AI Chat": "Chat de IA", "Analyze for insights": "Analizar para obtener información", "Ask anything...": "Pregunta lo que quieras...", + "Assistant said:": "El asistente dijo:", "Chat history": "Historial de chat", "Chat name": "Nombre del chat", + "Chat transcript": "Transcripción del chat", "Close": "Cerrar", + "Copy assistant response": "Copiar respuesta del asistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "No se pudo cargar el chat. Se produjo un error.", "Failed to render this message.": "No se pudo mostrar este mensaje.", @@ -870,9 +895,17 @@ "No chats found": "No se encontraron chats", "No conversations yet": "Aún no hay conversaciones", "Open full page": "Abrir página completa", + "Scroll to bottom": "Desplazarse hasta abajo", + "You said:": "Dijiste:", "Previous 7 days": "Últimos 7 días", "Previous 30 days": "Últimos 30 días", "Search chats...": "Buscar chats...", + "Search chats": "Buscar chats", + "Ask anything... Use @ to mention pages": "Pregunta lo que sea... Usa @ para mencionar páginas", + "Ask anything or search your workspace": "Pregunta cualquier cosa o busca en tu espacio de trabajo", + "Welcome to {{name}}": "Te damos la bienvenida a {{name}}", + "Add files": "Agregar archivos", + "Mention a page": "Mencionar una página", "Start a new chat to see it here.": "Inicia un nuevo chat para verlo aquí.", "Summarize this page": "Resumir esta página", "Toggle AI Chat": "Alternar chat de IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Prueba con otro término de búsqueda.", "Try again": "Intentar de nuevo", "Untitled chat": "Chat sin título", - "What can I help you with?": "¿En qué puedo ayudarte?" + "What can I help you with?": "¿En qué puedo ayudarte?", + "Are you sure you want to revoke this {{credential}}": "¿Está seguro de que desea revocar esta {{credential}}?", + "Automatically provision users and groups from your identity provider via SCIM.": "Aprovisione automáticamente usuarios y grupos desde su proveedor de identidad mediante SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure su proveedor de identidad con esta URL para aprovisionar usuarios y grupos.", + "Create {{credential}}": "Crear {{credential}}", + "{{credential}} created": "{{credential}} creada", + "{{credential}} created successfully": "{{credential}} creada con éxito", + "Created by": "Creado por", + "Custom": "Personalizado", + "Enable SCIM": "Habilitar SCIM", + "Enter a descriptive name": "Introduzca un nombre descriptivo", + "I've saved my {{credential}}": "He guardado mi {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Asegúrese de copiar su {{credential}} ahora. ¡No podrá volver a verla!", + "Never": "Nunca", + "Revoke {{credential}}": "Revocar {{credential}}", + "SCIM endpoint URL": "URL del endpoint de SCIM", + "SCIM provisioning": "Aprovisionamiento SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM tiene prioridad sobre la sincronización de grupos de SSO mientras esté habilitado.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Ha alcanzado el máximo de {{max}} tokens SCIM. Elimine un token existente para crear uno nuevo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Tokens SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Esta acción no se puede deshacer. Su proveedor de identidad dejará de sincronizarse inmediatamente.", + "Toggle SCIM provisioning": "Activar o desactivar el aprovisionamiento SCIM", + "Token": "Token", + "Page menu": "Menú de la página", + "Expand": "Expandir", + "Collapse": "Contraer", + "Comment menu": "Menú de comentarios", + "Group menu": "Menú del grupo", + "Show hidden breadcrumbs": "Mostrar rutas de navegación ocultas", + "Breadcrumbs": "Rutas de navegación", + "Page actions": "Acciones de la página", + "Pick emoji": "Elegir emoji", + "Template menu": "Menú de plantillas", + "Use": "Usar", + "Use template": "Usar plantilla", + "Preview template: {{title}}": "Vista previa de la plantilla: {{title}}", + "Use a template": "Usar una plantilla", + "Search templates...": "Buscar plantillas...", + "Search spaces...": "Buscar espacios...", + "No templates found": "No se encontraron plantillas", + "No spaces found": "No se encontraron espacios", + "Browse all templates": "Ver todas las plantillas", + "This space": "Este espacio", + "All templates": "Todas las plantillas", + "Global": "Global", + "New template": "Nueva plantilla", + "Edit template": "Editar plantilla", + "Are you sure you want to delete this template?": "¿Seguro que quieres eliminar esta plantilla?", + "Template scope updated": "Alcance de la plantilla actualizado", + "Choose which space this template belongs to": "Elige a qué espacio pertenece esta plantilla", + "Scope": "Alcance", + "Select scope": "Seleccionar alcance", + "Title": "Título", + "Saving...": "Guardando...", + "Saved": "Guardado", + "Save failed. Retry": "Error al guardar. Reintentar", + "By {{name}}": "Por {{name}}", + "Updated {{time}}": "Actualizado {{time}}", + "Choose destination": "Elegir destino", + "Search pages and spaces...": "Buscar páginas y espacios...", + "No results found": "No se encontraron resultados", + "You don't have permission to create pages here": "No tienes permiso para crear páginas aquí", + "Chat menu": "Menú del chat", + "API key menu": "Menú de la clave API", + "Jump to comment selection": "Ir a la selección de comentarios", + "Slash commands": "Comandos de barra", + "Mention suggestions": "Sugerencias de menciones", + "Link suggestions": "Sugerencias de enlaces", + "Diagram editor": "Editor de diagramas", + "Add comment": "Agregar comentario", + "Find and replace": "Buscar y reemplazar", + "Main navigation": "Navegación principal", + "Space navigation": "Navegación del espacio", + "Settings navigation": "Navegación de configuración", + "AI navigation": "Navegación de IA", + "Breadcrumb": "Ruta de navegación", + "Synced block": "Bloque sincronizado", + "Create a block that stays in sync across pages.": "Crea un bloque que se mantenga sincronizado entre páginas.", + "Editing original": "Editando original", + "Copy synced block": "Copiar bloque sincronizado", + "Unsync": "Desincronizar", + "Delete synced block": "Eliminar bloque sincronizado", + "Synced to {{count}} other page_one": "Sincronizado con {{count}} página más", + "Synced to {{count}} other page_other": "Sincronizado con {{count}} páginas más", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "ESTA PÁGINA", + "No pages": "No hay páginas", + "The original synced block no longer exists": "El bloque sincronizado original ya no existe", + "You don't have access to this synced block": "No tienes acceso a este bloque sincronizado", + "Failed to load this synced block": "No se pudo cargar este bloque sincronizado", + "Fixed editor toolbar": "Barra de herramientas fija del editor", + "Show a formatting toolbar above the editor with quick access to common actions.": "Muestra una barra de herramientas de formato sobre el editor con acceso rápido a acciones comunes.", + "Toggle fixed editor toolbar": "Alternar barra de herramientas fija del editor", + "Normal text": "Texto normal", + "More inline formatting": "Más formato en línea", + "Subscript": "Subíndice", + "Superscript": "Superíndice", + "Inline code": "Código en línea", + "Insert media": "Insertar contenido multimedia", + "Mention": "Mención", + "Emoji": "Emojis", + "Columns": "Columnas", + "More inserts": "Más inserciones", + "Embeds": "Integraciones", + "Diagrams": "Diagramas", + "Advanced": "Avanzado", + "Utility": "Utilidad", + "Decrease indent": "Disminuir sangría", + "Increase indent": "Aumentar sangría", + "Clear formatting": "Borrar formato", + "Code block": "Bloque de código", + "Experimental": "Experimental", + "Strikethrough": "Tachado", + "Undo": "Deshacer", + "Redo": "Rehacer", + "Backlinks": "Enlaces entrantes", + "Last updated by": "Última actualización por", + "Last updated": "Última actualización", + "Stats": "Estadísticas", + "Word count": "Recuento de palabras", + "Characters": "Caracteres", + "Incoming links": "Enlaces entrantes", + "Outgoing links": "Enlaces salientes", + "Incoming links ({{count}})": "Enlaces entrantes ({{count}})", + "Outgoing links ({{count}})": "Enlaces salientes ({{count}})", + "No pages link here yet.": "Todavía no hay páginas que enlacen aquí.", + "This page doesn't link to other pages yet.": "Esta página todavía no enlaza a otras páginas.", + "Verified until {{date}}": "Verificado hasta {{date}}", + "Labels": "Etiquetas", + "Add label": "Agregar etiqueta", + "No labels yet": "Todavía no hay etiquetas", + "Already added": "Ya agregado", + "Invalid label name": "Nombre de etiqueta no válido", + "No matches": "Sin coincidencias", + "Search or create…": "Buscar o crear…", + "Remove label {{name}}": "Eliminar etiqueta {{name}}", + "Failed to add label": "No se pudo agregar la etiqueta", + "Failed to remove label": "No se pudo eliminar la etiqueta", + "No pages with this label": "No hay páginas con esta etiqueta", + "Pages tagged with this label will appear here.": "Las páginas etiquetadas con esta etiqueta aparecerán aquí.", + "No pages match your search.": "Ninguna página coincide con tu búsqueda.", + "Updated {{date}}": "Actualizado el {{date}}", + "Cell actions": "Acciones de celda", + "Column actions": "Acciones de columna", + "Row actions": "Acciones de fila", + "Filter": "Filtrar", + "Page title": "Título de la página", + "Page content": "Contenido de la página", + "Member actions": "Acciones de miembro", + "Toggle password visibility": "Alternar visibilidad de la contraseña", + "Send comment": "Enviar comentario", + "Token actions": "Acciones de token", + "Template settings": "Configuración de la plantilla", + "Edit diagram": "Editar diagrama", + "Edit embed": "Editar contenido integrado", + "Edit drawing": "Editar dibujo", + "Delete equation": "Eliminar ecuación", + "Invite actions": "Acciones de invitación", + "Get started": "Comenzar", + "* indicates required fields": "* indica los campos obligatorios", + "List of spaces in this workspace": "Lista de espacios en este espacio de trabajo", + "Active sessions": "Sesiones activas", + "Add {{name}} to favorites": "Agregar {{name}} a favoritos", + "Remove {{name}} from favorites": "Quitar {{name}} de favoritos", + "Added to favorites": "Agregado a favoritos", + "Removed from favorites": "Quitado de favoritos", + "Added {{name}} to favorites": "Se agregó {{name}} a favoritos", + "Removed {{name}} from favorites": "Se quitó {{name}} de favoritos", + "Page menu for {{name}}": "Menú de página para {{name}}", + "Create subpage of {{name}}": "Crear subpágina de {{name}}" } diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 90172476a..20f354421 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -71,6 +71,7 @@ "Export": "Exporter", "Failed to create page": "Échec de la création de la page", "Failed to delete page": "Échec de la suppression de la page", + "Failed to restore page": "Échec de la restauration de la page", "Failed to fetch recent pages": "Échec de la récupération des pages récentes", "Failed to import pages": "Échec de l'importation des pages", "Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.", @@ -276,6 +277,9 @@ "Align left": "Aligner à gauche", "Align right": "Aligner à droite", "Align center": "Aligner au centre", + "Alt text": "Texte alternatif", + "Describe this for accessibility.": "Décrivez ceci pour l’accessibilité.", + "Add a description": "Ajouter une description", "Justify": "Justifier", "Merge cells": "Fusionner les cellules", "Split cell": "Diviser la cellule", @@ -286,6 +290,19 @@ "Add row above": "Ajouter une ligne au-dessus", "Add row below": "Ajouter une ligne en dessous", "Delete table": "Supprimer le tableau", + "Add column left": "Ajouter une colonne à gauche", + "Add column right": "Ajouter une colonne à droite", + "Clear cell": "Effacer la cellule", + "Clear cells": "Effacer les cellules", + "Toggle header cell": "Activer/désactiver la cellule d’en-tête", + "Toggle header column": "Activer/désactiver la colonne d’en-tête", + "Toggle header row": "Activer/désactiver la ligne d’en-tête", + "Move column left": "Déplacer la colonne vers la gauche", + "Move column right": "Déplacer la colonne vers la droite", + "Move row down": "Déplacer la ligne vers le bas", + "Move row up": "Déplacer la ligne vers le haut", + "Sort A → Z": "Trier de A à Z", + "Sort Z → A": "Trier de Z à A", "Info": "Info", "Note": "Remarque", "Success": "Succès", @@ -348,6 +365,8 @@ "Create block quote.": "Créez un bloc de citation.", "Insert code snippet.": "Insérez un extrait de code.", "Insert horizontal rule divider": "Insérer un séparateur de règle horizontale", + "Page break": "Saut de page", + "Insert a page break for printing.": "Insérer un saut de page pour l’impression.", "Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.", "Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.", "Upload any audio from your device.": "Téléchargez n'importe quel fichier audio depuis votre appareil.", @@ -392,6 +411,10 @@ "Write...": "Écrire...", "Column count": "Nombre de colonnes", "{{count}} Columns": "{count, plural, one {# colonne} other {# colonnes}}", + "{{count}} command available_one": "1 commande disponible", + "{{count}} command available_other": "{{count}} commandes disponibles", + "{{count}} result available_one": "1 résultat disponible", + "{{count}} result available_other": "{{count}} résultats disponibles", "Equal columns": "Colonnes égales", "Left sidebar": "Barre latérale gauche", "Right sidebar": "Barre latérale droite", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} est disponible", "Default page edit mode": "Mode d’édition par défaut de la page", "Choose your preferred page edit mode. Avoid accidental edits.": "Choisissez votre mode d'édition de page préféré. Évitez les modifications accidentelles.", + "Choose {{format}} file": "Choisir un fichier {{format}}", "Reading": "Lecture", "Delete member": "Supprimer le membre", "Member deleted successfully": "Membre supprimé avec succès", @@ -565,6 +589,8 @@ "Move to trash": "Déplacer vers la corbeille", "Move this page to trash?": "Déplacer cette page vers la corbeille ?", "Restore page": "Restaurer la page", + "Permanently delete": "Supprimer définitivement", + "{{name}} moved this page to Trash {{time}}.": "{{name}} a déplacé cette page vers la corbeille {{time}}.", "Page moved to trash": "Page déplacée vers la corbeille", "Page restored successfully": "Page restaurée avec succès", "Deleted by": "Supprimé par", @@ -608,25 +634,21 @@ "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", + "Update": "Mettre à jour", + "Update {{credential}}": "Mettre à jour {{credential}}", "Manage API keys for all users in the workspace": "Gérer les clés API pour tous les utilisateurs dans l'espace de travail", "Restrict API key creation to admins": "Restreindre la création de clés API aux administrateurs", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Seuls les administrateurs et les propriétaires peuvent créer de nouvelles clés API. Les clés des membres existants continueront de fonctionner.", @@ -858,9 +880,12 @@ "AI Chat": "Chat IA", "Analyze for insights": "Analyser pour obtenir des informations", "Ask anything...": "Posez n’importe quelle question...", + "Assistant said:": "L’assistant a dit :", "Chat history": "Historique des discussions", "Chat name": "Nom de la discussion", + "Chat transcript": "Transcription du chat", "Close": "Fermer", + "Copy assistant response": "Copier la réponse de l’assistant", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Échec du chargement de la discussion. Une erreur s’est produite.", "Failed to render this message.": "Échec de l’affichage de ce message.", @@ -870,9 +895,17 @@ "No chats found": "Aucune discussion trouvée", "No conversations yet": "Aucune conversation pour le moment", "Open full page": "Ouvrir la page complète", + "Scroll to bottom": "Faire défiler jusqu’en bas", + "You said:": "Vous avez dit :", "Previous 7 days": "7 derniers jours", "Previous 30 days": "30 derniers jours", "Search chats...": "Rechercher des discussions...", + "Search chats": "Rechercher des discussions", + "Ask anything... Use @ to mention pages": "Demandez n’importe quoi… Utilisez @ pour mentionner des pages", + "Ask anything or search your workspace": "Posez n’importe quelle question ou recherchez dans votre espace de travail", + "Welcome to {{name}}": "Bienvenue sur {{name}}", + "Add files": "Ajouter des fichiers", + "Mention a page": "Mentionner une page", "Start a new chat to see it here.": "Commencez une nouvelle discussion pour la voir ici.", "Summarize this page": "Résumer cette page", "Toggle AI Chat": "Basculer le chat IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Essayez un autre terme de recherche.", "Try again": "Réessayer", "Untitled chat": "Discussion sans titre", - "What can I help you with?": "Que puis-je faire pour vous aider ?" + "What can I help you with?": "Que puis-je faire pour vous aider ?", + "Are you sure you want to revoke this {{credential}}": "Êtes-vous sûr de vouloir révoquer ce/cette {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Provisionnez automatiquement les utilisateurs et les groupes depuis votre fournisseur d’identité via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configurez votre fournisseur d’identité avec cette URL pour provisionner les utilisateurs et les groupes.", + "Create {{credential}}": "Créer {{credential}}", + "{{credential}} created": "{{credential}} créé", + "{{credential}} created successfully": "{{credential}} créé avec succès", + "Created by": "Créé par", + "Custom": "Personnalisé", + "Enable SCIM": "Activer SCIM", + "Enter a descriptive name": "Saisissez un nom descriptif", + "I've saved my {{credential}}": "J’ai enregistré mon/ma {{credential}}", + "Important": "Important", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Assurez-vous de copier votre {{credential}} maintenant. Vous ne pourrez plus le/la voir ensuite !", + "Never": "Jamais", + "Revoke {{credential}}": "Révoquer {{credential}}", + "SCIM endpoint URL": "URL du point de terminaison SCIM", + "SCIM provisioning": "Provisionnement SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM a priorité sur la synchronisation des groupes SSO lorsqu’il est activé.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Vous avez atteint le maximum de {{max}} jetons SCIM. Supprimez un jeton existant pour en créer un nouveau.", + "SCIM token": "Jeton SCIM", + "SCIM tokens": "Jetons SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Cette action est irréversible. Votre fournisseur d’identité cessera immédiatement la synchronisation.", + "Toggle SCIM provisioning": "Activer/désactiver le provisionnement SCIM", + "Token": "Jeton", + "Page menu": "Menu de la page", + "Expand": "Développer", + "Collapse": "Réduire", + "Comment menu": "Menu du commentaire", + "Group menu": "Menu du groupe", + "Show hidden breadcrumbs": "Afficher les fils d’Ariane masqués", + "Breadcrumbs": "Fils d’Ariane", + "Page actions": "Actions de la page", + "Pick emoji": "Choisir un emoji", + "Template menu": "Menu du modèle", + "Use": "Utiliser", + "Use template": "Utiliser le modèle", + "Preview template: {{title}}": "Aperçu du modèle : {{title}}", + "Use a template": "Utiliser un modèle", + "Search templates...": "Rechercher des modèles...", + "Search spaces...": "Rechercher des espaces...", + "No templates found": "Aucun modèle trouvé", + "No spaces found": "Aucun espace trouvé", + "Browse all templates": "Parcourir tous les modèles", + "This space": "Cet espace", + "All templates": "Tous les modèles", + "Global": "Global", + "New template": "Nouveau modèle", + "Edit template": "Modifier le modèle", + "Are you sure you want to delete this template?": "Êtes-vous sûr de vouloir supprimer ce modèle ?", + "Template scope updated": "Portée du modèle mise à jour", + "Choose which space this template belongs to": "Choisissez à quel espace appartient ce modèle", + "Scope": "Portée", + "Select scope": "Sélectionner la portée", + "Title": "Titre", + "Saving...": "Enregistrement...", + "Saved": "Enregistré", + "Save failed. Retry": "Échec de l’enregistrement. Réessayer", + "By {{name}}": "Par {{name}}", + "Updated {{time}}": "Mis à jour {{time}}", + "Choose destination": "Choisir la destination", + "Search pages and spaces...": "Rechercher des pages et des espaces...", + "No results found": "Aucun résultat trouvé", + "You don't have permission to create pages here": "Vous n’avez pas l’autorisation de créer des pages ici", + "Chat menu": "Menu du chat", + "API key menu": "Menu de la clé API", + "Jump to comment selection": "Aller à la sélection de commentaires", + "Slash commands": "Commandes slash", + "Mention suggestions": "Suggestions de mention", + "Link suggestions": "Suggestions de liens", + "Diagram editor": "Éditeur de diagrammes", + "Add comment": "Ajouter un commentaire", + "Find and replace": "Rechercher et remplacer", + "Main navigation": "Navigation principale", + "Space navigation": "Navigation de l’espace", + "Settings navigation": "Navigation des paramètres", + "AI navigation": "Navigation IA", + "Breadcrumb": "Fil d’Ariane", + "Synced block": "Bloc synchronisé", + "Create a block that stays in sync across pages.": "Créez un bloc qui reste synchronisé entre les pages.", + "Editing original": "Modification de l’original", + "Copy synced block": "Copier le bloc synchronisé", + "Unsync": "Désynchroniser", + "Delete synced block": "Supprimer le bloc synchronisé", + "Synced to {{count}} other page_one": "Synchronisée avec {{count}} autre page", + "Synced to {{count}} other page_other": "Synchronisée avec {{count}} autres pages", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "CETTE PAGE", + "No pages": "Aucune page", + "The original synced block no longer exists": "Le bloc synchronisé d’origine n’existe plus", + "You don't have access to this synced block": "Vous n’avez pas accès à ce bloc synchronisé", + "Failed to load this synced block": "Échec du chargement de ce bloc synchronisé", + "Fixed editor toolbar": "Barre d’outils de l’éditeur fixe", + "Show a formatting toolbar above the editor with quick access to common actions.": "Afficher une barre d’outils de mise en forme au-dessus de l’éditeur avec un accès rapide aux actions courantes.", + "Toggle fixed editor toolbar": "Activer/désactiver la barre d’outils de l’éditeur fixe", + "Normal text": "Texte normal", + "More inline formatting": "Plus de mise en forme en ligne", + "Subscript": "Indice", + "Superscript": "Exposant", + "Inline code": "Code en ligne", + "Insert media": "Insérer un média", + "Mention": "Mention", + "Emoji": "Emoji", + "Columns": "Colonnes", + "More inserts": "Plus d’insertions", + "Embeds": "Intégrations", + "Diagrams": "Diagrammes", + "Advanced": "Avancé", + "Utility": "Utilitaire", + "Decrease indent": "Réduire le retrait", + "Increase indent": "Augmenter le retrait", + "Clear formatting": "Effacer la mise en forme", + "Code block": "Bloc de code", + "Experimental": "Expérimental", + "Strikethrough": "Barré", + "Undo": "Annuler", + "Redo": "Rétablir", + "Backlinks": "Liens retour", + "Last updated by": "Dernière mise à jour par", + "Last updated": "Dernière mise à jour", + "Stats": "Statistiques", + "Word count": "Nombre de mots", + "Characters": "Caractères", + "Incoming links": "Liens entrants", + "Outgoing links": "Liens sortants", + "Incoming links ({{count}})": "Liens entrants ({{count}})", + "Outgoing links ({{count}})": "Liens sortants ({{count}})", + "No pages link here yet.": "Aucune page ne pointe encore ici.", + "This page doesn't link to other pages yet.": "Cette page ne renvoie pas encore vers d’autres pages.", + "Verified until {{date}}": "Vérifié jusqu’au {{date}}", + "Labels": "Étiquettes", + "Add label": "Ajouter une étiquette", + "No labels yet": "Aucune étiquette pour l’instant", + "Already added": "Déjà ajouté", + "Invalid label name": "Nom d’étiquette invalide", + "No matches": "Aucune correspondance", + "Search or create…": "Rechercher ou créer…", + "Remove label {{name}}": "Supprimer l’étiquette {{name}}", + "Failed to add label": "Échec de l’ajout de l’étiquette", + "Failed to remove label": "Échec de la suppression de l’étiquette", + "No pages with this label": "Aucune page avec cette étiquette", + "Pages tagged with this label will appear here.": "Les pages portant cette étiquette apparaîtront ici.", + "No pages match your search.": "Aucune page ne correspond à votre recherche.", + "Updated {{date}}": "Mis à jour le {{date}}", + "Cell actions": "Actions de cellule", + "Column actions": "Actions de colonne", + "Row actions": "Actions de ligne", + "Filter": "Filtrer", + "Page title": "Titre de la page", + "Page content": "Contenu de la page", + "Member actions": "Actions des membres", + "Toggle password visibility": "Afficher/masquer le mot de passe", + "Send comment": "Envoyer le commentaire", + "Token actions": "Actions du jeton", + "Template settings": "Paramètres du modèle", + "Edit diagram": "Modifier le diagramme", + "Edit embed": "Modifier l’intégration", + "Edit drawing": "Modifier le dessin", + "Delete equation": "Supprimer l’équation", + "Invite actions": "Actions d’invitation", + "Get started": "Commencer", + "* indicates required fields": "* indique les champs obligatoires", + "List of spaces in this workspace": "Liste des espaces de cet espace de travail", + "Active sessions": "Sessions actives", + "Add {{name}} to favorites": "Ajouter {{name}} aux favoris", + "Remove {{name}} from favorites": "Retirer {{name}} des favoris", + "Added to favorites": "Ajouté aux favoris", + "Removed from favorites": "Retiré des favoris", + "Added {{name}} to favorites": "{{name}} a été ajouté aux favoris", + "Removed {{name}} from favorites": "{{name}} a été retiré des favoris", + "Page menu for {{name}}": "Menu de la page pour {{name}}", + "Create subpage of {{name}}": "Créer une sous-page de {{name}}" } diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index 9ceec008a..441c90f69 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -71,6 +71,7 @@ "Export": "Esporta", "Failed to create page": "Impossibile creare la pagina", "Failed to delete page": "Impossibile eliminare la pagina", + "Failed to restore page": "Impossibile ripristinare la pagina", "Failed to fetch recent pages": "Impossibile recuperare le pagine recenti", "Failed to import pages": "Impossibile importare le pagine", "Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.", @@ -276,6 +277,9 @@ "Align left": "Allinea a sinistra", "Align right": "Allinea a destra", "Align center": "Allinea al centro", + "Alt text": "Testo alternativo", + "Describe this for accessibility.": "Descrivi questo contenuto per l'accessibilità.", + "Add a description": "Aggiungi una descrizione", "Justify": "Giustifica", "Merge cells": "Unisci celle", "Split cell": "Dividi cella", @@ -286,6 +290,19 @@ "Add row above": "Aggiungi riga sopra", "Add row below": "Aggiungi riga sotto", "Delete table": "Elimina tabella", + "Add column left": "Aggiungi colonna a sinistra", + "Add column right": "Aggiungi colonna a destra", + "Clear cell": "Cancella cella", + "Clear cells": "Cancella celle", + "Toggle header cell": "Attiva/disattiva cella di intestazione", + "Toggle header column": "Attiva/disattiva colonna di intestazione", + "Toggle header row": "Attiva/disattiva riga di intestazione", + "Move column left": "Sposta colonna a sinistra", + "Move column right": "Sposta colonna a destra", + "Move row down": "Sposta riga in basso", + "Move row up": "Sposta riga in alto", + "Sort A → Z": "Ordina A → Z", + "Sort Z → A": "Ordina Z → A", "Info": "Informazioni", "Note": "Nota", "Success": "Successo", @@ -348,6 +365,8 @@ "Create block quote.": "Crea blocco citazione.", "Insert code snippet.": "Inserisci frammento di codice.", "Insert horizontal rule divider": "Inserisci divisore di regola orizzontale", + "Page break": "Interruzione di pagina", + "Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.", "Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.", "Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Scrivi...", "Column count": "Numero di colonne", "{{count}} Columns": "{{count}} colonne", + "{{count}} command available_one": "1 comando disponibile", + "{{count}} command available_other": "{{count}} comandi disponibili", + "{{count}} result available_one": "1 risultato disponibile", + "{{count}} result available_other": "{{count}} risultati disponibili", "Equal columns": "Colonne uguali", "Left sidebar": "Barra laterale sinistra", "Right sidebar": "Barra laterale destra", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} è disponibile", "Default page edit mode": "Modalità di modifica predefinita della pagina", "Choose your preferred page edit mode. Avoid accidental edits.": "Scegli la tua modalità di modifica della pagina preferita. Evita modifiche accidentali.", + "Choose {{format}} file": "Scegli file {{format}}", "Reading": "Lettura", "Delete member": "Elimina membro", "Member deleted successfully": "Membro eliminato con successo", @@ -565,6 +589,8 @@ "Move to trash": "Sposta nel cestino", "Move this page to trash?": "Spostare questa pagina nel cestino?", "Restore page": "Ripristina pagina", + "Permanently delete": "Elimina definitivamente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} ha spostato questa pagina nel Cestino {{time}}.", "Page moved to trash": "Pagina spostata nel cestino", "Page restored successfully": "Pagina ripristinata con successo", "Deleted by": "Eliminato da", @@ -608,25 +634,21 @@ "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", + "Update": "Aggiorna", + "Update {{credential}}": "Aggiorna {{credential}}", "Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro", "Restrict API key creation to admins": "Limita la creazione delle chiavi API agli amministratori", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Solo gli amministratori e i proprietari possono creare nuove chiavi API. Le chiavi dei membri esistenti continueranno a funzionare.", @@ -858,9 +880,12 @@ "AI Chat": "Chat IA", "Analyze for insights": "Analizza per ottenere approfondimenti", "Ask anything...": "Chiedi qualsiasi cosa...", + "Assistant said:": "L'assistente ha detto:", "Chat history": "Cronologia chat", "Chat name": "Nome chat", + "Chat transcript": "Trascrizione della chat", "Close": "Chiudi", + "Copy assistant response": "Copia risposta dell'assistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Caricamento della chat non riuscito. Si è verificato un errore.", "Failed to render this message.": "Impossibile visualizzare questo messaggio.", @@ -870,9 +895,17 @@ "No chats found": "Nessuna chat trovata", "No conversations yet": "Nessuna conversazione al momento", "Open full page": "Apri pagina completa", + "Scroll to bottom": "Scorri in basso", + "You said:": "Hai detto:", "Previous 7 days": "Ultimi 7 giorni", "Previous 30 days": "Ultimi 30 giorni", "Search chats...": "Cerca nelle chat...", + "Search chats": "Cerca nelle chat", + "Ask anything... Use @ to mention pages": "Chiedi qualsiasi cosa... Usa @ per menzionare le pagine", + "Ask anything or search your workspace": "Chiedi qualsiasi cosa o cerca nel tuo spazio di lavoro", + "Welcome to {{name}}": "Benvenuto in {{name}}", + "Add files": "Aggiungi file", + "Mention a page": "Menziona una pagina", "Start a new chat to see it here.": "Avvia una nuova chat per vederla qui.", "Summarize this page": "Riassumi questa pagina", "Toggle AI Chat": "Attiva/disattiva Chat IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Prova un termine di ricerca diverso.", "Try again": "Riprova", "Untitled chat": "Chat senza titolo", - "What can I help you with?": "Con cosa posso aiutarti?" + "What can I help you with?": "Con cosa posso aiutarti?", + "Are you sure you want to revoke this {{credential}}": "Sei sicuro di voler revocare questa {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Esegui automaticamente il provisioning di utenti e gruppi dal tuo provider di identità tramite SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configura il tuo provider di identità con questo URL per eseguire il provisioning di utenti e gruppi.", + "Create {{credential}}": "Crea {{credential}}", + "{{credential}} created": "{{credential}} creata", + "{{credential}} created successfully": "{{credential}} creata con successo", + "Created by": "Creata da", + "Custom": "Personalizzato", + "Enable SCIM": "Abilita SCIM", + "Enter a descriptive name": "Inserisci un nome descrittivo", + "I've saved my {{credential}}": "Ho salvato la mia {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Assicurati di copiare subito la tua {{credential}}. Non potrai più visualizzarla!", + "Never": "Mai", + "Revoke {{credential}}": "Revoca {{credential}}", + "SCIM endpoint URL": "URL dell'endpoint SCIM", + "SCIM provisioning": "Provisioning SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM ha la precedenza sulla sincronizzazione dei gruppi SSO quando è abilitato.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Hai raggiunto il numero massimo di {{max}} token SCIM. Elimina un token esistente per crearne uno nuovo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Token SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Questa azione non può essere annullata. Il tuo provider di identità smetterà di sincronizzarsi immediatamente.", + "Toggle SCIM provisioning": "Attiva/disattiva il provisioning SCIM", + "Token": "Token", + "Page menu": "Menu della pagina", + "Expand": "Espandi", + "Collapse": "Comprimi", + "Comment menu": "Menu dei commenti", + "Group menu": "Menu del gruppo", + "Show hidden breadcrumbs": "Mostra breadcrumb nascosti", + "Breadcrumbs": "Breadcrumb", + "Page actions": "Azioni della pagina", + "Pick emoji": "Scegli emoji", + "Template menu": "Menu del modello", + "Use": "Usa", + "Use template": "Usa modello", + "Preview template: {{title}}": "Anteprima modello: {{title}}", + "Use a template": "Usa un modello", + "Search templates...": "Cerca modelli...", + "Search spaces...": "Cerca spazi...", + "No templates found": "Nessun modello trovato", + "No spaces found": "Nessuno spazio trovato", + "Browse all templates": "Sfoglia tutti i modelli", + "This space": "Questo spazio", + "All templates": "Tutti i modelli", + "Global": "Globale", + "New template": "Nuovo modello", + "Edit template": "Modifica modello", + "Are you sure you want to delete this template?": "Sei sicuro di voler eliminare questo modello?", + "Template scope updated": "Ambito del modello aggiornato", + "Choose which space this template belongs to": "Scegli a quale spazio appartiene questo modello", + "Scope": "Ambito", + "Select scope": "Seleziona ambito", + "Title": "Titolo", + "Saving...": "Salvataggio...", + "Saved": "Salvato", + "Save failed. Retry": "Salvataggio non riuscito. Riprova", + "By {{name}}": "Di {{name}}", + "Updated {{time}}": "Aggiornato {{time}}", + "Choose destination": "Scegli destinazione", + "Search pages and spaces...": "Cerca pagine e spazi...", + "No results found": "Nessun risultato trovato", + "You don't have permission to create pages here": "Non hai l'autorizzazione per creare pagine qui", + "Chat menu": "Menu della chat", + "API key menu": "Menu della chiave API", + "Jump to comment selection": "Vai alla selezione dei commenti", + "Slash commands": "Comandi slash", + "Mention suggestions": "Suggerimenti di menzione", + "Link suggestions": "Suggerimenti di link", + "Diagram editor": "Editor di diagrammi", + "Add comment": "Aggiungi commento", + "Find and replace": "Trova e sostituisci", + "Main navigation": "Navigazione principale", + "Space navigation": "Navigazione dello spazio", + "Settings navigation": "Navigazione delle impostazioni", + "AI navigation": "Navigazione AI", + "Breadcrumb": "Percorso di navigazione", + "Synced block": "Blocco sincronizzato", + "Create a block that stays in sync across pages.": "Crea un blocco che rimanga sincronizzato tra le pagine.", + "Editing original": "Modifica originale", + "Copy synced block": "Copia blocco sincronizzato", + "Unsync": "Annulla sincronizzazione", + "Delete synced block": "Elimina blocco sincronizzato", + "Synced to {{count}} other page_one": "Sincronizzato con {{count}} altra pagina", + "Synced to {{count}} other page_other": "Sincronizzato con {{count}} altre pagine", + "ORIGINAL": "ORIGINALE", + "THIS PAGE": "QUESTA PAGINA", + "No pages": "Nessuna pagina", + "The original synced block no longer exists": "Il blocco sincronizzato originale non esiste più", + "You don't have access to this synced block": "Non hai accesso a questo blocco sincronizzato", + "Failed to load this synced block": "Impossibile caricare questo blocco sincronizzato", + "Fixed editor toolbar": "Barra degli strumenti dell'editor fissa", + "Show a formatting toolbar above the editor with quick access to common actions.": "Mostra una barra degli strumenti di formattazione sopra l'editor con accesso rapido alle azioni comuni.", + "Toggle fixed editor toolbar": "Attiva/disattiva barra degli strumenti dell'editor fissa", + "Normal text": "Testo normale", + "More inline formatting": "Altra formattazione in linea", + "Subscript": "Pedice", + "Superscript": "Apice", + "Inline code": "Codice in linea", + "Insert media": "Inserisci contenuti multimediali", + "Mention": "Menzione", + "Emoji": "Emoji", + "Columns": "Colonne", + "More inserts": "Altri inserimenti", + "Embeds": "Incorporamenti", + "Diagrams": "Diagrammi", + "Advanced": "Avanzate", + "Utility": "Utilità", + "Decrease indent": "Riduci rientro", + "Increase indent": "Aumenta rientro", + "Clear formatting": "Cancella formattazione", + "Code block": "Blocco di codice", + "Experimental": "Sperimentale", + "Strikethrough": "Barrato", + "Undo": "Annulla", + "Redo": "Ripeti", + "Backlinks": "Backlink", + "Last updated by": "Ultimo aggiornamento di", + "Last updated": "Ultimo aggiornamento", + "Stats": "Statistiche", + "Word count": "Conteggio parole", + "Characters": "Caratteri", + "Incoming links": "Link in entrata", + "Outgoing links": "Link in uscita", + "Incoming links ({{count}})": "Link in entrata ({{count}})", + "Outgoing links ({{count}})": "Link in uscita ({{count}})", + "No pages link here yet.": "Nessuna pagina rimanda ancora qui.", + "This page doesn't link to other pages yet.": "Questa pagina non rimanda ancora ad altre pagine.", + "Verified until {{date}}": "Verificato fino al {{date}}", + "Labels": "Etichette", + "Add label": "Aggiungi etichetta", + "No labels yet": "Nessuna etichetta per ora", + "Already added": "Già aggiunto", + "Invalid label name": "Nome etichetta non valido", + "No matches": "Nessuna corrispondenza", + "Search or create…": "Cerca o crea…", + "Remove label {{name}}": "Rimuovi etichetta {{name}}", + "Failed to add label": "Impossibile aggiungere l'etichetta", + "Failed to remove label": "Impossibile rimuovere l'etichetta", + "No pages with this label": "Nessuna pagina con questa etichetta", + "Pages tagged with this label will appear here.": "Le pagine contrassegnate con questa etichetta appariranno qui.", + "No pages match your search.": "Nessuna pagina corrisponde alla tua ricerca.", + "Updated {{date}}": "Aggiornato il {{date}}", + "Cell actions": "Azioni cella", + "Column actions": "Azioni colonna", + "Row actions": "Azioni riga", + "Filter": "Filtro", + "Page title": "Titolo pagina", + "Page content": "Contenuto della pagina", + "Member actions": "Azioni membro", + "Toggle password visibility": "Attiva/disattiva visibilità password", + "Send comment": "Invia commento", + "Token actions": "Azioni token", + "Template settings": "Impostazioni modello", + "Edit diagram": "Modifica diagramma", + "Edit embed": "Modifica incorporamento", + "Edit drawing": "Modifica disegno", + "Delete equation": "Elimina equazione", + "Invite actions": "Azioni invito", + "Get started": "Inizia", + "* indicates required fields": "* indica i campi obbligatori", + "List of spaces in this workspace": "Elenco degli spazi in questo spazio di lavoro", + "Active sessions": "Sessioni attive", + "Add {{name}} to favorites": "Aggiungi {{name}} ai preferiti", + "Remove {{name}} from favorites": "Rimuovi {{name}} dai preferiti", + "Added to favorites": "Aggiunto ai preferiti", + "Removed from favorites": "Rimosso dai preferiti", + "Added {{name}} to favorites": "{{name}} aggiunto ai preferiti", + "Removed {{name}} from favorites": "{{name}} rimosso dai preferiti", + "Page menu for {{name}}": "Menu della pagina per {{name}}", + "Create subpage of {{name}}": "Crea sottopagina di {{name}}" } diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index 14285e238..492868cf2 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -71,6 +71,7 @@ "Export": "エクスポート", "Failed to create page": "ページの作成に失敗しました", "Failed to delete page": "ページの削除に失敗しました", + "Failed to restore page": "ページの復元に失敗しました", "Failed to fetch recent pages": "最近のページを取得できませんでした", "Failed to import pages": "ページのインポートに失敗しました", "Failed to load page. An error occurred.": "ページの読み込みに失敗しました。エラーが発生しました。", @@ -276,6 +277,9 @@ "Align left": "左揃え", "Align right": "右揃え", "Align center": "中央揃え", + "Alt text": "代替テキスト", + "Describe this for accessibility.": "アクセシビリティのために説明を追加してください。", + "Add a description": "説明を追加", "Justify": "両端揃え", "Merge cells": "セルを結合", "Split cell": "セルを分割", @@ -286,6 +290,19 @@ "Add row above": "上に行を追加", "Add row below": "下に行を追加", "Delete table": "テーブルを削除", + "Add column left": "左に列を追加", + "Add column right": "右に列を追加", + "Clear cell": "セルをクリア", + "Clear cells": "セルをクリア", + "Toggle header cell": "ヘッダーセルを切り替え", + "Toggle header column": "ヘッダー列を切り替え", + "Toggle header row": "ヘッダー行を切り替え", + "Move column left": "列を左に移動", + "Move column right": "列を右に移動", + "Move row down": "行を下に移動", + "Move row up": "行を上に移動", + "Sort A → Z": "A → Z で並べ替え", + "Sort Z → A": "Z → A で並べ替え", "Info": "情報", "Note": "ノート", "Success": "成功", @@ -348,6 +365,8 @@ "Create block quote.": "引用ブロックを作成します", "Insert code snippet.": "コードスニペットを挿入します", "Insert horizontal rule divider": "区切り線を挿入します", + "Page break": "改ページ", + "Insert a page break for printing.": "印刷用に改ページを挿入します。", "Upload any image from your device.": "デバイスから画像をアップロードします", "Upload any video from your device.": "デバイスから動画をアップロードします", "Upload any audio from your device.": "デバイスから音声ファイルをアップロードします。", @@ -392,6 +411,10 @@ "Write...": "ここに入力...", "Column count": "列数", "{{count}} Columns": "{{count}}列", + "{{count}} command available_one": "利用可能なコマンドが1件あります", + "{{count}} command available_other": "利用可能なコマンドが{{count}}件あります", + "{{count}} result available_one": "結果が1件あります", + "{{count}} result available_other": "結果が{{count}}件あります", "Equal columns": "均等な列", "Left sidebar": "左サイドバー", "Right sidebar": "右サイドバー", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} が利用可能です", "Default page edit mode": "デフォルトのページ編集モード", "Choose your preferred page edit mode. Avoid accidental edits.": "お好みのページ編集モードを選択してください(誤編集を防止します)", + "Choose {{format}} file": "{{format}} ファイルを選択", "Reading": "閲覧", "Delete member": "メンバーを削除", "Member deleted successfully": "メンバーが正常に削除されました", @@ -565,6 +589,8 @@ "Move to trash": "ゴミ箱に移動", "Move this page to trash?": "このページをごみ箱に移動しますか?", "Restore page": "ページを復元", + "Permanently delete": "完全に削除", + "{{name}} moved this page to Trash {{time}}.": "{{name}} が {{time}} にこのページをゴミ箱に移動しました。", "Page moved to trash": "ページをゴミ箱に移動しました", "Page restored successfully": "ページが正常に復元されました", "Deleted by": "削除者", @@ -608,25 +634,21 @@ "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キーを更新", + "Update": "更新", + "Update {{credential}}": "{{credential}}を更新", "Manage API keys for all users in the workspace": "ワークスペース内のすべてのユーザーのAPIキーを管理", "Restrict API key creation to admins": "APIキーの作成を管理者のみに制限する", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "新しいAPIキーを作成できるのは管理者とオーナーのみです。既存のメンバーキーは引き続き有効です。", @@ -858,9 +880,12 @@ "AI Chat": "AI チャット", "Analyze for insights": "分析してインサイトを得る", "Ask anything...": "何でも聞いてください...", + "Assistant said:": "アシスタントの回答:", "Chat history": "チャット履歴", "Chat name": "チャット名", + "Chat transcript": "チャットの記録", "Close": "閉じる", + "Copy assistant response": "アシスタントの回答をコピー", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "チャットの読み込みに失敗しました。エラーが発生しました。", "Failed to render this message.": "このメッセージの表示に失敗しました。", @@ -870,9 +895,17 @@ "No chats found": "チャットが見つかりません", "No conversations yet": "会話はまだありません", "Open full page": "全ページで開く", + "Scroll to bottom": "一番下までスクロール", + "You said:": "あなたの発言:", "Previous 7 days": "過去 7 日間", "Previous 30 days": "過去 30 日間", "Search chats...": "チャットを検索...", + "Search chats": "チャットを検索", + "Ask anything... Use @ to mention pages": "何でも質問してください… @ を使ってページにメンションできます", + "Ask anything or search your workspace": "何でも質問するか、ワークスペースを検索", + "Welcome to {{name}}": "{{name}} へようこそ", + "Add files": "ファイルを追加", + "Mention a page": "ページにメンション", "Start a new chat to see it here.": "ここに表示するには新しいチャットを開始してください。", "Summarize this page": "このページを要約", "Toggle AI Chat": "AI チャットを切り替え", @@ -880,5 +913,176 @@ "Try a different search term.": "別の検索語を試してください。", "Try again": "再試行", "Untitled chat": "無題のチャット", - "What can I help you with?": "何をお手伝いしましょうか?" + "What can I help you with?": "何をお手伝いしましょうか?", + "Are you sure you want to revoke this {{credential}}": "この{{credential}}を無効にしてもよろしいですか", + "Automatically provision users and groups from your identity provider via SCIM.": "SCIM を介して、ID プロバイダーからユーザーとグループを自動的にプロビジョニングします。", + "Configure your identity provider with this URL to provision users and groups.": "この URL を使用して ID プロバイダーを設定し、ユーザーとグループをプロビジョニングします。", + "Create {{credential}}": "{{credential}}を作成", + "{{credential}} created": "{{credential}}を作成しました", + "{{credential}} created successfully": "{{credential}}を正常に作成しました", + "Created by": "作成者", + "Custom": "カスタム", + "Enable SCIM": "SCIM を有効にする", + "Enter a descriptive name": "説明的な名前を入力してください", + "I've saved my {{credential}}": "{{credential}}を保存しました", + "Important": "重要", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "今すぐ {{credential}} をコピーしてください。後でもう一度表示することはできません!", + "Never": "なし", + "Revoke {{credential}}": "{{credential}}を無効にする", + "SCIM endpoint URL": "SCIM エンドポイント URL", + "SCIM provisioning": "SCIM プロビジョニング", + "SCIM takes precedence over SSO group sync while enabled.": "有効になっている間は、SCIM が SSO グループ同期より優先されます。", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "SCIM トークンの上限 {{max}} に達しました。新しいトークンを作成するには、既存のトークンを削除してください。", + "SCIM token": "SCIM トークン", + "SCIM tokens": "SCIM トークン", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "この操作は元に戻せません。ID プロバイダーは直ちに同期を停止します。", + "Toggle SCIM provisioning": "SCIM プロビジョニングを切り替える", + "Token": "トークン", + "Page menu": "ページメニュー", + "Expand": "展開", + "Collapse": "折りたたむ", + "Comment menu": "コメントメニュー", + "Group menu": "グループメニュー", + "Show hidden breadcrumbs": "非表示のパンくずリストを表示", + "Breadcrumbs": "パンくずリスト", + "Page actions": "ページアクション", + "Pick emoji": "絵文字を選択", + "Template menu": "テンプレートメニュー", + "Use": "使用", + "Use template": "テンプレートを使用", + "Preview template: {{title}}": "テンプレートをプレビュー: {{title}}", + "Use a template": "テンプレートを使用", + "Search templates...": "テンプレートを検索…", + "Search spaces...": "スペースを検索…", + "No templates found": "テンプレートが見つかりません", + "No spaces found": "スペースが見つかりません", + "Browse all templates": "すべてのテンプレートを表示", + "This space": "このスペース", + "All templates": "すべてのテンプレート", + "Global": "グローバル", + "New template": "新しいテンプレート", + "Edit template": "テンプレートを編集", + "Are you sure you want to delete this template?": "このテンプレートを削除してもよろしいですか?", + "Template scope updated": "テンプレートのスコープを更新しました", + "Choose which space this template belongs to": "このテンプレートを所属させるスペースを選択", + "Scope": "スコープ", + "Select scope": "スコープを選択", + "Title": "タイトル", + "Saving...": "保存中…", + "Saved": "保存しました", + "Save failed. Retry": "保存に失敗しました。再試行", + "By {{name}}": "{{name}} 作成", + "Updated {{time}}": "{{time}} に更新", + "Choose destination": "保存先を選択", + "Search pages and spaces...": "ページとスペースを検索…", + "No results found": "結果が見つかりません", + "You don't have permission to create pages here": "ここにページを作成する権限がありません", + "Chat menu": "チャットメニュー", + "API key menu": "API キーメニュー", + "Jump to comment selection": "コメント選択に移動", + "Slash commands": "スラッシュコマンド", + "Mention suggestions": "メンション候補", + "Link suggestions": "リンク候補", + "Diagram editor": "ダイアグラムエディター", + "Add comment": "コメントを追加", + "Find and replace": "検索と置換", + "Main navigation": "メインナビゲーション", + "Space navigation": "スペースナビゲーション", + "Settings navigation": "設定ナビゲーション", + "AI navigation": "AI ナビゲーション", + "Breadcrumb": "パンくずリスト", + "Synced block": "同期ブロック", + "Create a block that stays in sync across pages.": "ページ間で同期されたままになるブロックを作成します。", + "Editing original": "オリジナルを編集中", + "Copy synced block": "同期ブロックをコピー", + "Unsync": "同期解除", + "Delete synced block": "同期ブロックを削除", + "Synced to {{count}} other page_one": "他の{{count}}ページに同期済みです", + "Synced to {{count}} other page_other": "他の{{count}}ページに同期済みです", + "ORIGINAL": "オリジナル", + "THIS PAGE": "このページ", + "No pages": "ページがありません", + "The original synced block no longer exists": "元の同期ブロックは存在しなくなりました", + "You don't have access to this synced block": "この同期ブロックにアクセスできません", + "Failed to load this synced block": "この同期ブロックの読み込みに失敗しました", + "Fixed editor toolbar": "固定エディターツールバー", + "Show a formatting toolbar above the editor with quick access to common actions.": "一般的な操作にすばやくアクセスできる書式設定ツールバーをエディターの上に表示します。", + "Toggle fixed editor toolbar": "固定エディターツールバーを切り替え", + "Normal text": "通常のテキスト", + "More inline formatting": "その他のインライン書式", + "Subscript": "下付き", + "Superscript": "上付き", + "Inline code": "インラインコード", + "Insert media": "メディアを挿入", + "Mention": "メンション", + "Emoji": "絵文字", + "Columns": "列", + "More inserts": "その他の挿入", + "Embeds": "埋め込み", + "Diagrams": "ダイアグラム", + "Advanced": "詳細", + "Utility": "ユーティリティ", + "Decrease indent": "インデントを減らす", + "Increase indent": "インデントを増やす", + "Clear formatting": "書式をクリア", + "Code block": "コードブロック", + "Experimental": "実験的", + "Strikethrough": "取り消し線", + "Undo": "元に戻す", + "Redo": "やり直す", + "Backlinks": "バックリンク", + "Last updated by": "最終更新者", + "Last updated": "最終更新", + "Stats": "統計", + "Word count": "単語数", + "Characters": "文字数", + "Incoming links": "被リンク", + "Outgoing links": "発リンク", + "Incoming links ({{count}})": "被リンク ({{count}})", + "Outgoing links ({{count}})": "発リンク ({{count}})", + "No pages link here yet.": "まだこのページにリンクしているページはありません。", + "This page doesn't link to other pages yet.": "このページはまだ他のページにリンクしていません。", + "Verified until {{date}}": "{{date}} まで検証済み", + "Labels": "ラベル", + "Add label": "ラベルを追加", + "No labels yet": "まだラベルはありません", + "Already added": "追加済み", + "Invalid label name": "無効なラベル名", + "No matches": "一致するものがありません", + "Search or create…": "検索または作成…", + "Remove label {{name}}": "ラベル {{name}} を削除", + "Failed to add label": "ラベルの追加に失敗しました", + "Failed to remove label": "ラベルの削除に失敗しました", + "No pages with this label": "このラベルが付いたページはありません", + "Pages tagged with this label will appear here.": "このラベルが付いたページがここに表示されます。", + "No pages match your search.": "検索に一致するページはありません。", + "Updated {{date}}": "{{date}} に更新", + "Cell actions": "セルの操作", + "Column actions": "列の操作", + "Row actions": "行の操作", + "Filter": "フィルター", + "Page title": "ページタイトル", + "Page content": "ページ内容", + "Member actions": "メンバーの操作", + "Toggle password visibility": "パスワードの表示を切り替え", + "Send comment": "コメントを送信", + "Token actions": "トークンの操作", + "Template settings": "テンプレート設定", + "Edit diagram": "ダイアグラムを編集", + "Edit embed": "埋め込みを編集", + "Edit drawing": "図を編集", + "Delete equation": "数式を削除", + "Invite actions": "招待の操作", + "Get started": "始める", + "* indicates required fields": "* は必須項目を示します", + "List of spaces in this workspace": "このワークスペース内のスペース一覧", + "Active sessions": "アクティブなセッション", + "Add {{name}} to favorites": "{{name}} をお気に入りに追加", + "Remove {{name}} from favorites": "{{name}} をお気に入りから削除", + "Added to favorites": "お気に入りに追加しました", + "Removed from favorites": "お気に入りから削除しました", + "Added {{name}} to favorites": "{{name}} をお気に入りに追加しました", + "Removed {{name}} from favorites": "{{name}} をお気に入りから削除しました", + "Page menu for {{name}}": "{{name}} のページメニュー", + "Create subpage of {{name}}": "{{name}} のサブページを作成" } diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index e02f16335..5847ab19a 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -71,6 +71,7 @@ "Export": "내보내기", "Failed to create page": "페이지 생성 실패", "Failed to delete page": "페이지 삭제 실패", + "Failed to restore page": "페이지를 복원하지 못했습니다", "Failed to fetch recent pages": "최근 페이지 불러오기 실패", "Failed to import pages": "페이지 가져오기 실패", "Failed to load page. An error occurred.": "페이지 불러오기 실패. 오류가 발생했습니다.", @@ -276,6 +277,9 @@ "Align left": "왼쪽 정렬", "Align right": "오른쪽 정렬", "Align center": "가운데 정렬", + "Alt text": "대체 텍스트", + "Describe this for accessibility.": "접근성을 위해 이를 설명하세요.", + "Add a description": "설명 추가", "Justify": "양쪽 정렬", "Merge cells": "셀 병합", "Split cell": "셀 분할", @@ -286,6 +290,19 @@ "Add row above": "위에 행 추가", "Add row below": "아래에 행 추가", "Delete table": "테이블 삭제", + "Add column left": "왼쪽에 열 추가", + "Add column right": "오른쪽에 열 추가", + "Clear cell": "셀 지우기", + "Clear cells": "셀 지우기", + "Toggle header cell": "헤더 셀 전환", + "Toggle header column": "헤더 열 전환", + "Toggle header row": "헤더 행 전환", + "Move column left": "열 왼쪽으로 이동", + "Move column right": "열 오른쪽으로 이동", + "Move row down": "행 아래로 이동", + "Move row up": "행 위로 이동", + "Sort A → Z": "A → Z 정렬", + "Sort Z → A": "Z → A 정렬", "Info": "정보", "Note": "참고", "Success": "완료", @@ -348,6 +365,8 @@ "Create block quote.": "인용구 만들기.", "Insert code snippet.": "코드 블록 삽입.", "Insert horizontal rule divider": "가로 구분선 삽입", + "Page break": "페이지 나누기", + "Insert a page break for printing.": "인쇄용 페이지 나누기를 삽입합니다.", "Upload any image from your device.": "기기에서 이미지를 업로드하세요.", "Upload any video from your device.": "기기에서 비디오를 업로드하세요.", "Upload any audio from your device.": "기기에서 오디오를 업로드하세요.", @@ -392,6 +411,10 @@ "Write...": "작성...", "Column count": "열 개수", "{{count}} Columns": "{{count}}열", + "{{count}} command available_one": "사용 가능한 명령 1개", + "{{count}} command available_other": "사용 가능한 명령 {{count}}개", + "{{count}} result available_one": "사용 가능한 결과 1개", + "{{count}} result available_other": "사용 가능한 결과 {{count}}개", "Equal columns": "열 너비 균등", "Left sidebar": "왼쪽 사이드바", "Right sidebar": "오른쪽 사이드바", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} 버전을 사용할 수 있습니다", "Default page edit mode": "기본 페이지 편집 모드", "Choose your preferred page edit mode. Avoid accidental edits.": "선호하는 페이지 편집 모드를 선택하세요. 실수로 인한 편집을 방지하세요.", + "Choose {{format}} file": "{{format}} 파일 선택", "Reading": "읽기", "Delete member": "멤버 삭제", "Member deleted successfully": "멤버가 성공적으로 삭제되었습니다", @@ -565,6 +589,8 @@ "Move to trash": "휴지통으로 이동", "Move this page to trash?": "이 페이지를 휴지통으로 이동하시겠습니까?", "Restore page": "페이지 복원", + "Permanently delete": "영구 삭제", + "{{name}} moved this page to Trash {{time}}.": "{{name}}님이 {{time}}에 이 페이지를 휴지통으로 이동했습니다.", "Page moved to trash": "페이지가 휴지통으로 이동되었습니다", "Page restored successfully": "페이지가 성공적으로 복원되었습니다", "Deleted by": "삭제한 사람", @@ -608,25 +634,21 @@ "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 키 갱신", + "Update": "업데이트", + "Update {{credential}}": "{{credential}} 업데이트", "Manage API keys for all users in the workspace": "워크스페이스 내 모든 사용자의 API 키 관리", "Restrict API key creation to admins": "API 키 생성 권한을 관리자에게만 제한합니다", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "새로운 API 키는 관리자와 소유자만 생성할 수 있습니다. 기존 멤버 키는 계속 사용할 수 있습니다.", @@ -858,9 +880,12 @@ "AI Chat": "AI 채팅", "Analyze for insights": "인사이트 분석", "Ask anything...": "무엇이든 물어보세요...", + "Assistant said:": "어시스턴트의 답변:", "Chat history": "채팅 기록", "Chat name": "채팅 이름", + "Chat transcript": "채팅 기록", "Close": "닫기", + "Copy assistant response": "어시스턴트 응답 복사", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "채팅을 불러오지 못했습니다. 오류가 발생했습니다.", "Failed to render this message.": "이 메시지를 렌더링하지 못했습니다.", @@ -870,9 +895,17 @@ "No chats found": "채팅을 찾을 수 없습니다", "No conversations yet": "아직 대화가 없습니다", "Open full page": "전체 페이지 열기", + "Scroll to bottom": "맨 아래로 스크롤", + "You said:": "내가 한 말:", "Previous 7 days": "지난 7일", "Previous 30 days": "지난 30일", "Search chats...": "채팅 검색...", + "Search chats": "채팅 검색", + "Ask anything... Use @ to mention pages": "무엇이든 물어보세요... 페이지를 언급하려면 @를 사용하세요", + "Ask anything or search your workspace": "무엇이든 물어보거나 워크스페이스를 검색하세요", + "Welcome to {{name}}": "{{name}}에 오신 것을 환영합니다", + "Add files": "파일 추가", + "Mention a page": "페이지 멘션", "Start a new chat to see it here.": "여기에 표시하려면 새 채팅을 시작하세요.", "Summarize this page": "이 페이지 요약", "Toggle AI Chat": "AI 채팅 전환", @@ -880,5 +913,176 @@ "Try a different search term.": "다른 검색어를 사용해 보세요.", "Try again": "다시 시도", "Untitled chat": "제목 없는 채팅", - "What can I help you with?": "무엇을 도와드릴까요?" + "What can I help you with?": "무엇을 도와드릴까요?", + "Are you sure you want to revoke this {{credential}}": "이 {{credential}}을 취소하시겠습니까?", + "Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자에서 사용자와 그룹을 자동으로 프로비저닝합니다.", + "Configure your identity provider with this URL to provision users and groups.": "사용자와 그룹을 프로비저닝할 수 있도록 이 URL로 ID 공급자를 구성하세요.", + "Create {{credential}}": "{{credential}} 만들기", + "{{credential}} created": "{{credential}} 생성됨", + "{{credential}} created successfully": "{{credential}} 생성 완료", + "Created by": "생성한 사람", + "Custom": "사용자 지정", + "Enable SCIM": "SCIM 활성화", + "Enter a descriptive name": "설명적인 이름을 입력하세요", + "I've saved my {{credential}}": "내 {{credential}}를 저장했습니다", + "Important": "중요", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "지금 {{credential}}를 복사해 두세요. 다시는 볼 수 없습니다!", + "Never": "안 함", + "Revoke {{credential}}": "{{credential}} 취소", + "SCIM endpoint URL": "SCIM 엔드포인트 URL", + "SCIM provisioning": "SCIM 프로비저닝", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM이 활성화되어 있는 동안에는 SSO 그룹 동기화보다 SCIM이 우선 적용됩니다.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "SCIM 토큰은 최대 {{max}}개까지 만들 수 있습니다. 새 토큰을 만들려면 기존 토큰을 삭제하세요.", + "SCIM token": "SCIM 토큰", + "SCIM tokens": "SCIM 토큰", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "이 작업은 되돌릴 수 없습니다. ID 공급자가 즉시 동기화를 중지합니다.", + "Toggle SCIM provisioning": "SCIM 프로비저닝 전환", + "Token": "토큰", + "Page menu": "페이지 메뉴", + "Expand": "펼치기", + "Collapse": "접기", + "Comment menu": "댓글 메뉴", + "Group menu": "그룹 메뉴", + "Show hidden breadcrumbs": "숨겨진 이동 경로 표시", + "Breadcrumbs": "이동 경로", + "Page actions": "페이지 작업", + "Pick emoji": "이모지 선택", + "Template menu": "템플릿 메뉴", + "Use": "사용", + "Use template": "템플릿 사용", + "Preview template: {{title}}": "템플릿 미리보기: {{title}}", + "Use a template": "템플릿 사용", + "Search templates...": "템플릿 검색...", + "Search spaces...": "스페이스 검색...", + "No templates found": "템플릿을 찾을 수 없습니다", + "No spaces found": "스페이스를 찾을 수 없습니다", + "Browse all templates": "모든 템플릿 보기", + "This space": "이 스페이스", + "All templates": "모든 템플릿", + "Global": "전역", + "New template": "새 템플릿", + "Edit template": "템플릿 편집", + "Are you sure you want to delete this template?": "이 템플릿을 삭제하시겠습니까?", + "Template scope updated": "템플릿 범위가 업데이트되었습니다", + "Choose which space this template belongs to": "이 템플릿이 속할 스페이스를 선택하세요", + "Scope": "범위", + "Select scope": "범위 선택", + "Title": "제목", + "Saving...": "저장 중...", + "Saved": "저장됨", + "Save failed. Retry": "저장에 실패했습니다. 다시 시도하세요", + "By {{name}}": "작성자 {{name}}", + "Updated {{time}}": "{{time}}에 업데이트됨", + "Choose destination": "대상 선택", + "Search pages and spaces...": "페이지와 스페이스 검색...", + "No results found": "결과를 찾을 수 없습니다", + "You don't have permission to create pages here": "여기에서 페이지를 만들 권한이 없습니다", + "Chat menu": "채팅 메뉴", + "API key menu": "API 키 메뉴", + "Jump to comment selection": "댓글 선택으로 이동", + "Slash commands": "슬래시 명령어", + "Mention suggestions": "멘션 추천", + "Link suggestions": "링크 추천", + "Diagram editor": "다이어그램 편집기", + "Add comment": "댓글 추가", + "Find and replace": "찾기 및 바꾸기", + "Main navigation": "기본 탐색", + "Space navigation": "스페이스 탐색", + "Settings navigation": "설정 탐색", + "AI navigation": "AI 탐색", + "Breadcrumb": "이동 경로", + "Synced block": "동기화된 블록", + "Create a block that stays in sync across pages.": "페이지 간에 동기화된 상태로 유지되는 블록을 만드세요.", + "Editing original": "원본 편집 중", + "Copy synced block": "동기화된 블록 복사", + "Unsync": "동기화 해제", + "Delete synced block": "동기화된 블록 삭제", + "Synced to {{count}} other page_one": "다른 페이지 {{count}}개에 동기화됨", + "Synced to {{count}} other page_other": "다른 페이지 {{count}}개에 동기화됨", + "ORIGINAL": "원본", + "THIS PAGE": "이 페이지", + "No pages": "페이지 없음", + "The original synced block no longer exists": "원본 동기화 블록이 더 이상 존재하지 않습니다", + "You don't have access to this synced block": "이 동기화된 블록에 접근할 수 없습니다", + "Failed to load this synced block": "이 동기화된 블록을 불러오지 못했습니다", + "Fixed editor toolbar": "고정된 편집기 도구 모음", + "Show a formatting toolbar above the editor with quick access to common actions.": "일반적인 작업에 빠르게 접근할 수 있도록 편집기 위에 서식 도구 모음을 표시합니다.", + "Toggle fixed editor toolbar": "고정된 편집기 도구 모음 전환", + "Normal text": "일반 텍스트", + "More inline formatting": "추가 인라인 서식", + "Subscript": "아래 첨자", + "Superscript": "위 첨자", + "Inline code": "인라인 코드", + "Insert media": "미디어 삽입", + "Mention": "멘션", + "Emoji": "이모지", + "Columns": "열", + "More inserts": "더 많은 삽입", + "Embeds": "임베드", + "Diagrams": "다이어그램", + "Advanced": "고급", + "Utility": "유틸리티", + "Decrease indent": "들여쓰기 줄이기", + "Increase indent": "들여쓰기 늘리기", + "Clear formatting": "서식 지우기", + "Code block": "코드 블록", + "Experimental": "실험 기능", + "Strikethrough": "취소선", + "Undo": "실행 취소", + "Redo": "다시 실행", + "Backlinks": "백링크", + "Last updated by": "마지막 업데이트한 사람", + "Last updated": "마지막 업데이트", + "Stats": "통계", + "Word count": "단어 수", + "Characters": "문자 수", + "Incoming links": "들어오는 링크", + "Outgoing links": "나가는 링크", + "Incoming links ({{count}})": "들어오는 링크 ({{count}})", + "Outgoing links ({{count}})": "나가는 링크 ({{count}})", + "No pages link here yet.": "아직 여기에 링크된 페이지가 없습니다.", + "This page doesn't link to other pages yet.": "이 페이지는 아직 다른 페이지에 링크되어 있지 않습니다.", + "Verified until {{date}}": "{{date}}까지 검증됨", + "Labels": "라벨", + "Add label": "라벨 추가", + "No labels yet": "아직 라벨이 없습니다", + "Already added": "이미 추가됨", + "Invalid label name": "유효하지 않은 라벨 이름", + "No matches": "일치하는 항목 없음", + "Search or create…": "검색하거나 만들기…", + "Remove label {{name}}": "라벨 {{name}} 제거", + "Failed to add label": "라벨 추가 실패", + "Failed to remove label": "라벨 제거 실패", + "No pages with this label": "이 라벨이 지정된 페이지가 없습니다", + "Pages tagged with this label will appear here.": "이 라벨이 지정된 페이지가 여기에 표시됩니다.", + "No pages match your search.": "검색과 일치하는 페이지가 없습니다.", + "Updated {{date}}": "{{date}}에 업데이트됨", + "Cell actions": "셀 작업", + "Column actions": "열 작업", + "Row actions": "행 작업", + "Filter": "필터", + "Page title": "페이지 제목", + "Page content": "페이지 내용", + "Member actions": "멤버 작업", + "Toggle password visibility": "비밀번호 표시 전환", + "Send comment": "댓글 보내기", + "Token actions": "토큰 작업", + "Template settings": "템플릿 설정", + "Edit diagram": "다이어그램 편집", + "Edit embed": "임베드 편집", + "Edit drawing": "드로잉 편집", + "Delete equation": "수식 삭제", + "Invite actions": "초대 작업", + "Get started": "시작하기", + "* indicates required fields": "* 는 필수 입력 항목을 나타냅니다", + "List of spaces in this workspace": "이 워크스페이스의 스페이스 목록", + "Active sessions": "활성 세션", + "Add {{name}} to favorites": "{{name}} 즐겨찾기에 추가", + "Remove {{name}} from favorites": "{{name}} 즐겨찾기에서 제거", + "Added to favorites": "즐겨찾기에 추가됨", + "Removed from favorites": "즐겨찾기에서 제거됨", + "Added {{name}} to favorites": "{{name}} 즐겨찾기에 추가됨", + "Removed {{name}} from favorites": "{{name}} 즐겨찾기에서 제거됨", + "Page menu for {{name}}": "{{name}}의 페이지 메뉴", + "Create subpage of {{name}}": "{{name}}의 하위 페이지 만들기" } diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 0ef2a3ba2..591c96746 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -71,6 +71,7 @@ "Export": "Exporteer", "Failed to create page": "Pagina aanmaken mislukt", "Failed to delete page": "Verwijderen van pagina mislukt", + "Failed to restore page": "Pagina herstellen mislukt", "Failed to fetch recent pages": "Kan recente pagina's niet ophalen", "Failed to import pages": "Pagina's importeren mislukt", "Failed to load page. An error occurred.": "Laden van pagina mislukt. Er is een fout opgetreden.", @@ -276,6 +277,9 @@ "Align left": "Links uitlijnen", "Align right": "Rechts uitlijnen", "Align center": "Centreren", + "Alt text": "Alternatieve tekst", + "Describe this for accessibility.": "Beschrijf dit voor toegankelijkheid.", + "Add a description": "Een beschrijving toevoegen", "Justify": "Uitvullen", "Merge cells": "Cellen samenvoegen", "Split cell": "Cel splitsen", @@ -286,6 +290,19 @@ "Add row above": "Rij hierboven toevoegen", "Add row below": "Rij hieronder toevoegen", "Delete table": "Verwijder tabel", + "Add column left": "Kolom links toevoegen", + "Add column right": "Kolom rechts toevoegen", + "Clear cell": "Cel wissen", + "Clear cells": "Cellen wissen", + "Toggle header cell": "Kopcel in-/uitschakelen", + "Toggle header column": "Kopkolom in-/uitschakelen", + "Toggle header row": "Koprij in-/uitschakelen", + "Move column left": "Kolom naar links verplaatsen", + "Move column right": "Kolom naar rechts verplaatsen", + "Move row down": "Rij omlaag verplaatsen", + "Move row up": "Rij omhoog verplaatsen", + "Sort A → Z": "Sorteren A → Z", + "Sort Z → A": "Sorteren Z → A", "Info": "Info", "Note": "Opmerking", "Success": "Geslaagd", @@ -348,6 +365,8 @@ "Create block quote.": "Maak een block quote.", "Insert code snippet.": "Codefragment invoegen.", "Insert horizontal rule divider": "Horizontale lijn invoegen", + "Page break": "Pagina-einde", + "Insert a page break for printing.": "Voeg een pagina-einde in voor het afdrukken.", "Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.", "Upload any video from your device.": "Upload een video vanaf uw apparaat.", "Upload any audio from your device.": "Upload een audio vanaf uw apparaat.", @@ -392,6 +411,10 @@ "Write...": "Typ...", "Column count": "Aantal kolommen", "{{count}} Columns": "{{count}} kolommen", + "{{count}} command available_one": "1 opdracht beschikbaar", + "{{count}} command available_other": "{{count}} opdrachten beschikbaar", + "{{count}} result available_one": "1 resultaat beschikbaar", + "{{count}} result available_other": "{{count}} resultaten beschikbaar", "Equal columns": "Gelijke kolommen", "Left sidebar": "Linker zijbalk", "Right sidebar": "Rechter zijbalk", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} is beschikbaar", "Default page edit mode": "Standaard bewerkingsmodus voor pagina", "Choose your preferred page edit mode. Avoid accidental edits.": "Kies uw voorkeurs bewerkmodus voor pagina's. Vermijd per ongeluk bewerken.", + "Choose {{format}} file": "Kies {{format}}-bestand", "Reading": "Lezen", "Delete member": "Lid verwijderen", "Member deleted successfully": "Lid succesvol verwijderd", @@ -565,6 +589,8 @@ "Move to trash": "Verplaatsen naar prullenbak", "Move this page to trash?": "Deze pagina naar de prullenbak verplaatsen?", "Restore page": "Pagina herstellen", + "Permanently delete": "Permanent verwijderen", + "{{name}} moved this page to Trash {{time}}.": "{{name}} heeft deze pagina {{time}} naar de prullenbak verplaatst.", "Page moved to trash": "Pagina verplaatst naar prullenbak", "Page restored successfully": "Pagina succesvol hersteld", "Deleted by": "Verwijderd door", @@ -608,25 +634,21 @@ "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", + "Update": "Bijwerken", + "Update {{credential}}": "{{credential}} bijwerken", "Manage API keys for all users in the workspace": "Beheer API-sleutels voor alle gebruikers in de werkruimte", "Restrict API key creation to admins": "Beperk het aanmaken van API-sleutels tot beheerders.", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Alleen beheerders en eigenaren kunnen nieuwe API-sleutels aanmaken. Bestaande leden-sleutels blijven werken.", @@ -858,9 +880,12 @@ "AI Chat": "AI-chat", "Analyze for insights": "Analyseren voor inzichten", "Ask anything...": "Vraag iets...", + "Assistant said:": "Assistent zei:", "Chat history": "Chatgeschiedenis", "Chat name": "Chatnaam", + "Chat transcript": "Chattranscript", "Close": "Sluiten", + "Copy assistant response": "Reactie van assistent kopiëren", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Chat laden mislukt. Er is een fout opgetreden.", "Failed to render this message.": "Dit bericht kon niet worden weergegeven.", @@ -870,9 +895,17 @@ "No chats found": "Geen chats gevonden", "No conversations yet": "Nog geen gesprekken", "Open full page": "Volledige pagina openen", + "Scroll to bottom": "Naar beneden scrollen", + "You said:": "Jij zei:", "Previous 7 days": "Afgelopen 7 dagen", "Previous 30 days": "Afgelopen 30 dagen", "Search chats...": "Chats zoeken...", + "Search chats": "Chats zoeken", + "Ask anything... Use @ to mention pages": "Vraag iets... Gebruik @ om pagina's te vermelden", + "Ask anything or search your workspace": "Vraag iets of doorzoek je werkruimte", + "Welcome to {{name}}": "Welkom bij {{name}}", + "Add files": "Bestanden toevoegen", + "Mention a page": "Een pagina vermelden", "Start a new chat to see it here.": "Start een nieuwe chat om die hier te zien.", "Summarize this page": "Vat deze pagina samen", "Toggle AI Chat": "AI-chat in-/uitschakelen", @@ -880,5 +913,176 @@ "Try a different search term.": "Probeer een andere zoekterm.", "Try again": "Probeer opnieuw", "Untitled chat": "Chat zonder titel", - "What can I help you with?": "Waar kan ik je mee helpen?" + "What can I help you with?": "Waar kan ik je mee helpen?", + "Are you sure you want to revoke this {{credential}}": "Weet u zeker dat u deze {{credential}} wilt intrekken", + "Automatically provision users and groups from your identity provider via SCIM.": "Voorzie gebruikers en groepen automatisch vanuit uw identiteitsprovider via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configureer uw identiteitsprovider met deze URL om gebruikers en groepen te provisioneren.", + "Create {{credential}}": "{{credential}} maken", + "{{credential}} created": "{{credential}} aangemaakt", + "{{credential}} created successfully": "{{credential}} succesvol aangemaakt", + "Created by": "Aangemaakt door", + "Custom": "Aangepast", + "Enable SCIM": "SCIM inschakelen", + "Enter a descriptive name": "Voer een beschrijvende naam in", + "I've saved my {{credential}}": "Ik heb mijn {{credential}} opgeslagen", + "Important": "Belangrijk", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Zorg ervoor dat u uw {{credential}} nu kopieert. U kunt deze niet meer bekijken!", + "Never": "Nooit", + "Revoke {{credential}}": "{{credential}} intrekken", + "SCIM endpoint URL": "SCIM-eindpunt-URL", + "SCIM provisioning": "SCIM-provisioning", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM heeft voorrang op SSO-groepssynchronisatie zolang het is ingeschakeld.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "U heeft het maximum van {{max}} SCIM-tokens bereikt. Verwijder een bestaand token om een nieuw token aan te maken.", + "SCIM token": "SCIM-token", + "SCIM tokens": "SCIM-tokens", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Deze actie kan niet ongedaan worden gemaakt. Uw identiteitsprovider stopt onmiddellijk met synchroniseren.", + "Toggle SCIM provisioning": "SCIM-provisioning in-/uitschakelen", + "Token": "Token", + "Page menu": "Paginamenu", + "Expand": "Uitvouwen", + "Collapse": "Samenvouwen", + "Comment menu": "Reactiemenu", + "Group menu": "Groepsmenu", + "Show hidden breadcrumbs": "Verborgen broodkruimels weergeven", + "Breadcrumbs": "Broodkruimels", + "Page actions": "Pagina-acties", + "Pick emoji": "Emoji kiezen", + "Template menu": "Sjabloonmenu", + "Use": "Gebruiken", + "Use template": "Sjabloon gebruiken", + "Preview template: {{title}}": "Voorbeeld van sjabloon: {{title}}", + "Use a template": "Een sjabloon gebruiken", + "Search templates...": "Sjablonen zoeken...", + "Search spaces...": "Werkruimtes zoeken...", + "No templates found": "Geen sjablonen gevonden", + "No spaces found": "Geen werkruimtes gevonden", + "Browse all templates": "Alle sjablonen bekijken", + "This space": "Deze werkruimte", + "All templates": "Alle sjablonen", + "Global": "Globaal", + "New template": "Nieuw sjabloon", + "Edit template": "Sjabloon bewerken", + "Are you sure you want to delete this template?": "Weet je zeker dat je dit sjabloon wilt verwijderen?", + "Template scope updated": "Bereik van sjabloon bijgewerkt", + "Choose which space this template belongs to": "Kies bij welke werkruimte dit sjabloon hoort", + "Scope": "Bereik", + "Select scope": "Bereik selecteren", + "Title": "Titel", + "Saving...": "Opslaan...", + "Saved": "Opgeslagen", + "Save failed. Retry": "Opslaan mislukt. Opnieuw proberen", + "By {{name}}": "Door {{name}}", + "Updated {{time}}": "Bijgewerkt {{time}}", + "Choose destination": "Bestemming kiezen", + "Search pages and spaces...": "Pagina's en werkruimtes zoeken...", + "No results found": "Geen resultaten gevonden", + "You don't have permission to create pages here": "Je hebt geen toestemming om hier pagina's te maken", + "Chat menu": "Chatmenu", + "API key menu": "API-sleutelmenu", + "Jump to comment selection": "Naar reactieselectie springen", + "Slash commands": "Slash-opdrachten", + "Mention suggestions": "Vermeldingssuggesties", + "Link suggestions": "Linksuggesties", + "Diagram editor": "Diagrameditor", + "Add comment": "Reactie toevoegen", + "Find and replace": "Zoeken en vervangen", + "Main navigation": "Hoofdnavigatie", + "Space navigation": "Ruimtenavigatie", + "Settings navigation": "Instellingennavigatie", + "AI navigation": "AI-navigatie", + "Breadcrumb": "Broodkruimel", + "Synced block": "Gesynchroniseerd blok", + "Create a block that stays in sync across pages.": "Maak een blok dat gesynchroniseerd blijft op meerdere pagina's.", + "Editing original": "Origineel bewerken", + "Copy synced block": "Gesynchroniseerd blok kopiëren", + "Unsync": "Synchronisatie opheffen", + "Delete synced block": "Gesynchroniseerd blok verwijderen", + "Synced to {{count}} other page_one": "Gesynchroniseerd met {{count}} andere pagina", + "Synced to {{count}} other page_other": "Gesynchroniseerd met {{count}} andere pagina's", + "ORIGINAL": "ORIGINEEL", + "THIS PAGE": "DEZE PAGINA", + "No pages": "Geen pagina's", + "The original synced block no longer exists": "Het oorspronkelijke gesynchroniseerde blok bestaat niet meer", + "You don't have access to this synced block": "Je hebt geen toegang tot dit gesynchroniseerde blok", + "Failed to load this synced block": "Dit gesynchroniseerde blok kon niet worden geladen", + "Fixed editor toolbar": "Vaste editorwerkbalk", + "Show a formatting toolbar above the editor with quick access to common actions.": "Toon een opmaakwerkbalk boven de editor met snelle toegang tot veelgebruikte acties.", + "Toggle fixed editor toolbar": "Vaste editorwerkbalk in-/uitschakelen", + "Normal text": "Normale tekst", + "More inline formatting": "Meer inline-opmaak", + "Subscript": "Subscript", + "Superscript": "Superscript", + "Inline code": "Inline-code", + "Insert media": "Media invoegen", + "Mention": "Vermelding", + "Emoji": "Emoji", + "Columns": "Kolommen", + "More inserts": "Meer invoegingen", + "Embeds": "Insluitingen", + "Diagrams": "Diagrammen", + "Advanced": "Geavanceerd", + "Utility": "Hulpprogramma's", + "Decrease indent": "Inspringing verkleinen", + "Increase indent": "Inspringing vergroten", + "Clear formatting": "Opmaak wissen", + "Code block": "Codeblok", + "Experimental": "Experimenteel", + "Strikethrough": "Doorhalen", + "Undo": "Ongedaan maken", + "Redo": "Opnieuw", + "Backlinks": "Terugkoppelingen", + "Last updated by": "Laatst bijgewerkt door", + "Last updated": "Laatst bijgewerkt", + "Stats": "Statistieken", + "Word count": "Aantal woorden", + "Characters": "Tekens", + "Incoming links": "Inkomende links", + "Outgoing links": "Uitgaande links", + "Incoming links ({{count}})": "Inkomende links ({{count}})", + "Outgoing links ({{count}})": "Uitgaande links ({{count}})", + "No pages link here yet.": "Er linken nog geen pagina's hiernaartoe.", + "This page doesn't link to other pages yet.": "Deze pagina linkt nog niet naar andere pagina's.", + "Verified until {{date}}": "Geverifieerd tot {{date}}", + "Labels": "Labels", + "Add label": "Label toevoegen", + "No labels yet": "Nog geen labels", + "Already added": "Al toegevoegd", + "Invalid label name": "Ongeldige labelnaam", + "No matches": "Geen overeenkomsten", + "Search or create…": "Zoeken of maken…", + "Remove label {{name}}": "Label {{name}} verwijderen", + "Failed to add label": "Label toevoegen mislukt", + "Failed to remove label": "Label verwijderen mislukt", + "No pages with this label": "Geen pagina's met dit label", + "Pages tagged with this label will appear here.": "Pagina's met dit label worden hier weergegeven.", + "No pages match your search.": "Geen pagina's komen overeen met je zoekopdracht.", + "Updated {{date}}": "Bijgewerkt op {{date}}", + "Cell actions": "Celacties", + "Column actions": "Kolomacties", + "Row actions": "Rijacties", + "Filter": "Filter", + "Page title": "Paginatitel", + "Page content": "Pagina-inhoud", + "Member actions": "Lidacties", + "Toggle password visibility": "Wachtwoordzichtbaarheid in-/uitschakelen", + "Send comment": "Reactie verzenden", + "Token actions": "Tokenacties", + "Template settings": "Sjablooninstellingen", + "Edit diagram": "Diagram bewerken", + "Edit embed": "Insluiting bewerken", + "Edit drawing": "Tekening bewerken", + "Delete equation": "Vergelijking verwijderen", + "Invite actions": "Uitnodigingsacties", + "Get started": "Aan de slag", + "* indicates required fields": "* geeft verplichte velden aan", + "List of spaces in this workspace": "Lijst met werkruimtes in deze workspace", + "Active sessions": "Actieve sessies", + "Add {{name}} to favorites": "{{name}} aan favorieten toevoegen", + "Remove {{name}} from favorites": "{{name}} uit favorieten verwijderen", + "Added to favorites": "Toegevoegd aan favorieten", + "Removed from favorites": "Verwijderd uit favorieten", + "Added {{name}} to favorites": "{{name}} toegevoegd aan favorieten", + "Removed {{name}} from favorites": "{{name}} verwijderd uit favorieten", + "Page menu for {{name}}": "Paginamenu voor {{name}}", + "Create subpage of {{name}}": "Subpagina van {{name}} maken" } diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index f71b2e59b..e2788f1fe 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -71,6 +71,7 @@ "Export": "Exportar", "Failed to create page": "Falha ao criar página", "Failed to delete page": "Falha ao excluir página", + "Failed to restore page": "Falha ao restaurar página", "Failed to fetch recent pages": "Falha ao buscar páginas recentes", "Failed to import pages": "Falha ao importar páginas", "Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.", @@ -276,6 +277,9 @@ "Align left": "Alinhar à esquerda", "Align right": "Alinhar à direita", "Align center": "Alinhar ao centro", + "Alt text": "Texto alternativo", + "Describe this for accessibility.": "Descreva isto para acessibilidade.", + "Add a description": "Adicionar uma descrição", "Justify": "Justificar", "Merge cells": "Mesclar células", "Split cell": "Dividir célula", @@ -286,6 +290,19 @@ "Add row above": "Adicionar linha acima", "Add row below": "Adicionar linha abaixo", "Delete table": "Excluir tabela", + "Add column left": "Adicionar coluna à esquerda", + "Add column right": "Adicionar coluna à direita", + "Clear cell": "Limpar célula", + "Clear cells": "Limpar células", + "Toggle header cell": "Alternar célula de cabeçalho", + "Toggle header column": "Alternar coluna de cabeçalho", + "Toggle header row": "Alternar linha de cabeçalho", + "Move column left": "Mover coluna para a esquerda", + "Move column right": "Mover coluna para a direita", + "Move row down": "Mover linha para baixo", + "Move row up": "Mover linha para cima", + "Sort A → Z": "Ordenar A → Z", + "Sort Z → A": "Ordenar Z → A", "Info": "Informação", "Note": "Observação", "Success": "Sucesso", @@ -348,6 +365,8 @@ "Create block quote.": "Crie uma citação em bloco.", "Insert code snippet.": "Insira um trecho de código.", "Insert horizontal rule divider": "Insira um divisor horizontal", + "Page break": "Quebra de página", + "Insert a page break for printing.": "Insira uma quebra de página para impressão.", "Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.", "Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Escreva...", "Column count": "Número de colunas", "{{count}} Columns": "{{count}} colunas", + "{{count}} command available_one": "1 comando disponível", + "{{count}} command available_other": "{{count}} comandos disponíveis", + "{{count}} result available_one": "1 resultado disponível", + "{{count}} result available_other": "{{count}} resultados disponíveis", "Equal columns": "Colunas iguais", "Left sidebar": "Barra lateral esquerda", "Right sidebar": "Barra lateral direita", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} está disponível", "Default page edit mode": "Modo padrão de edição da página", "Choose your preferred page edit mode. Avoid accidental edits.": "Escolha o modo de edição de página preferido. Evite edições acidentais.", + "Choose {{format}} file": "Escolher arquivo {{format}}", "Reading": "Leitura", "Delete member": "Excluir membro", "Member deleted successfully": "Membro excluído com sucesso", @@ -565,6 +589,8 @@ "Move to trash": "Mover para a lixeira", "Move this page to trash?": "Mover esta página para a lixeira?", "Restore page": "Restaurar página", + "Permanently delete": "Excluir permanentemente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} moveu esta página para a Lixeira {{time}}.", "Page moved to trash": "Página movida para a lixeira", "Page restored successfully": "Página restaurada com sucesso", "Deleted by": "Excluído por", @@ -608,25 +634,21 @@ "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", + "Update": "Atualizar", + "Update {{credential}}": "Atualizar {{credential}}", "Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho", "Restrict API key creation to admins": "Restringir a criação de chave de API aos administradores", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Somente administradores e proprietários podem criar novas chaves de API. As chaves de membros já existentes continuarão funcionando.", @@ -858,9 +880,12 @@ "AI Chat": "Chat com IA", "Analyze for insights": "Analisar para obter insights", "Ask anything...": "Pergunte qualquer coisa...", + "Assistant said:": "O assistente disse:", "Chat history": "Histórico de chats", "Chat name": "Nome do chat", + "Chat transcript": "Transcrição do chat", "Close": "Fechar", + "Copy assistant response": "Copiar resposta do assistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Falha ao carregar o chat. Ocorreu um erro.", "Failed to render this message.": "Falha ao renderizar esta mensagem.", @@ -870,9 +895,17 @@ "No chats found": "Nenhum chat encontrado", "No conversations yet": "Ainda não há conversas", "Open full page": "Abrir página inteira", + "Scroll to bottom": "Rolar até o fim", + "You said:": "Você disse:", "Previous 7 days": "Últimos 7 dias", "Previous 30 days": "Últimos 30 dias", "Search chats...": "Pesquisar chats...", + "Search chats": "Pesquisar chats", + "Ask anything... Use @ to mention pages": "Pergunte qualquer coisa... Use @ para mencionar páginas", + "Ask anything or search your workspace": "Pergunte qualquer coisa ou pesquise no seu workspace", + "Welcome to {{name}}": "Boas-vindas a {{name}}", + "Add files": "Adicionar arquivos", + "Mention a page": "Mencionar uma página", "Start a new chat to see it here.": "Inicie um novo chat para vê-lo aqui.", "Summarize this page": "Resumir esta página", "Toggle AI Chat": "Alternar chat com IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Tente um termo de pesquisa diferente.", "Try again": "Tentar novamente", "Untitled chat": "Chat sem título", - "What can I help you with?": "Com o que posso ajudar você?" + "What can I help you with?": "Com o que posso ajudar você?", + "Are you sure you want to revoke this {{credential}}": "Tem certeza de que deseja revogar esta {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Provisione automaticamente usuários e grupos do seu provedor de identidade via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure seu provedor de identidade com esta URL para provisionar usuários e grupos.", + "Create {{credential}}": "Criar {{credential}}", + "{{credential}} created": "{{credential}} criada", + "{{credential}} created successfully": "{{credential}} criada com sucesso", + "Created by": "Criado por", + "Custom": "Personalizado", + "Enable SCIM": "Ativar SCIM", + "Enter a descriptive name": "Insira um nome descritivo", + "I've saved my {{credential}}": "Salvei minha {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Copie sua {{credential}} agora. Você não poderá vê-la novamente!", + "Never": "Nunca", + "Revoke {{credential}}": "Revogar {{credential}}", + "SCIM endpoint URL": "URL do endpoint SCIM", + "SCIM provisioning": "Provisionamento SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "O SCIM tem precedência sobre a sincronização de grupos por SSO enquanto estiver ativado.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Você atingiu o máximo de {{max}} tokens SCIM. Exclua um token existente para criar um novo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Tokens SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Esta ação não pode ser desfeita. Seu provedor de identidade deixará de sincronizar imediatamente.", + "Toggle SCIM provisioning": "Alternar provisionamento SCIM", + "Token": "Token", + "Page menu": "Menu da página", + "Expand": "Expandir", + "Collapse": "Recolher", + "Comment menu": "Menu de comentários", + "Group menu": "Menu do grupo", + "Show hidden breadcrumbs": "Mostrar breadcrumbs ocultos", + "Breadcrumbs": "Trilhas de navegação", + "Page actions": "Ações da página", + "Pick emoji": "Escolher emoji", + "Template menu": "Menu do modelo", + "Use": "Usar", + "Use template": "Usar modelo", + "Preview template: {{title}}": "Visualizar modelo: {{title}}", + "Use a template": "Usar um modelo", + "Search templates...": "Pesquisar modelos...", + "Search spaces...": "Pesquisar espaços...", + "No templates found": "Nenhum modelo encontrado", + "No spaces found": "Nenhum espaço encontrado", + "Browse all templates": "Ver todos os modelos", + "This space": "Este espaço", + "All templates": "Todos os modelos", + "Global": "Global", + "New template": "Novo modelo", + "Edit template": "Editar modelo", + "Are you sure you want to delete this template?": "Tem certeza de que deseja excluir este modelo?", + "Template scope updated": "Escopo do modelo atualizado", + "Choose which space this template belongs to": "Escolha a qual espaço este modelo pertence", + "Scope": "Escopo", + "Select scope": "Selecionar escopo", + "Title": "Título", + "Saving...": "Salvando...", + "Saved": "Salvo", + "Save failed. Retry": "Falha ao salvar. Tentar novamente", + "By {{name}}": "Por {{name}}", + "Updated {{time}}": "Atualizado {{time}}", + "Choose destination": "Escolher destino", + "Search pages and spaces...": "Pesquisar páginas e espaços...", + "No results found": "Nenhum resultado encontrado", + "You don't have permission to create pages here": "Você não tem permissão para criar páginas aqui", + "Chat menu": "Menu do chat", + "API key menu": "Menu da chave de API", + "Jump to comment selection": "Ir para a seleção de comentários", + "Slash commands": "Comandos de barra", + "Mention suggestions": "Sugestões de menção", + "Link suggestions": "Sugestões de links", + "Diagram editor": "Editor de diagramas", + "Add comment": "Adicionar comentário", + "Find and replace": "Localizar e substituir", + "Main navigation": "Navegação principal", + "Space navigation": "Navegação do espaço", + "Settings navigation": "Navegação de configurações", + "AI navigation": "Navegação de IA", + "Breadcrumb": "Trilha de navegação", + "Synced block": "Bloco sincronizado", + "Create a block that stays in sync across pages.": "Crie um bloco que permaneça sincronizado entre páginas.", + "Editing original": "Editando original", + "Copy synced block": "Copiar bloco sincronizado", + "Unsync": "Desfazer sincronização", + "Delete synced block": "Excluir bloco sincronizado", + "Synced to {{count}} other page_one": "Sincronizado com {{count}} outra página", + "Synced to {{count}} other page_other": "Sincronizado com {{count}} outras páginas", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "ESTA PÁGINA", + "No pages": "Nenhuma página", + "The original synced block no longer exists": "O bloco sincronizado original não existe mais", + "You don't have access to this synced block": "Você não tem acesso a este bloco sincronizado", + "Failed to load this synced block": "Falha ao carregar este bloco sincronizado", + "Fixed editor toolbar": "Barra de ferramentas fixa do editor", + "Show a formatting toolbar above the editor with quick access to common actions.": "Mostre uma barra de ferramentas de formatação acima do editor com acesso rápido a ações comuns.", + "Toggle fixed editor toolbar": "Alternar barra de ferramentas fixa do editor", + "Normal text": "Texto normal", + "More inline formatting": "Mais formatação em linha", + "Subscript": "Subscrito", + "Superscript": "Sobrescrito", + "Inline code": "Código em linha", + "Insert media": "Inserir mídia", + "Mention": "Menção", + "Emoji": "Emoji", + "Columns": "Colunas", + "More inserts": "Mais inserções", + "Embeds": "Incorporações", + "Diagrams": "Diagramas", + "Advanced": "Avançado", + "Utility": "Utilitário", + "Decrease indent": "Diminuir recuo", + "Increase indent": "Aumentar recuo", + "Clear formatting": "Limpar formatação", + "Code block": "Bloco de código", + "Experimental": "Experimental", + "Strikethrough": "Tachado", + "Undo": "Desfazer", + "Redo": "Refazer", + "Backlinks": "Links de retorno", + "Last updated by": "Última atualização por", + "Last updated": "Última atualização", + "Stats": "Estatísticas", + "Word count": "Contagem de palavras", + "Characters": "Caracteres", + "Incoming links": "Links recebidos", + "Outgoing links": "Links de saída", + "Incoming links ({{count}})": "Links recebidos ({{count}})", + "Outgoing links ({{count}})": "Links de saída ({{count}})", + "No pages link here yet.": "Nenhuma página tem link para cá ainda.", + "This page doesn't link to other pages yet.": "Esta página ainda não tem links para outras páginas.", + "Verified until {{date}}": "Verificado até {{date}}", + "Labels": "Rótulos", + "Add label": "Adicionar rótulo", + "No labels yet": "Ainda não há rótulos", + "Already added": "Já adicionado", + "Invalid label name": "Nome de rótulo inválido", + "No matches": "Sem correspondências", + "Search or create…": "Pesquisar ou criar…", + "Remove label {{name}}": "Remover rótulo {{name}}", + "Failed to add label": "Falha ao adicionar rótulo", + "Failed to remove label": "Falha ao remover rótulo", + "No pages with this label": "Nenhuma página com este rótulo", + "Pages tagged with this label will appear here.": "As páginas marcadas com este rótulo aparecerão aqui.", + "No pages match your search.": "Nenhuma página corresponde à sua pesquisa.", + "Updated {{date}}": "Atualizado em {{date}}", + "Cell actions": "Ações da célula", + "Column actions": "Ações da coluna", + "Row actions": "Ações da linha", + "Filter": "Filtrar", + "Page title": "Título da página", + "Page content": "Conteúdo da página", + "Member actions": "Ações do membro", + "Toggle password visibility": "Alternar visibilidade da senha", + "Send comment": "Enviar comentário", + "Token actions": "Ações do token", + "Template settings": "Configurações do modelo", + "Edit diagram": "Editar diagrama", + "Edit embed": "Editar incorporação", + "Edit drawing": "Editar desenho", + "Delete equation": "Excluir equação", + "Invite actions": "Ações do convite", + "Get started": "Começar", + "* indicates required fields": "* indica campos obrigatórios", + "List of spaces in this workspace": "Lista de espaços neste workspace", + "Active sessions": "Sessões ativas", + "Add {{name}} to favorites": "Adicionar {{name}} aos favoritos", + "Remove {{name}} from favorites": "Remover {{name}} dos favoritos", + "Added to favorites": "Adicionado aos favoritos", + "Removed from favorites": "Removido dos favoritos", + "Added {{name}} to favorites": "{{name}} adicionado aos favoritos", + "Removed {{name}} from favorites": "{{name}} removido dos favoritos", + "Page menu for {{name}}": "Menu da página de {{name}}", + "Create subpage of {{name}}": "Criar subpágina de {{name}}" } diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index e5ac8fe85..3d93ec67d 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -71,6 +71,7 @@ "Export": "Экспорт", "Failed to create page": "Не удалось создать страницу", "Failed to delete page": "Не удалось удалить страницу", + "Failed to restore page": "Не удалось восстановить страницу", "Failed to fetch recent pages": "Не удалось получить недавние страницы", "Failed to import pages": "Не удалось импортировать страницы", "Failed to load page. An error occurred.": "Не удалось загрузить страницу. Произошла ошибка.", @@ -276,6 +277,9 @@ "Align left": "По левому краю", "Align right": "По правому краю", "Align center": "По центру", + "Alt text": "Альтернативный текст", + "Describe this for accessibility.": "Опишите это для специальных возможностей.", + "Add a description": "Добавить описание", "Justify": "По ширине", "Merge cells": "Объединить ячейки", "Split cell": "Разделить ячейку", @@ -286,6 +290,19 @@ "Add row above": "Добавить строку выше", "Add row below": "Добавить строку ниже", "Delete table": "Удалить таблицу", + "Add column left": "Добавить столбец слева", + "Add column right": "Добавить столбец справа", + "Clear cell": "Очистить ячейку", + "Clear cells": "Очистить ячейки", + "Toggle header cell": "Переключить ячейку заголовка", + "Toggle header column": "Переключить столбец заголовка", + "Toggle header row": "Переключить строку заголовка", + "Move column left": "Переместить столбец влево", + "Move column right": "Переместить столбец вправо", + "Move row down": "Переместить строку вниз", + "Move row up": "Переместить строку вверх", + "Sort A → Z": "Сортировать A → Я", + "Sort Z → A": "Сортировать Я → A", "Info": "Информация", "Note": "Примечание", "Success": "Успешно", @@ -348,6 +365,8 @@ "Create block quote.": "Создать блок цитирования.", "Insert code snippet.": "Вставить фрагмент кода.", "Insert horizontal rule divider": "Вставить горизонтальный разделитель", + "Page break": "Разрыв страницы", + "Insert a page break for printing.": "Вставить разрыв страницы для печати.", "Upload any image from your device.": "Загрузить любое изображение с вашего устройства.", "Upload any video from your device.": "Загрузить любое видео с вашего устройства.", "Upload any audio from your device.": "Загрузите любой аудиофайл с вашего устройства.", @@ -392,6 +411,10 @@ "Write...": "Напишите...", "Column count": "Количество столбцов", "{{count}} Columns": "{count, plural, one{# столбец} few{# столбца} many{# столбцов} other{# столбца}}", + "{{count}} command available_one": "Доступна 1 команда", + "{{count}} command available_other": "Доступно {{count}} команд", + "{{count}} result available_one": "Доступен 1 результат", + "{{count}} result available_other": "Доступно {{count}} результатов", "Equal columns": "Равные столбцы", "Left sidebar": "Левая боковая панель", "Right sidebar": "Правая боковая панель", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "Доступна версия {{latestVersion}}", "Default page edit mode": "Режим редактирования страницы по умолчанию", "Choose your preferred page edit mode. Avoid accidental edits.": "Выберите предпочитаемый режим редактирования страницы. Избегайте случайных изменений.", + "Choose {{format}} file": "Выберите файл {{format}}", "Reading": "Чтение", "Delete member": "Удалить участника", "Member deleted successfully": "Участник успешно удалён", @@ -565,6 +589,8 @@ "Move to trash": "Переместить в корзину", "Move this page to trash?": "Переместить эту страницу в корзину?", "Restore page": "Восстановить страницу", + "Permanently delete": "Удалить навсегда", + "{{name}} moved this page to Trash {{time}}.": "{{name}} переместил(а) эту страницу в корзину {{time}}.", "Page moved to trash": "Страница перемещена в корзину", "Page restored successfully": "Страница успешно восстановлена", "Deleted by": "Удалено пользователем", @@ -608,25 +634,21 @@ "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 ключ", + "Update": "Обновить", + "Update {{credential}}": "Обновить {{credential}}", "Manage API keys for all users in the workspace": "Управлять API ключами для всех пользователей в рабочей области", "Restrict API key creation to admins": "Ограничить создание API-ключей только администраторами.", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Только администраторы и владельцы могут создавать новые API-ключи. Существующие ключи участников продолжат работать.", @@ -858,9 +880,12 @@ "AI Chat": "Чат с ИИ", "Analyze for insights": "Проанализировать и получить выводы", "Ask anything...": "Спросите что угодно...", + "Assistant said:": "Ассистент ответил:", "Chat history": "История чатов", "Chat name": "Название чата", + "Chat transcript": "Расшифровка чата", "Close": "Закрыть", + "Copy assistant response": "Скопировать ответ ассистента", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Не удалось загрузить чат. Произошла ошибка.", "Failed to render this message.": "Не удалось отобразить это сообщение.", @@ -870,9 +895,17 @@ "No chats found": "Чаты не найдены", "No conversations yet": "Пока нет разговоров", "Open full page": "Открыть полную страницу", + "Scroll to bottom": "Прокрутить вниз", + "You said:": "Вы сказали:", "Previous 7 days": "Предыдущие 7 дней", "Previous 30 days": "Предыдущие 30 дней", "Search chats...": "Поиск чатов...", + "Search chats": "Поиск чатов", + "Ask anything... Use @ to mention pages": "Спросите что угодно... Используйте @, чтобы упомянуть страницы", + "Ask anything or search your workspace": "Спросите что угодно или выполните поиск по рабочему пространству", + "Welcome to {{name}}": "Добро пожаловать в {{name}}", + "Add files": "Добавить файлы", + "Mention a page": "Упомянуть страницу", "Start a new chat to see it here.": "Начните новый чат, чтобы увидеть его здесь.", "Summarize this page": "Суммировать эту страницу", "Toggle AI Chat": "Переключить чат с ИИ", @@ -880,5 +913,176 @@ "Try a different search term.": "Попробуйте другой поисковый запрос.", "Try again": "Попробовать снова", "Untitled chat": "Чат без названия", - "What can I help you with?": "Чем я могу вам помочь?" + "What can I help you with?": "Чем я могу вам помочь?", + "Are you sure you want to revoke this {{credential}}": "Вы уверены, что хотите отозвать этот {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Автоматически предоставляйте доступ пользователям и группам из вашего провайдера удостоверений через SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Настройте ваш провайдер удостоверений с этим URL для предоставления доступа пользователям и группам.", + "Create {{credential}}": "Создать {{credential}}", + "{{credential}} created": "{{credential}} создан", + "{{credential}} created successfully": "{{credential}} успешно создан", + "Created by": "Создан", + "Custom": "Пользовательский", + "Enable SCIM": "Включить SCIM", + "Enter a descriptive name": "Введите понятное имя", + "I've saved my {{credential}}": "Я сохранил свой {{credential}}", + "Important": "Важно", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Обязательно скопируйте ваш {{credential}} сейчас. Позже вы не сможете увидеть его снова!", + "Never": "Никогда", + "Revoke {{credential}}": "Отозвать {{credential}}", + "SCIM endpoint URL": "URL конечной точки SCIM", + "SCIM provisioning": "SCIM-подготовка учетных записей", + "SCIM takes precedence over SSO group sync while enabled.": "Пока SCIM включен, он имеет приоритет над синхронизацией групп через SSO.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Вы достигли максимального количества токенов SCIM: {{max}}. Удалите существующий токен, чтобы создать новый.", + "SCIM token": "Токен SCIM", + "SCIM tokens": "Токены SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Это действие нельзя отменить. Ваш провайдер удостоверений немедленно прекратит синхронизацию.", + "Toggle SCIM provisioning": "Переключить подготовку учетных записей SCIM", + "Token": "Токен", + "Page menu": "Меню страницы", + "Expand": "Развернуть", + "Collapse": "Свернуть", + "Comment menu": "Меню комментария", + "Group menu": "Меню группы", + "Show hidden breadcrumbs": "Показать скрытые хлебные крошки", + "Breadcrumbs": "Хлебные крошки", + "Page actions": "Действия со страницей", + "Pick emoji": "Выбрать эмодзи", + "Template menu": "Меню шаблона", + "Use": "Использовать", + "Use template": "Использовать шаблон", + "Preview template: {{title}}": "Предпросмотр шаблона: {{title}}", + "Use a template": "Использовать шаблон", + "Search templates...": "Поиск шаблонов...", + "Search spaces...": "Поиск пространств...", + "No templates found": "Шаблоны не найдены", + "No spaces found": "Пространства не найдены", + "Browse all templates": "Просмотреть все шаблоны", + "This space": "Это пространство", + "All templates": "Все шаблоны", + "Global": "Глобально", + "New template": "Новый шаблон", + "Edit template": "Редактировать шаблон", + "Are you sure you want to delete this template?": "Вы уверены, что хотите удалить этот шаблон?", + "Template scope updated": "Область действия шаблона обновлена", + "Choose which space this template belongs to": "Выберите, к какому пространству относится этот шаблон", + "Scope": "Область действия", + "Select scope": "Выберите область действия", + "Title": "Заголовок", + "Saving...": "Сохранение...", + "Saved": "Сохранено", + "Save failed. Retry": "Не удалось сохранить. Повторите попытку", + "By {{name}}": "От {{name}}", + "Updated {{time}}": "Обновлено {{time}}", + "Choose destination": "Выберите место назначения", + "Search pages and spaces...": "Поиск страниц и пространств...", + "No results found": "Результаты не найдены", + "You don't have permission to create pages here": "У вас нет прав на создание страниц здесь", + "Chat menu": "Меню чата", + "API key menu": "Меню API-ключа", + "Jump to comment selection": "Перейти к выбору комментария", + "Slash commands": "Команды со слешем", + "Mention suggestions": "Подсказки упоминаний", + "Link suggestions": "Подсказки ссылок", + "Diagram editor": "Редактор диаграмм", + "Add comment": "Добавить комментарий", + "Find and replace": "Найти и заменить", + "Main navigation": "Основная навигация", + "Space navigation": "Навигация по пространству", + "Settings navigation": "Навигация по настройкам", + "AI navigation": "Навигация ИИ", + "Breadcrumb": "Хлебная крошка", + "Synced block": "Синхронизированный блок", + "Create a block that stays in sync across pages.": "Создайте блок, который будет синхронизироваться между страницами.", + "Editing original": "Редактирование оригинала", + "Copy synced block": "Скопировать синхронизированный блок", + "Unsync": "Не синхронизировать", + "Delete synced block": "Удалить синхронизированный блок", + "Synced to {{count}} other page_one": "Синхронизировано с {{count}} с другой страницей", + "Synced to {{count}} other page_other": "Синхронизировано с {{count}} с другими страницами", + "ORIGINAL": "ОРИГИНАЛ", + "THIS PAGE": "ЭТА СТРАНИЦА", + "No pages": "Нет страниц", + "The original synced block no longer exists": "Исходный синхронизированный блок больше не существует", + "You don't have access to this synced block": "У вас нет доступа к этому синхронизированному блоку", + "Failed to load this synced block": "Не удалось загрузить этот синхронизированный блок", + "Fixed editor toolbar": "Закреплённая панель инструментов редактора", + "Show a formatting toolbar above the editor with quick access to common actions.": "Показывать панель форматирования над редактором для быстрого доступа к часто используемым действиям.", + "Toggle fixed editor toolbar": "Переключить закреплённую панель инструментов редактора", + "Normal text": "Обычный текст", + "More inline formatting": "Больше вариантов встроенного форматирования", + "Subscript": "Подстрочный", + "Superscript": "Надстрочный", + "Inline code": "Встроенный код", + "Insert media": "Вставить медиа", + "Mention": "Упоминание", + "Emoji": "Эмодзи", + "Columns": "Столбцы", + "More inserts": "Больше вариантов вставки", + "Embeds": "Встраивания", + "Diagrams": "Диаграммы", + "Advanced": "Дополнительно", + "Utility": "Служебное", + "Decrease indent": "Уменьшить отступ", + "Increase indent": "Увеличить отступ", + "Clear formatting": "Очистить форматирование", + "Code block": "Блок кода", + "Experimental": "Экспериментальное", + "Strikethrough": "Зачеркивание", + "Undo": "Отменить", + "Redo": "Повторить", + "Backlinks": "Обратные ссылки", + "Last updated by": "Последний изменивший", + "Last updated": "Последнее обновление", + "Stats": "Статистика", + "Word count": "Количество слов", + "Characters": "Символы", + "Incoming links": "Входящие ссылки", + "Outgoing links": "Исходящие ссылки", + "Incoming links ({{count}})": "Входящие ссылки ({{count}})", + "Outgoing links ({{count}})": "Исходящие ссылки ({{count}})", + "No pages link here yet.": "На эту страницу пока что нет ссылок.", + "This page doesn't link to other pages yet.": "Эта страница пока не содержит ссылок на другие страницы.", + "Verified until {{date}}": "Подтверждено до: {{date}}", + "Labels": "Метки", + "Add label": "Добавить метку", + "No labels yet": "Меток пока нет", + "Already added": "Уже добавлено", + "Invalid label name": "Недопустимое имя метки", + "No matches": "Совпадений нет", + "Search or create…": "Найти или создать…", + "Remove label {{name}}": "Удалить метку {{name}}", + "Failed to add label": "Не удалось добавить метку", + "Failed to remove label": "Не удалось удалить метку", + "No pages with this label": "Нет страниц с этой меткой", + "Pages tagged with this label will appear here.": "Здесь будут отображаться страницы с этой меткой.", + "No pages match your search.": "Нет страниц, соответствующих вашему запросу.", + "Updated {{date}}": "Обновлено {{date}}", + "Cell actions": "Действия с ячейкой", + "Column actions": "Действия со столбцом", + "Row actions": "Действия со строкой", + "Filter": "Фильтр", + "Page title": "Заголовок страницы", + "Page content": "Содержимое страницы", + "Member actions": "Действия с участником", + "Toggle password visibility": "Переключить видимость пароля", + "Send comment": "Отправить комментарий", + "Token actions": "Действия с токеном", + "Template settings": "Настройки шаблона", + "Edit diagram": "Редактировать диаграмму", + "Edit embed": "Редактировать встраивание", + "Edit drawing": "Редактировать рисунок", + "Delete equation": "Удалить уравнение", + "Invite actions": "Действия с приглашением", + "Get started": "Начать", + "* indicates required fields": "* обозначает обязательные поля", + "List of spaces in this workspace": "Список пространств в этом рабочем пространстве", + "Active sessions": "Активные сеансы", + "Add {{name}} to favorites": "Добавить {{name}} в избранное", + "Remove {{name}} from favorites": "Удалить {{name}} из избранного", + "Added to favorites": "Добавлено в избранное", + "Removed from favorites": "Удалено из избранного", + "Added {{name}} to favorites": "{{name}} добавлено в избранное", + "Removed {{name}} from favorites": "{{name}} удалено из избранного", + "Page menu for {{name}}": "Меню страницы для {{name}}", + "Create subpage of {{name}}": "Создать подстраницу для {{name}}" } diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index aff25b4ae..6144df469 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -71,6 +71,7 @@ "Export": "Експорт", "Failed to create page": "Не вдалося створити сторінку", "Failed to delete page": "Не вдалося видалити сторінку", + "Failed to restore page": "Не вдалося відновити сторінку", "Failed to fetch recent pages": "Не вдалося отримати нещодавні сторінки", "Failed to import pages": "Не вдалося імпортувати сторінки", "Failed to load page. An error occurred.": "Не вдалося завантажити сторінку. Сталася помилка.", @@ -276,6 +277,9 @@ "Align left": "По лівому краю", "Align right": "По правому краю", "Align center": "По центру", + "Alt text": "Альтернативний текст", + "Describe this for accessibility.": "Опишіть це для доступності.", + "Add a description": "Додати опис", "Justify": "По ширині", "Merge cells": "Об'єднати комірки", "Split cell": "Розділити комірку", @@ -286,6 +290,19 @@ "Add row above": "Додати рядок вище", "Add row below": "Додати рядок нижче", "Delete table": "Видалити таблицю", + "Add column left": "Додати стовпець ліворуч", + "Add column right": "Додати стовпець праворуч", + "Clear cell": "Очистити комірку", + "Clear cells": "Очистити комірки", + "Toggle header cell": "Перемкнути комірку заголовка", + "Toggle header column": "Перемкнути стовпець заголовка", + "Toggle header row": "Перемкнути рядок заголовка", + "Move column left": "Перемістити стовпець ліворуч", + "Move column right": "Перемістити стовпець праворуч", + "Move row down": "Перемістити рядок вниз", + "Move row up": "Перемістити рядок вгору", + "Sort A → Z": "Сортувати A → Z", + "Sort Z → A": "Сортувати Z → A", "Info": "Інформація", "Note": "Примітка", "Success": "Успішно", @@ -348,6 +365,8 @@ "Create block quote.": "Створити блок цитування.", "Insert code snippet.": "Вставити фрагмент коду.", "Insert horizontal rule divider": "Вставити горизонтальний роздільник", + "Page break": "Розрив сторінки", + "Insert a page break for printing.": "Вставте розрив сторінки для друку.", "Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.", "Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.", "Upload any audio from your device.": "Завантажте будь-який аудіофайл зі свого пристрою.", @@ -392,6 +411,10 @@ "Write...": "Напишіть...", "Column count": "Кількість колонок", "{{count}} Columns": "{count, plural, one{# колонка} few{# колонки} many{# колонок} other{# колонки}}", + "{{count}} command available_one": "Доступна 1 команда", + "{{count}} command available_other": "Доступно {{count}} команд", + "{{count}} result available_one": "Доступний 1 результат", + "{{count}} result available_other": "Доступно {{count}} результатів", "Equal columns": "Рівні колонки", "Left sidebar": "Ліва бічна панель", "Right sidebar": "Права бічна панель", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "Доступна версія {{latestVersion}}", "Default page edit mode": "Режим редагування сторінки за замовчуванням", "Choose your preferred page edit mode. Avoid accidental edits.": "Виберіть бажаний режим редагування сторінки. Уникайте випадкових редагувань.", + "Choose {{format}} file": "Виберіть файл {{format}}", "Reading": "Читання", "Delete member": "Видалити учасника", "Member deleted successfully": "Учасника успішно видалено", @@ -565,6 +589,8 @@ "Move to trash": "Перемістити в кошик", "Move this page to trash?": "Перемістити цю сторінку до кошика?", "Restore page": "Відновити сторінку", + "Permanently delete": "Видалити назавжди", + "{{name}} moved this page to Trash {{time}}.": "{{name}} перемістив цю сторінку до кошика {{time}}.", "Page moved to trash": "Сторінку переміщено в кошик", "Page restored successfully": "Сторінку успішно відновлено", "Deleted by": "Видалив", @@ -608,25 +634,21 @@ "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", + "Update": "Оновити", + "Update {{credential}}": "Оновити {{credential}}", "Manage API keys for all users in the workspace": "Керувати ключами API для всіх користувачів у робочій області", "Restrict API key creation to admins": "Обмежити створення API-ключів лише для адміністраторів", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Тільки адміністратори та власники можуть створювати нові API-ключі. Існуючі ключі учасників і надалі працюватимуть.", @@ -858,9 +880,12 @@ "AI Chat": "AI-чат", "Analyze for insights": "Проаналізувати для отримання висновків", "Ask anything...": "Запитайте що завгодно...", + "Assistant said:": "Помічник сказав:", "Chat history": "Історія чатів", "Chat name": "Назва чату", + "Chat transcript": "Стенограма чату", "Close": "Закрити", + "Copy assistant response": "Копіювати відповідь помічника", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Не вдалося завантажити чат. Сталася помилка.", "Failed to render this message.": "Не вдалося відобразити це повідомлення.", @@ -870,9 +895,17 @@ "No chats found": "Чатів не знайдено", "No conversations yet": "Розмов поки немає", "Open full page": "Відкрити повну сторінку", + "Scroll to bottom": "Прокрутити вниз", + "You said:": "Ви сказали:", "Previous 7 days": "Попередні 7 днів", "Previous 30 days": "Попередні 30 днів", "Search chats...": "Шукати чати...", + "Search chats": "Шукати чати", + "Ask anything... Use @ to mention pages": "Запитайте будь-що... Використовуйте @, щоб згадувати сторінки", + "Ask anything or search your workspace": "Запитайте будь-що або шукайте у своєму робочому просторі", + "Welcome to {{name}}": "Ласкаво просимо до {{name}}", + "Add files": "Додати файли", + "Mention a page": "Згадати сторінку", "Start a new chat to see it here.": "Почніть новий чат, щоб побачити його тут.", "Summarize this page": "Підсумувати цю сторінку", "Toggle AI Chat": "Перемкнути AI-чат", @@ -880,5 +913,176 @@ "Try a different search term.": "Спробуйте інший пошуковий запит.", "Try again": "Спробувати ще раз", "Untitled chat": "Чат без назви", - "What can I help you with?": "Чим я можу вам допомогти?" + "What can I help you with?": "Чим я можу вам допомогти?", + "Are you sure you want to revoke this {{credential}}": "Ви впевнені, що хочете відкликати цей {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Автоматично надавайте користувачів і групи від вашого постачальника ідентифікації через SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Налаштуйте свого постачальника ідентифікації за допомогою цієї URL-адреси для надання користувачів і груп.", + "Create {{credential}}": "Створити {{credential}}", + "{{credential}} created": "{{credential}} створено", + "{{credential}} created successfully": "{{credential}} успішно створено", + "Created by": "Створено", + "Custom": "Користувацький", + "Enable SCIM": "Увімкнути SCIM", + "Enter a descriptive name": "Введіть описову назву", + "I've saved my {{credential}}": "Я зберіг(ла) свій {{credential}}", + "Important": "Важливо", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Обов’язково скопіюйте свій {{credential}} зараз. Ви більше не зможете побачити його знову!", + "Never": "Ніколи", + "Revoke {{credential}}": "Відкликати {{credential}}", + "SCIM endpoint URL": "URL-адреса кінцевої точки SCIM", + "SCIM provisioning": "Надання SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM має пріоритет над синхронізацією груп SSO, коли його ввімкнено.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Ви досягли максимальної кількості токенів SCIM: {{max}}. Видаліть наявний токен, щоб створити новий.", + "SCIM token": "Токен SCIM", + "SCIM tokens": "Токени SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Цю дію не можна скасувати. Ваш постачальник ідентифікації негайно припинить синхронізацію.", + "Toggle SCIM provisioning": "Перемкнути надання SCIM", + "Token": "Токен", + "Page menu": "Меню сторінки", + "Expand": "Розгорнути", + "Collapse": "Згорнути", + "Comment menu": "Меню коментаря", + "Group menu": "Меню групи", + "Show hidden breadcrumbs": "Показати приховані \"хлібні крихти\"", + "Breadcrumbs": "\"Хлібні крихти\"", + "Page actions": "Дії сторінки", + "Pick emoji": "Вибрати емодзі", + "Template menu": "Меню шаблону", + "Use": "Використати", + "Use template": "Використати шаблон", + "Preview template: {{title}}": "Попередній перегляд шаблону: {{title}}", + "Use a template": "Використати шаблон", + "Search templates...": "Шукати шаблони...", + "Search spaces...": "Шукати простори...", + "No templates found": "Шаблони не знайдено", + "No spaces found": "Простори не знайдено", + "Browse all templates": "Переглянути всі шаблони", + "This space": "Цей простір", + "All templates": "Усі шаблони", + "Global": "Глобальний", + "New template": "Новий шаблон", + "Edit template": "Редагувати шаблон", + "Are you sure you want to delete this template?": "Ви впевнені, що хочете видалити цей шаблон?", + "Template scope updated": "Область дії шаблону оновлено", + "Choose which space this template belongs to": "Виберіть, до якого простору належить цей шаблон", + "Scope": "Область дії", + "Select scope": "Вибрати область дії", + "Title": "Назва", + "Saving...": "Збереження...", + "Saved": "Збережено", + "Save failed. Retry": "Не вдалося зберегти. Повторіть спробу", + "By {{name}}": "Від {{name}}", + "Updated {{time}}": "Оновлено {{time}}", + "Choose destination": "Вибрати місце призначення", + "Search pages and spaces...": "Шукати сторінки та простори...", + "No results found": "Результати не знайдено", + "You don't have permission to create pages here": "У вас немає дозволу на створення сторінок тут", + "Chat menu": "Меню чату", + "API key menu": "Меню ключа API", + "Jump to comment selection": "Перейти до вибору коментаря", + "Slash commands": "Слеш-команди", + "Mention suggestions": "Підказки згадок", + "Link suggestions": "Підказки посилань", + "Diagram editor": "Редактор діаграм", + "Add comment": "Додати коментар", + "Find and replace": "Знайти й замінити", + "Main navigation": "Основна навігація", + "Space navigation": "Навігація простору", + "Settings navigation": "Навігація налаштувань", + "AI navigation": "Навігація AI", + "Breadcrumb": "Хлібна крихта", + "Synced block": "Синхронізований блок", + "Create a block that stays in sync across pages.": "Створіть блок, який синхронізується між сторінками.", + "Editing original": "Редагування оригіналу", + "Copy synced block": "Скопіювати синхронізований блок", + "Unsync": "Скасувати синхронізацію", + "Delete synced block": "Видалити синхронізований блок", + "Synced to {{count}} other page_one": "Синхронізовано з {{count}} іншою сторінкою", + "Synced to {{count}} other page_other": "Синхронізовано з {{count}} іншими сторінками", + "ORIGINAL": "ОРИГІНАЛ", + "THIS PAGE": "ЦЯ СТОРІНКА", + "No pages": "Немає сторінок", + "The original synced block no longer exists": "Оригінальний синхронізований блок більше не існує", + "You don't have access to this synced block": "У вас немає доступу до цього синхронізованого блоку", + "Failed to load this synced block": "Не вдалося завантажити цей синхронізований блок", + "Fixed editor toolbar": "Закріплена панель інструментів редактора", + "Show a formatting toolbar above the editor with quick access to common actions.": "Показувати панель форматування над редактором для швидкого доступу до поширених дій.", + "Toggle fixed editor toolbar": "Перемкнути закріплену панель інструментів редактора", + "Normal text": "Звичайний текст", + "More inline formatting": "Більше вбудованого форматування", + "Subscript": "Нижній індекс", + "Superscript": "Верхній індекс", + "Inline code": "Вбудований код", + "Insert media": "Вставити медіа", + "Mention": "Згадка", + "Emoji": "Емодзі", + "Columns": "Стовпці", + "More inserts": "Більше вставок", + "Embeds": "Вбудовування", + "Diagrams": "Діаграми", + "Advanced": "Додатково", + "Utility": "Службові", + "Decrease indent": "Зменшити відступ", + "Increase indent": "Збільшити відступ", + "Clear formatting": "Очистити форматування", + "Code block": "Блок коду", + "Experimental": "Експериментальне", + "Strikethrough": "Закреслення", + "Undo": "Скасувати", + "Redo": "Повторити", + "Backlinks": "Зворотні посилання", + "Last updated by": "Востаннє оновив", + "Last updated": "Останнє оновлення", + "Stats": "Статистика", + "Word count": "Кількість слів", + "Characters": "Символи", + "Incoming links": "Вхідні посилання", + "Outgoing links": "Вихідні посилання", + "Incoming links ({{count}})": "Вхідні посилання ({{count}})", + "Outgoing links ({{count}})": "Вихідні посилання ({{count}})", + "No pages link here yet.": "Поки що жодна сторінка не посилається сюди.", + "This page doesn't link to other pages yet.": "Ця сторінка ще не містить посилань на інші сторінки.", + "Verified until {{date}}": "Перевірено до {{date}}", + "Labels": "Мітки", + "Add label": "Додати мітку", + "No labels yet": "Міток поки немає", + "Already added": "Уже додано", + "Invalid label name": "Некоректна назва мітки", + "No matches": "Немає збігів", + "Search or create…": "Шукати або створити…", + "Remove label {{name}}": "Видалити мітку {{name}}", + "Failed to add label": "Не вдалося додати мітку", + "Failed to remove label": "Не вдалося видалити мітку", + "No pages with this label": "Немає сторінок із цією міткою", + "Pages tagged with this label will appear here.": "Тут з’являться сторінки, позначені цією міткою.", + "No pages match your search.": "Немає сторінок, що відповідають вашому запиту.", + "Updated {{date}}": "Оновлено {{date}}", + "Cell actions": "Дії з коміркою", + "Column actions": "Дії зі стовпцем", + "Row actions": "Дії з рядком", + "Filter": "Фільтр", + "Page title": "Назва сторінки", + "Page content": "Вміст сторінки", + "Member actions": "Дії з учасником", + "Toggle password visibility": "Перемкнути видимість пароля", + "Send comment": "Надіслати коментар", + "Token actions": "Дії з токеном", + "Template settings": "Налаштування шаблону", + "Edit diagram": "Редагувати діаграму", + "Edit embed": "Редагувати вбудований елемент", + "Edit drawing": "Редагувати рисунок", + "Delete equation": "Видалити рівняння", + "Invite actions": "Дії із запрошенням", + "Get started": "Почати", + "* indicates required fields": "* позначає обов’язкові поля", + "List of spaces in this workspace": "Список просторів у цьому робочому просторі", + "Active sessions": "Активні сеанси", + "Add {{name}} to favorites": "Додати {{name}} до обраного", + "Remove {{name}} from favorites": "Видалити {{name}} з обраного", + "Added to favorites": "Додано до обраного", + "Removed from favorites": "Видалено з обраного", + "Added {{name}} to favorites": "{{name}} додано до обраного", + "Removed {{name}} from favorites": "{{name}} видалено з обраного", + "Page menu for {{name}}": "Меню сторінки для {{name}}", + "Create subpage of {{name}}": "Створити підсторінку для {{name}}" } diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index d8fd340d3..40cd5c1ba 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -71,6 +71,7 @@ "Export": "导出", "Failed to create page": "创建页面失败", "Failed to delete page": "删除页面失败", + "Failed to restore page": "恢复页面失败", "Failed to fetch recent pages": "获取最近页面失败", "Failed to import pages": "导入页面失败", "Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。", @@ -276,6 +277,9 @@ "Align left": "靠左对齐", "Align right": "靠右对齐", "Align center": "居中对齐", + "Alt text": "替代文本", + "Describe this for accessibility.": "为无障碍访问添加描述。", + "Add a description": "添加描述", "Justify": "两端对齐", "Merge cells": "合并单元格", "Split cell": "分割单元格", @@ -286,6 +290,19 @@ "Add row above": "在上方添加行", "Add row below": "在下方插入行", "Delete table": "删除表格", + "Add column left": "在左侧添加列", + "Add column right": "在右侧添加列", + "Clear cell": "清空单元格", + "Clear cells": "清空单元格", + "Toggle header cell": "切换标题单元格", + "Toggle header column": "切换标题列", + "Toggle header row": "切换标题行", + "Move column left": "左移列", + "Move column right": "右移列", + "Move row down": "下移行", + "Move row up": "上移行", + "Sort A → Z": "按 A → Z 排序", + "Sort Z → A": "按 Z → A 排序", "Info": "信息", "Note": "注意", "Success": "成功", @@ -348,6 +365,8 @@ "Create block quote.": "创建引用块", "Insert code snippet.": "插入代码片段", "Insert horizontal rule divider": "插入水平分割线", + "Page break": "分页符", + "Insert a page break for printing.": "插入一个用于打印的分页符。", "Upload any image from your device.": "从设备上传任何图像", "Upload any video from your device.": "从设备上传任何视频", "Upload any audio from your device.": "从您的设备上传任意音频文件。", @@ -392,6 +411,10 @@ "Write...": "写点内容...", "Column count": "列数", "{{count}} Columns": "{{count}} 列", + "{{count}} command available_one": "有 1 个可用命令", + "{{count}} command available_other": "有 {{count}} 个可用命令", + "{{count}} result available_one": "有 1 个可用结果", + "{{count}} result available_other": "有 {{count}} 个可用结果", "Equal columns": "等宽列", "Left sidebar": "左侧边栏", "Right sidebar": "右侧边栏", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} 可用", "Default page edit mode": "默认页面编辑模式", "Choose your preferred page edit mode. Avoid accidental edits.": "选择您偏好的页面编辑模式。避免意外编辑。", + "Choose {{format}} file": "选择 {{format}} 文件", "Reading": "阅读", "Delete member": "删除成员", "Member deleted successfully": "成员删除成功", @@ -565,6 +589,8 @@ "Move to trash": "移至回收站", "Move this page to trash?": "将此页面移至垃圾箱?", "Restore page": "恢复页面", + "Permanently delete": "永久删除", + "{{name}} moved this page to Trash {{time}}.": "{{name}} 于 {{time}} 将此页面移至回收站。", "Page moved to trash": "页面已移至回收站", "Page restored successfully": "页面恢复成功", "Deleted by": "删除者", @@ -608,25 +634,21 @@ "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密钥", + "Update": "更新", + "Update {{credential}}": "更新{{credential}}", "Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥", "Restrict API key creation to admins": "仅限管理员创建 API 密钥。", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "只有管理员和所有者可以创建新的 API 密钥。现有成员密钥将继续有效。", @@ -858,9 +880,12 @@ "AI Chat": "AI 聊天", "Analyze for insights": "分析并获取洞察", "Ask anything...": "随便问点什么...", + "Assistant said:": "助手说:", "Chat history": "聊天记录", "Chat name": "聊天名称", + "Chat transcript": "聊天记录", "Close": "关闭", + "Copy assistant response": "复制助手回复", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "加载聊天失败。发生错误。", "Failed to render this message.": "渲染此消息失败。", @@ -870,9 +895,17 @@ "No chats found": "未找到聊天", "No conversations yet": "暂无对话", "Open full page": "打开完整页面", + "Scroll to bottom": "滚动到底部", + "You said:": "你说:", "Previous 7 days": "前 7 天", "Previous 30 days": "前 30 天", "Search chats...": "搜索聊天...", + "Search chats": "搜索聊天", + "Ask anything... Use @ to mention pages": "询问任何内容……使用 @ 提及页面", + "Ask anything or search your workspace": "询问任何问题或搜索你的工作区", + "Welcome to {{name}}": "欢迎使用 {{name}}", + "Add files": "添加文件", + "Mention a page": "提及页面", "Start a new chat to see it here.": "开始新的聊天后会显示在这里。", "Summarize this page": "总结此页面", "Toggle AI Chat": "切换 AI 聊天", @@ -880,5 +913,176 @@ "Try a different search term.": "请尝试其他搜索词。", "Try again": "重试", "Untitled chat": "未命名聊天", - "What can I help you with?": "我能帮您做什么?" + "What can I help you with?": "我能帮您做什么?", + "Are you sure you want to revoke this {{credential}}": "确定要撤销此{{credential}}吗", + "Automatically provision users and groups from your identity provider via SCIM.": "通过 SCIM 从您的身份提供商自动预配用户和群组。", + "Configure your identity provider with this URL to provision users and groups.": "使用此 URL 配置您的身份提供商以预配用户和群组。", + "Create {{credential}}": "创建{{credential}}", + "{{credential}} created": "已创建{{credential}}", + "{{credential}} created successfully": "已成功创建{{credential}}", + "Created by": "创建者", + "Custom": "自定义", + "Enable SCIM": "启用 SCIM", + "Enter a descriptive name": "输入描述性名称", + "I've saved my {{credential}}": "我已保存我的{{credential}}", + "Important": "重要", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "请务必立即复制您的{{credential}}。之后您将无法再次查看!", + "Never": "从不", + "Revoke {{credential}}": "撤销{{credential}}", + "SCIM endpoint URL": "SCIM 端点 URL", + "SCIM provisioning": "SCIM 预配", + "SCIM takes precedence over SSO group sync while enabled.": "启用后,SCIM 的优先级高于 SSO 群组同步。", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "您已达到 {{max}} 个 SCIM 令牌的上限。请删除一个现有令牌以创建新令牌。", + "SCIM token": "SCIM 令牌", + "SCIM tokens": "SCIM 令牌", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "此操作无法撤销。您的身份提供商将立即停止同步。", + "Toggle SCIM provisioning": "切换 SCIM 预配", + "Token": "令牌", + "Page menu": "页面菜单", + "Expand": "展开", + "Collapse": "折叠", + "Comment menu": "评论菜单", + "Group menu": "群组菜单", + "Show hidden breadcrumbs": "显示隐藏的面包屑", + "Breadcrumbs": "面包屑", + "Page actions": "页面操作", + "Pick emoji": "选择表情符号", + "Template menu": "模板菜单", + "Use": "使用", + "Use template": "使用模板", + "Preview template: {{title}}": "预览模板:{{title}}", + "Use a template": "使用模板", + "Search templates...": "搜索模板……", + "Search spaces...": "搜索空间……", + "No templates found": "未找到模板", + "No spaces found": "未找到空间", + "Browse all templates": "浏览所有模板", + "This space": "此空间", + "All templates": "所有模板", + "Global": "全局", + "New template": "新建模板", + "Edit template": "编辑模板", + "Are you sure you want to delete this template?": "你确定要删除此模板吗?", + "Template scope updated": "模板范围已更新", + "Choose which space this template belongs to": "选择此模板所属的空间", + "Scope": "范围", + "Select scope": "选择范围", + "Title": "标题", + "Saving...": "正在保存……", + "Saved": "已保存", + "Save failed. Retry": "保存失败。重试", + "By {{name}}": "作者:{{name}}", + "Updated {{time}}": "更新于 {{time}}", + "Choose destination": "选择目标位置", + "Search pages and spaces...": "搜索页面和空间……", + "No results found": "未找到结果", + "You don't have permission to create pages here": "你无权在此处创建页面", + "Chat menu": "聊天菜单", + "API key menu": "API 密钥菜单", + "Jump to comment selection": "跳转到评论选择", + "Slash commands": "斜杠命令", + "Mention suggestions": "提及建议", + "Link suggestions": "链接建议", + "Diagram editor": "图表编辑器", + "Add comment": "添加评论", + "Find and replace": "查找和替换", + "Main navigation": "主导航", + "Space navigation": "空间导航", + "Settings navigation": "设置导航", + "AI navigation": "AI 导航", + "Breadcrumb": "面包屑", + "Synced block": "同步块", + "Create a block that stays in sync across pages.": "创建一个可在多个页面间保持同步的块。", + "Editing original": "正在编辑原始内容", + "Copy synced block": "复制同步块", + "Unsync": "取消同步", + "Delete synced block": "删除同步块", + "Synced to {{count}} other page_one": "已与另外 {{count}} 个页面同步", + "Synced to {{count}} other page_other": "已与另外 {{count}} 个页面同步", + "ORIGINAL": "原始内容", + "THIS PAGE": "此页面", + "No pages": "没有页面", + "The original synced block no longer exists": "原始同步块已不存在", + "You don't have access to this synced block": "你无权访问此同步块", + "Failed to load this synced block": "加载此同步块失败", + "Fixed editor toolbar": "固定编辑器工具栏", + "Show a formatting toolbar above the editor with quick access to common actions.": "在编辑器上方显示格式工具栏,便于快速访问常用操作。", + "Toggle fixed editor toolbar": "切换固定编辑器工具栏", + "Normal text": "普通文本", + "More inline formatting": "更多内联格式", + "Subscript": "下标", + "Superscript": "上标", + "Inline code": "行内代码", + "Insert media": "插入媒体", + "Mention": "提及", + "Emoji": "表情符号", + "Columns": "分栏", + "More inserts": "更多插入项", + "Embeds": "嵌入内容", + "Diagrams": "图表", + "Advanced": "高级", + "Utility": "实用工具", + "Decrease indent": "减少缩进", + "Increase indent": "增加缩进", + "Clear formatting": "清除格式", + "Code block": "代码块", + "Experimental": "实验性", + "Strikethrough": "删除线", + "Undo": "撤销", + "Redo": "重做", + "Backlinks": "反向链接", + "Last updated by": "最后更新者", + "Last updated": "最后更新", + "Stats": "统计", + "Word count": "字数", + "Characters": "字符数", + "Incoming links": "传入链接", + "Outgoing links": "传出链接", + "Incoming links ({{count}})": "传入链接({{count}})", + "Outgoing links ({{count}})": "传出链接({{count}})", + "No pages link here yet.": "还没有页面链接到这里。", + "This page doesn't link to other pages yet.": "此页面尚未链接到其他页面。", + "Verified until {{date}}": "验证有效期至 {{date}}", + "Labels": "标签", + "Add label": "添加标签", + "No labels yet": "还没有标签", + "Already added": "已添加", + "Invalid label name": "标签名称无效", + "No matches": "无匹配结果", + "Search or create…": "搜索或创建…", + "Remove label {{name}}": "移除标签 {{name}}", + "Failed to add label": "添加标签失败", + "Failed to remove label": "移除标签失败", + "No pages with this label": "没有带有此标签的页面", + "Pages tagged with this label will appear here.": "带有此标签的页面将显示在这里。", + "No pages match your search.": "没有页面匹配你的搜索。", + "Updated {{date}}": "更新于 {{date}}", + "Cell actions": "单元格操作", + "Column actions": "列操作", + "Row actions": "行操作", + "Filter": "筛选", + "Page title": "页面标题", + "Page content": "页面内容", + "Member actions": "成员操作", + "Toggle password visibility": "切换密码可见性", + "Send comment": "发送评论", + "Token actions": "令牌操作", + "Template settings": "模板设置", + "Edit diagram": "编辑图表", + "Edit embed": "编辑嵌入内容", + "Edit drawing": "编辑绘图", + "Delete equation": "删除公式", + "Invite actions": "邀请操作", + "Get started": "开始使用", + "* indicates required fields": "* 表示必填字段", + "List of spaces in this workspace": "此工作区中的空间列表", + "Active sessions": "活动会话", + "Add {{name}} to favorites": "将 {{name}} 添加到收藏", + "Remove {{name}} from favorites": "将 {{name}} 从收藏中移除", + "Added to favorites": "已添加到收藏", + "Removed from favorites": "已从收藏中移除", + "Added {{name}} to favorites": "已将 {{name}} 添加到收藏", + "Removed {{name}} from favorites": "已将 {{name}} 从收藏中移除", + "Page menu for {{name}}": "{{name}} 的页面菜单", + "Create subpage of {{name}}": "创建 {{name}} 的子页面" } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 1fa669c17..addd783b5 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -38,6 +38,7 @@ 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"; +import BasePage from "@/ee/base/pages/base-page.tsx"; import AuditLogs from "@/ee/audit/pages/audit-logs.tsx"; import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx"; import TemplateList from "@/ee/template/pages/template-list"; @@ -107,6 +108,8 @@ export default function App() { element={} /> + } /> + } /> { if (disabled) return; diff --git a/apps/client/src/components/common/copy.tsx b/apps/client/src/components/common/copy.tsx index 2144417b9..8bf4ec938 100644 --- a/apps/client/src/components/common/copy.tsx +++ b/apps/client/src/components/common/copy.tsx @@ -8,15 +8,19 @@ interface CopyProps { text: string; size?: MantineSize; color?: MantineColor; + /** Override the accessible name (and tooltip) when not yet copied. Lets callers disambiguate adjacent copy buttons for screen readers. */ + label?: string; } -export default function CopyTextButton({ text, size }: CopyProps) { +export default function CopyTextButton({ text, size, label }: CopyProps) { const { t } = useTranslation(); + const copyLabel = label ?? t("Copy"); + return ( {({ copied, copy }) => ( @@ -25,7 +29,7 @@ export default function CopyTextButton({ text, size }: CopyProps) { variant="subtle" onClick={copy} size={size} - aria-label={copied ? t("Copied") : t("Copy")} + aria-label={copied ? t("Copied") : copyLabel} > {copied ? : } diff --git a/apps/client/src/components/common/export-modal.tsx b/apps/client/src/components/common/export-modal.tsx index 53de82467..bbd58b64d 100644 --- a/apps/client/src/components/common/export-modal.tsx +++ b/apps/client/src/components/common/export-modal.tsx @@ -6,13 +6,21 @@ import { Select, Switch, Divider, + Tooltip, + Badge, } from "@mantine/core"; -import { exportPage } from "@/features/page/services/page-service.ts"; +import { + exportPage, + exportPageToDocx, +} from "@/features/page/services/page-service.ts"; import { useState } from "react"; import { ExportFormat } from "@/features/page/types/page.types.ts"; import { notifications } from "@mantine/notifications"; import { exportSpace } from "@/features/space/services/space-service"; import { useTranslation } from "react-i18next"; +import { Feature } from "@/ee/features"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; interface ExportModalProps { id: string; @@ -32,17 +40,25 @@ export default function ExportModal({ const [includeAttachments, setIncludeAttachments] = useState(false); const [isExporting, setIsExporting] = useState(false); const { t } = useTranslation(); + const upgradeLabel = useUpgradeLabel(); + const isDocx = format === ExportFormat.Docx; + const docxEntitled = useHasFeature(Feature.DOCX_EXPORT); + const blockedByLicense = isDocx && !docxEntitled; const handleExport = async () => { setIsExporting(true); try { if (type === "page") { - await exportPage({ - pageId: id, - format, - includeChildren, - includeAttachments, - }); + if (format === ExportFormat.Docx) { + await exportPageToDocx({ pageId: id }); + } else { + await exportPage({ + pageId: id, + format, + includeChildren, + includeAttachments, + }); + } } if (type === "space") { await exportSpace({ spaceId: id, format, includeAttachments }); @@ -81,17 +97,22 @@ export default function ExportModal({ {t(`Export ${type}`)} - +
{t("Format")}
- +
- {type === "page" && ( + {type === "page" && !isDocx && ( <> @@ -143,7 +164,16 @@ export default function ExportModal({ - + + +
@@ -154,23 +184,49 @@ export default function ExportModal({ interface ExportFormatSelection { format: ExportFormat; onChange: (value: string) => void; + includeDocx?: boolean; + docxEntitled?: boolean; } -function ExportFormatSelection({ format, onChange }: ExportFormatSelection) { +function ExportFormatSelection({ + format, + onChange, + includeDocx, + docxEntitled, +}: ExportFormatSelection) { const { t } = useTranslation(); + const data = [ + { value: "markdown", label: "Markdown" }, + { value: "html", label: "HTML" }, + ...(includeDocx + ? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }] + : []), + ]; + return ( { + setValue(e.currentTarget.value); + debouncedCommit(); + }} + onFocus={() => { + focusedRef.current = true; + }} + onBlur={() => { + focusedRef.current = false; + commit(); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + if (e.key === "Escape") { + setValue(page?.title ?? ""); + e.currentTarget.blur(); + } + }} + /> + ); +} diff --git a/apps/client/src/ee/base/components/base-table-skeleton.tsx b/apps/client/src/ee/base/components/base-table-skeleton.tsx new file mode 100644 index 000000000..cd7dfc6fd --- /dev/null +++ b/apps/client/src/ee/base/components/base-table-skeleton.tsx @@ -0,0 +1,92 @@ +import { Skeleton } from "@mantine/core"; +import gridClasses from "@/ee/base/styles/grid.module.css"; +import classes from "@/ee/base/styles/base-table-skeleton.module.css"; + +const ROW_NUMBER_WIDTH = 64; +const COLUMN_WIDTH = 180; +const DEFAULT_COLUMN_COUNT = 6; +const DEFAULT_ROW_COUNT = 10; + +// Deterministic widths prevent flicker between renders. +const CELL_WIDTH_RATIOS = [0.78, 0.62, 0.84, 0.55, 0.71, 0.66]; +const HEADER_WIDTH_RATIOS = [0.42, 0.58, 0.5, 0.64, 0.46, 0.54]; + +type BaseTableSkeletonProps = { + // Match the eventual content shape to avoid a jarring size jump on swap. + rows?: number; + columns?: number; +}; + +export function BaseTableSkeleton({ + rows = DEFAULT_ROW_COUNT, + columns = DEFAULT_COLUMN_COUNT, +}: BaseTableSkeletonProps = {}) { + const gridTemplateColumns = [ + `${ROW_NUMBER_WIDTH}px`, + ...Array.from({ length: columns }, () => `${COLUMN_WIDTH}px`), + ].join(" "); + + return ( +
+
+
+ + + +
+
+ + + + +
+
+ +
+
+
+
+ +
+
+ {Array.from({ length: columns }).map((_, colIndex) => ( +
+
+ + +
+
+ ))} + + {Array.from({ length: rows }).map((_, rowIndex) => ( +
+
+
+ +
+
+ {Array.from({ length: columns }).map((_, colIndex) => ( +
+
+ +
+
+ ))} +
+ ))} +
+
+
+ ); +} diff --git a/apps/client/src/ee/base/components/base-table.tsx b/apps/client/src/ee/base/components/base-table.tsx new file mode 100644 index 000000000..be449c3f9 --- /dev/null +++ b/apps/client/src/ee/base/components/base-table.tsx @@ -0,0 +1,70 @@ +import { GridContainer } from "@/ee/base/components/grid/grid-container"; +import { Table } from "@tanstack/react-table"; +import { + IBase, + IBaseRow, + IBaseView, +} from "@/ee/base/types/base.types"; + +type BaseTableProps = { + base: IBase; + rows: IBaseRow[]; + effectiveView: IBaseView | undefined; + table: Table; + pageId: string; + embedded?: boolean; + isFiltered: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onFetchNextPage: () => void; + onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; + onAddRow: (afterRowId?: string, focusPropertyId?: string) => void; + onColumnReorder: (columnId: string, finishIndex: number) => void; + onResizeEnd: () => void; + onRowReorder: ( + rowId: string, + targetRowId: string, + dropPosition: "above" | "below", + ) => void; + persistViewConfig: () => void; + scrollportRef: React.RefObject; + aboveBand?: React.ReactNode; +}; + +export function BaseTable({ + base, + rows: _rows, + table, + pageId, + embedded, + isFiltered, + hasNextPage, + isFetchingNextPage, + onFetchNextPage, + onCellUpdate, + onAddRow, + onColumnReorder, + onResizeEnd, + onRowReorder, + scrollportRef, + aboveBand, +}: BaseTableProps) { + return ( + + ); +} diff --git a/apps/client/src/ee/base/components/base-toolbar.tsx b/apps/client/src/ee/base/components/base-toolbar.tsx new file mode 100644 index 000000000..213ce5874 --- /dev/null +++ b/apps/client/src/ee/base/components/base-toolbar.tsx @@ -0,0 +1,295 @@ +import { useState, useCallback, useMemo } from "react"; +import { ActionIcon, Tooltip, Badge } from "@mantine/core"; +import { Table } from "@tanstack/react-table"; +import { + IconSortAscending, + IconFilter, + IconEye, + IconDownload, + IconArrowsDiagonal, + IconLayoutColumns, + IconAdjustments, +} from "@tabler/icons-react"; +import { notifications } from "@mantine/notifications"; +import { + IBase, + IBaseRow, + IBaseView, + ViewSortConfig, + FilterCondition, + FilterGroup, +} from "@/ee/base/types/base.types"; +import { exportBaseToCsv } from "@/ee/base/services/base-service"; +import { getApiErrorMessage } from "@/lib/api-error"; +import { ViewTabs } from "@/ee/base/components/views/view-tabs"; +import { ViewSortConfigPopover } from "@/ee/base/components/views/view-sort-config"; +import { ViewFilterConfigPopover } from "@/ee/base/components/views/view-filter-config"; +import { ViewPropertyVisibility } from "@/ee/base/components/views/view-property-visibility"; +import { KanbanGroupByPicker } from "@/ee/base/components/kanban/kanban-group-by-picker"; +import { KanbanCardProperties } from "@/ee/base/components/kanban/kanban-card-properties"; +import { useTranslation } from "react-i18next"; +import classes from "@/ee/base/styles/grid.module.css"; +import toolbarClasses from "@/ee/base/styles/base-toolbar.module.css"; + +type BaseToolbarProps = { + base: IBase; + activeView: IBaseView | undefined; + views: IBaseView[]; + table?: Table; + onViewChange: (viewId: string) => void; + onAddView?: () => void; + canAddView?: boolean; + onPersistViewConfig: () => void; + onDraftSortsChange: (sorts: ViewSortConfig[] | undefined) => void; + onDraftFiltersChange: (filter: FilterGroup | undefined) => void; + onExpand?: () => void; + getViewShareUrl?: (viewId: string) => string | null; +}; + +export function BaseToolbar({ + base, + activeView, + views, + table, + onViewChange, + onAddView, + canAddView, + onPersistViewConfig, + onDraftSortsChange, + onDraftFiltersChange, + onExpand, + getViewShareUrl, +}: BaseToolbarProps) { + const { t } = useTranslation(); + const [sortOpened, setSortOpened] = useState(false); + const [filterOpened, setFilterOpened] = useState(false); + const [propertiesOpened, setPropertiesOpened] = useState(false); + const [cardPropertiesOpened, setCardPropertiesOpened] = useState(false); + const [exporting, setExporting] = useState(false); + + const isKanban = activeView?.type === "kanban"; + + const handleExport = useCallback(async () => { + if (exporting) return; + setExporting(true); + try { + await exportBaseToCsv(base.id); + } catch (err) { + notifications.show({ + color: "red", + message: getApiErrorMessage(err, t("Failed to export CSV")), + }); + } finally { + setExporting(false); + } + }, [base.id, exporting, t]); + + const openToolbar = useCallback((panel: "sort" | "filter" | "properties") => { + setSortOpened(panel === "sort" ? (v) => !v : false); + setFilterOpened(panel === "filter" ? (v) => !v : false); + setPropertiesOpened(panel === "properties" ? (v) => !v : false); + }, []); + + const sorts = activeView?.config?.sorts ?? []; + const conditions = useMemo(() => { + const filter = activeView?.config?.filter; + if (!filter || filter.op !== "and") return []; + return filter.children.filter( + (c): c is FilterCondition => !("children" in c), + ); + }, [activeView?.config?.filter]); + + const hiddenPropertyCount = useMemo(() => { + if (!table) return 0; + const cols = table.getAllLeafColumns().filter((col) => col.id !== "__row_number"); + return cols.filter((col) => col.getCanHide() && !col.getIsVisible()).length; + }, [table, table?.getState().columnVisibility]); + + const handleSortsChange = useCallback( + (newSorts: ViewSortConfig[]) => { + onDraftSortsChange(newSorts.length > 0 ? newSorts : undefined); + }, + [onDraftSortsChange], + ); + + const handleFiltersChange = useCallback( + (newConditions: FilterCondition[]) => { + const filter: FilterGroup | undefined = + newConditions.length > 0 + ? { op: "and", children: newConditions } + : undefined; + onDraftFiltersChange(filter); + }, + [onDraftFiltersChange], + ); + + return ( +
+ + +
+ + + + + + + setFilterOpened(false)} + conditions={conditions} + properties={base.properties} + onChange={handleFiltersChange} + > + + 0 ? "blue" : "gray"} + onClick={() => openToolbar("filter")} + > + + {conditions.length > 0 && ( + + {conditions.length} + + )} + + + + + {isKanban && activeView && ( + <> + + + + + + + + + setCardPropertiesOpened(false)} + base={base} + view={activeView} + pageId={base.id} + > + + setCardPropertiesOpened((v) => !v)} + > + + + + + + )} + + {!isKanban && ( + <> + setSortOpened(false)} + sorts={sorts} + properties={base.properties} + onChange={handleSortsChange} + > + + 0 ? "blue" : "gray"} + onClick={() => openToolbar("sort")} + > + + {sorts.length > 0 && ( + + {sorts.length} + + )} + + + + + {table && ( + setPropertiesOpened(false)} + table={table} + properties={base.properties} + onPersist={onPersistViewConfig} + > + + 0 ? "blue" : "gray"} + onClick={() => openToolbar("properties")} + > + + {hiddenPropertyCount > 0 && ( + + {hiddenPropertyCount} + + )} + + + + )} + + )} + + {onExpand && ( + + + + + + )} +
+
+ ); +} diff --git a/apps/client/src/ee/base/components/base-view-draft-banner.tsx b/apps/client/src/ee/base/components/base-view-draft-banner.tsx new file mode 100644 index 000000000..cda3c9f15 --- /dev/null +++ b/apps/client/src/ee/base/components/base-view-draft-banner.tsx @@ -0,0 +1,45 @@ +import { Group, Button, Tooltip } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +type BaseViewDraftBannerProps = { + isDirty: boolean; + canSave: boolean; + onReset: () => void; + onSave: () => void; + saving: boolean; +}; + +export function BaseViewDraftBanner({ + isDirty, + canSave, + onReset, + onSave, + saving, +}: BaseViewDraftBannerProps) { + const { t } = useTranslation(); + if (!isDirty) return null; + return ( + + + {canSave && ( + + + + )} + + ); +} diff --git a/apps/client/src/ee/base/components/base-view.tsx b/apps/client/src/ee/base/components/base-view.tsx new file mode 100644 index 000000000..22612f265 --- /dev/null +++ b/apps/client/src/ee/base/components/base-view.tsx @@ -0,0 +1,541 @@ +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { Text, Stack } from "@mantine/core"; +import { useAtom } from "jotai"; +import { IconTable } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { notifications } from "@mantine/notifications"; +import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; +import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; +import { useBaseQuery } from "@/ee/base/queries/base-query"; +import { useBaseSocket } from "@/ee/base/hooks/use-base-socket"; +import { + FilterGroup, + ViewSortConfig, + EditingCell, + FocusedCell, + IBaseProperty, +} from "@/ee/base/types/base.types"; +import { + useBaseRowsQuery, + flattenRows, + useCreateRowMutation, + useUpdateRowMutation, + useReorderRowMutation, +} from "@/ee/base/queries/base-row-query"; +import { useUpdateViewMutation } from "@/ee/base/queries/base-view-query"; +import { + activeViewIdAtomFamily, + editingCellAtomFamily, + focusedCellAtomFamily, +} from "@/ee/base/atoms/base-atoms"; +import { useBaseTable } from "@/ee/base/hooks/use-base-table"; +import { isSystemPropertyType } from "@/ee/base/property-types/property-type.registry"; +import { useRowSelection } from "@/ee/base/hooks/use-row-selection"; +import useCurrentUser from "@/features/user/hooks/use-current-user"; +import { useHydrateCurrentUser } from "@/ee/base/reference/reference-store"; +import { useViewDraft } from "@/ee/base/hooks/use-view-draft"; +import { BaseToolbar } from "@/ee/base/components/base-toolbar"; +import { BaseViewDraftBanner } from "@/ee/base/components/base-view-draft-banner"; +import { BaseEmbedTitle } from "@/ee/base/components/base-embed-title"; +import { BaseTableSkeleton } from "@/ee/base/components/base-table-skeleton"; +import { ViewRenderer } from "@/ee/base/components/views/view-renderer"; +import { RowDetailModal } from "@/ee/base/components/row-detail-modal/row-detail-modal"; +import { useRowDetailModal } from "@/ee/base/hooks/use-row-detail-modal"; +import { BaseEditableProvider } from "@/ee/base/context/base-editable"; +import { RowExpandProvider } from "@/ee/base/context/row-expand"; +import { usePageQuery } from "@/features/page/queries/page-query"; +import { buildPageUrl } from "@/features/page/page.utils"; +import { getAppUrl } from "@/lib/config.ts"; +import { useNavigate } from "react-router-dom"; +import classes from "@/ee/base/styles/grid.module.css"; +import viewClasses from "@/ee/base/styles/base-view.module.css"; +import kanbanClasses from "@/ee/base/styles/kanban.module.css"; + +type BaseViewProps = { + pageId: string; + embedded?: boolean; + /** False makes the view read-only. Standalone passes page.permissions.canEdit; + * embedded ANDs that with the host editor's editability. */ + editable?: boolean; + titleSlot?: React.ReactNode; +}; + +export function BaseView({ pageId, embedded, editable = true, titleSlot }: BaseViewProps) { + const { t } = useTranslation(); + // Subscribe so other clients' edits, schema changes, and async-job completions reconcile into cache. + useBaseSocket(pageId); + const { data: base, isLoading: baseLoading, error: baseError } = + useBaseQuery(pageId); + + const navigate = useNavigate(); + const { data: page } = usePageQuery({ pageId }); + const handleExpand = useCallback(() => { + if (!page) return; + navigate(buildPageUrl(page.space?.slug, page.slugId, page.title)); + }, [navigate, page]); + + // Share URL for a specific view; always points at the standalone page where ?view= is honored. + const getViewShareUrl = useCallback( + (viewId: string) => + page + ? `${getAppUrl()}${buildPageUrl(page.space?.slug, page.slugId, page.title)}?view=${encodeURIComponent(viewId)}` + : null, + [page], + ); + + const [activeViewId, setActiveViewId] = useAtom( + activeViewIdAtomFamily(pageId), + ) as unknown as [string | null, (val: string | null) => void]; + + const [, setEditingCell] = useAtom( + editingCellAtomFamily(pageId), + ) as unknown as [EditingCell, (val: EditingCell) => void]; + + const [, setFocusedCell] = useAtom( + focusedCellAtomFamily(pageId), + ) as unknown as [FocusedCell, (val: FocusedCell) => void]; + + const views = useMemo( + () => + [...(base?.views ?? [])].sort((a, b) => + a.position < b.position ? -1 : a.position > b.position ? 1 : 0, + ), + [base?.views], + ); + const activeView = useMemo(() => { + if (!views.length) return undefined; + return views.find((v) => v.id === activeViewId) ?? views[0]; + }, [views, activeViewId]); + + const { data: currentUser } = useCurrentUser(); + useHydrateCurrentUser(pageId); + const { + effectiveFilter, + effectiveSorts, + isDirty, + setFilter: setDraftFilter, + setSorts: setDraftSorts, + reset: resetDraft, + buildPromotedConfig, + } = useViewDraft({ + userId: currentUser?.user.id, + pageId, + viewId: activeView?.id, + baselineFilter: activeView?.config?.filter, + baselineSorts: activeView?.config?.sorts, + }); + + // Baseline merged with local draft. Used for table state and toolbar badge counts. + // The real activeView remains the auto-persist baseline so drafts can't leak into layout writes. + const effectiveView = useMemo( + () => + activeView + ? { + ...activeView, + config: { + ...activeView.config, + filter: effectiveFilter, + sorts: effectiveSorts, + }, + } + : undefined, + [activeView, effectiveFilter, effectiveSorts], + ); + + const activeFilter = effectiveFilter; + const activeSorts = effectiveSorts; + + const canSave = editable; + + // Gate on base to avoid a "bland" list request before the active view's + // config resolves, which would double network traffic for sorted/filtered views. + const isKanban = activeView?.type === "kanban"; + + const { + data: rowsData, + isLoading: rowsLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useBaseRowsQuery(base && !isKanban ? pageId : undefined, activeFilter, activeSorts); + + const updateRowMutation = useUpdateRowMutation(); + const createRowMutation = useCreateRowMutation(); + const reorderRowMutation = useReorderRowMutation(); + const updateViewMutation = useUpdateViewMutation(); + + useEffect(() => { + if (activeView && activeViewId !== activeView.id) { + setActiveViewId(activeView.id); + } + }, [activeView, activeViewId, setActiveViewId]); + + // Deep link: apply ?view= once after views load; skip if the id is + // unrecognised so we fall back to the default without fighting a later tab switch. + const appliedViewParamRef = useRef(false); + useEffect(() => { + if (appliedViewParamRef.current || views.length === 0) return; + const viewParam = new URLSearchParams(window.location.search).get("view"); + if (viewParam && views.some((v) => v.id === viewParam)) { + setActiveViewId(viewParam); + } + appliedViewParamRef.current = true; + }, [views, setActiveViewId]); + + const { clear: clearSelection } = useRowSelection(pageId); + useEffect(() => { + clearSelection(); + }, [pageId, activeView?.id, clearSelection]); + + const scrollportRef = useRef(null); + + const rows = useMemo(() => { + const flat = flattenRows(rowsData); + // With an active sort the server returns rows in sort order via keyset + // pagination; re-sorting by position on the client would break it as more + // pages load. Position sort only applies when no view sort is active. + if (activeSorts && activeSorts.length > 0) { + return flat; + } + return flat.sort((a, b) => + a.position < b.position ? -1 : a.position > b.position ? 1 : 0, + ); + }, [rowsData, activeSorts]); + const rowsRef = useRef(rows); + rowsRef.current = rows; + + const { table, persistViewConfig } = useBaseTable(base, rows, effectiveView); + + const guardedPersistViewConfig = useCallback(() => { + if (!editable) return; + persistViewConfig(); + }, [editable, persistViewConfig]); + + // Mutation result objects change identity every render; only .mutate is + // stable. Rows are memoized on these callbacks' identities, so they must + // not churn with unrelated re-renders. + const updateRow = updateRowMutation.mutate; + const handleCellUpdate = useCallback( + (rowId: string, propertyId: string, value: unknown) => { + if (!editable) return; + updateRow({ + rowId, + pageId, + cells: { [propertyId]: value }, + }); + }, + [editable, pageId, updateRow], + ); + + const handleAddRow = useCallback( + (afterRowId?: string, focusPropertyId?: string) => { + if (!editable) return; + createRowMutation.mutate( + { pageId, ...(afterRowId ? { afterRowId } : {}) }, + { + onSuccess: (newRow) => { + let propertyId = focusPropertyId; + if (!propertyId) { + const firstEditable = table.getVisibleLeafColumns().find((col) => { + if (col.id === "__row_number") return false; + const prop = col.columnDef.meta?.property as + | IBaseProperty + | undefined; + return ( + !!prop && + prop.type !== "checkbox" && + !isSystemPropertyType(prop.type) + ); + }); + propertyId = ( + firstEditable?.columnDef.meta?.property as + | IBaseProperty + | undefined + )?.id; + } + if (propertyId) { + setEditingCell({ rowId: newRow.id, propertyId }); + setFocusedCell({ rowId: newRow.id, propertyId }); + } + }, + }, + ); + }, + [editable, pageId, createRowMutation, table, setEditingCell, setFocusedCell], + ); + + const handleViewChange = useCallback( + (viewId: string) => { + setActiveViewId(viewId); + }, + [setActiveViewId], + ); + + const handleColumnReorder = useCallback( + (columnId: string, finishIndex: number) => { + const order = table.getState().columnOrder; + const startIndex = order.indexOf(columnId); + if (startIndex === -1 || startIndex === finishIndex) return; + table.setColumnOrder(reorder({ list: order, startIndex, finishIndex })); + guardedPersistViewConfig(); + }, + [table, guardedPersistViewConfig], + ); + + const handleResizeEnd = useCallback(() => { + guardedPersistViewConfig(); + }, [guardedPersistViewConfig]); + + const handleDraftSortsChange = useCallback( + (sorts: ViewSortConfig[] | undefined) => { + setDraftSorts(sorts && sorts.length > 0 ? sorts : undefined); + }, + [setDraftSorts], + ); + + const handleDraftFiltersChange = useCallback( + (filter: FilterGroup | undefined) => { + setDraftFilter(filter); + }, + [setDraftFilter], + ); + + const handleSaveDraft = useCallback(async () => { + if (!activeView || !base) return; + // Preserves non-draft baseline fields (widths/order/visibility), overwrites only filter/sorts. + const config = buildPromotedConfig(activeView.config); + try { + await updateViewMutation.mutateAsync({ + viewId: activeView.id, + pageId: base.id, + config, + }); + resetDraft(); + notifications.show({ message: t("View updated for everyone") }); + } catch { + // useUpdateViewMutation shows a toast and rolls back; keep the draft so the user can retry. + } + }, [ + activeView, + base, + buildPromotedConfig, + resetDraft, + t, + updateViewMutation, + ]); + + const { openRowId, openRow, closeRow } = useRowDetailModal(pageId); + // openRow's identity tracks searchParams; rows subscribe to the expand + // context, so hand them a stable wrapper instead. + const openRowRef = useRef(openRow); + openRowRef.current = openRow; + const handleExpandRow = useCallback((rowId: string) => { + openRowRef.current(rowId); + }, []); + const handleRowNavigate = useCallback((rowId: string) => { + openRowRef.current(rowId, { replace: true }); + }, []); + + const reorderRow = reorderRowMutation.mutate; + const handleRowReorder = useCallback( + (rowId: string, targetRowId: string, dropPosition: "above" | "below") => { + if (!editable) return; + const remainingRows = rowsRef.current.filter((r) => r.id !== rowId); + const targetIndex = remainingRows.findIndex((r) => r.id === targetRowId); + if (targetIndex === -1) return; + + let lowerPos: string | null = null; + let upperPos: string | null = null; + if (dropPosition === "above") { + lowerPos = + targetIndex > 0 ? remainingRows[targetIndex - 1]?.position : null; + upperPos = remainingRows[targetIndex]?.position ?? null; + } else { + lowerPos = remainingRows[targetIndex]?.position ?? null; + upperPos = + targetIndex < remainingRows.length - 1 + ? remainingRows[targetIndex + 1]?.position + : null; + } + + try { + let newPosition: string; + if (lowerPos && upperPos && lowerPos === upperPos) { + newPosition = generateJitteredKeyBetween(lowerPos, null); + } else { + newPosition = generateJitteredKeyBetween(lowerPos, upperPos); + } + reorderRow({ rowId, pageId, position: newPosition }); + } catch { + // Position computation failed; skip silently. + } + }, + [editable, pageId, reorderRow], + ); + + if (baseLoading || (!isKanban && rowsLoading)) { + return ; + } + if (baseError) { + return ( + + + {t("Failed to load base")} + + ); + } + if (!base) return null; + + // Ghost rows are an "empty base" affordance, not a "filter matched nothing" state. + const isFiltered = (activeFilter?.children?.length ?? 0) > 0; + + const banner = ( + + ); + + const toolbar = ( + + ); + + const kanbanBand = ( +
+ {embedded ? null : titleSlot} + {banner} + {toolbar} + {embedded ? : null} +
+ ); + + const viewRenderer = (folded: React.ReactNode) => ( + + ); + + if (embedded) { + if (isKanban) { + return ( + + + {kanbanBand} + {viewRenderer(null)} + + + + ); + } + + // Banner and toolbar go into aboveBand so they scroll with the host document; + // only the column-header row stays pinned (via --sticky-band-top). + return ( + + + {viewRenderer( + <> + {banner} + {toolbar} + + , + )} + + + + ); + } + + if (isKanban) { + return ( + +
+ + {kanbanBand} + {viewRenderer(null)} + +
+ +
+ ); + } + + // Standalone: title, banner, and toolbar go in aboveBand inside the scroll + // container so they scroll away; only the column-header row stays pinned. + return ( + +
+
+ + {viewRenderer( + <> + {titleSlot} + {banner} + {toolbar} + , + )} + +
+
+ +
+ ); +} diff --git a/apps/client/src/ee/base/components/cells/badge-overflow.tsx b/apps/client/src/ee/base/components/cells/badge-overflow.tsx new file mode 100644 index 000000000..278b84472 --- /dev/null +++ b/apps/client/src/ee/base/components/cells/badge-overflow.tsx @@ -0,0 +1,100 @@ +import { ReactElement, useLayoutEffect, useRef, useState } from "react"; +import { Tooltip } from "@mantine/core"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +export function computeVisibleBadgeCount( + itemWidths: number[], + gap: number, + available: number, + badgeWidth: number, +): number { + const count = itemWidths.length; + if (count === 0) return 0; + if (available <= 0) return count; + + let lineWidth = 0; + for (let i = 0; i < count; i++) { + lineWidth += itemWidths[i] + (i > 0 ? gap : 0); + } + if (lineWidth <= available) return count; + + let used = 0; + let fit = 0; + for (let i = 0; i < count; i++) { + const advance = itemWidths[i] + (i > 0 ? gap : 0); + if (used + advance + gap + badgeWidth <= available) { + used += advance; + fit = i + 1; + } else { + break; + } + } + return Math.max(fit, 1); +} + +const BADGE_GAP = 4; + +type BadgeOverflowListProps = { + chips: ReactElement[]; + measureKey: string; + tooltipLabel?: string; +}; + +export function BadgeOverflowList({ + chips, + measureKey, + tooltipLabel, +}: BadgeOverflowListProps) { + const containerRef = useRef(null); + const measureRef = useRef(null); + const [visibleCount, setVisibleCount] = useState(chips.length); + + useLayoutEffect(() => { + const container = containerRef.current; + const measure = measureRef.current; + if (!container || !measure) return; + + const recompute = () => { + const nodes = Array.from(measure.children) as HTMLElement[]; + const chipWidths = nodes.slice(0, -1).map((n) => n.offsetWidth); + const badgeWidth = nodes[nodes.length - 1]?.offsetWidth ?? 0; + setVisibleCount( + computeVisibleBadgeCount( + chipWidths, + BADGE_GAP, + container.clientWidth, + badgeWidth, + ), + ); + }; + + recompute(); + const observer = new ResizeObserver(recompute); + observer.observe(container); + return () => observer.disconnect(); + }, [measureKey]); + + const visible = chips.slice(0, visibleCount); + const overflow = chips.length - visibleCount; + + return ( + +
+
+ {chips} + +{chips.length} +
+ {visible} + {overflow > 0 && ( + +{overflow} + )} +
+
+ ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-checkbox.tsx b/apps/client/src/ee/base/components/cells/cell-checkbox.tsx new file mode 100644 index 000000000..163359dbd --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-checkbox.tsx @@ -0,0 +1,44 @@ +import { useCallback } from "react"; +import { Checkbox } from "@mantine/core"; +import { IBaseProperty } from "@/ee/base/types/base.types"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellCheckboxProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + readOnly?: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function CellCheckbox({ value, readOnly, onCommit }: CellCheckboxProps) { + const checked = value === true; + + const handleChange = useCallback(() => { + if (readOnly) return; + onCommit(!checked); + }, [readOnly, checked, onCommit]); + + return ( +
+ {}} + size="xs" + tabIndex={-1} + styles={{ + input: { + cursor: readOnly ? "default" : "pointer", + pointerEvents: "none", + }, + }} + /> +
+ ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-created-at.tsx b/apps/client/src/ee/base/components/cells/cell-created-at.tsx new file mode 100644 index 000000000..21286843c --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-created-at.tsx @@ -0,0 +1,22 @@ +import { IBaseProperty } from "@/ee/base/types/base.types"; +import { formatTimestamp } from "@/ee/base/formatters/cell-formatters"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellCreatedAtProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function CellCreatedAt({ value }: CellCreatedAtProps) { + const formatted = formatTimestamp(typeof value === "string" ? value : null); + + if (!formatted) { + return ; + } + + return {formatted}; +} diff --git a/apps/client/src/ee/base/components/cells/cell-date.tsx b/apps/client/src/ee/base/components/cells/cell-date.tsx new file mode 100644 index 000000000..a4af25a15 --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-date.tsx @@ -0,0 +1,146 @@ +import { useCallback } from "react"; +import { Popover } from "@mantine/core"; +import { DatePicker } from "@mantine/dates"; +import { + IBaseProperty, + DateTypeOptions, +} from "@/ee/base/types/base.types"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellDateProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function formatDateDisplay( + dateStr: string | null | undefined, + options: DateTypeOptions | undefined, +): string { + if (!dateStr) return ""; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return ""; + + const months = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + const month = months[date.getMonth()]; + const day = date.getDate(); + const year = date.getFullYear(); + + let result = `${month} ${day}, ${year}`; + + if (options?.includeTime) { + if (options.timeFormat === "24h") { + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + result += ` ${hours}:${minutes}`; + } else { + let hours = date.getHours(); + const ampm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12 || 12; + const minutes = String(date.getMinutes()).padStart(2, "0"); + result += ` ${hours}:${minutes} ${ampm}`; + } + } + + return result; + } catch { + return ""; + } +} + +function toISODateString(dateStr: string | null): string | null { + if (!dateStr) return null; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return null; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } catch { + return null; + } +} + +export function CellDate({ + value, + property, + isEditing, + onCommit, + onCancel, +}: CellDateProps) { + const typeOptions = property.typeOptions as DateTypeOptions | undefined; + const dateStr = typeof value === "string" ? value : null; + const pickerValue = toISODateString(dateStr); + + const handleChange = useCallback( + (selected: string | null) => { + if (selected) { + const date = new Date(selected); + onCommit(date.toISOString()); + } else { + onCommit(null); + } + }, + [onCommit], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onCancel(); + } + }, + [onCancel], + ); + + if (isEditing) { + return ( + { + if (!o) onCancel(); + }} + onClose={onCancel} + position="bottom-start" + width="auto" + trapFocus + closeOnClickOutside + closeOnEscape + > + +
+ + {formatDateDisplay(dateStr, typeOptions)} + +
+
+ + + +
+ ); + } + + if (!dateStr) { + return ; + } + + return ( + + {formatDateDisplay(dateStr, typeOptions)} + + ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-email.tsx b/apps/client/src/ee/base/components/cells/cell-email.tsx new file mode 100644 index 000000000..0d93f69fa --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-email.tsx @@ -0,0 +1,61 @@ +import { IBaseProperty } from "@/ee/base/types/base.types"; +import { Tooltip } from "@mantine/core"; +import { useEditableTextCell } from "@/ee/base/hooks/use-editable-text-cell"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellEmailProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +const toDraft = (value: unknown) => (typeof value === "string" ? value : ""); +const parse = (draft: string) => draft || null; + +export function CellEmail({ value, property, rowId, isEditing, onCommit, onCancel }: CellEmailProps) { + const { draft, setDraft, inputRef, handleKeyDown, handleBlur } = + useEditableTextCell({ + value, + isEditing, + onCommit, + onCancel, + toDraft, + parse, + rowId, + propertyId: property.id, + }); + + if (isEditing) { + return ( + setDraft(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleBlur} + /> + ); + } + + const displayValue = toDraft(value); + if (!displayValue) { + return ; + } + return ( + + e.stopPropagation()} + > + {displayValue} + + + ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-file.tsx b/apps/client/src/ee/base/components/cells/cell-file.tsx new file mode 100644 index 000000000..3d3bcd247 --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-file.tsx @@ -0,0 +1,236 @@ +import { useState, useRef, useCallback } from "react"; +import { Popover, ActionIcon, Text, UnstyledButton } from "@mantine/core"; +import { + IconPaperclip, + IconUpload, + IconFile, + IconX, +} from "@tabler/icons-react"; +import { IBaseProperty } from "@/ee/base/types/base.types"; +import cellClasses from "@/ee/base/styles/cells.module.css"; +import { uploadFile } from "@/features/page/services/page-service"; +import { getFileUrl } from "@/lib/config"; + +export type FileValue = { + id: string; + fileName: string; + mimeType?: string; + fileSize?: number; + url?: string; +}; + +function buildFileUrl(file: Pick): string { + return file.url ?? `/api/files/${file.id}/${encodeURIComponent(file.fileName)}`; +} + +type CellFileProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + readOnly?: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +function formatFileSize(bytes?: number): string { + if (!bytes) return ""; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function parseFiles(value: unknown): FileValue[] { + if (!Array.isArray(value)) return []; + return value.filter( + (f): f is FileValue => + f && typeof f === "object" && "id" in f && "fileName" in f, + ); +} + +export function CellFile({ + value, + property, + isEditing, + readOnly, + onCommit, + onCancel, +}: CellFileProps) { + const files = parseFiles(value); + const fileInputRef = useRef(null); + const [uploading, setUploading] = useState(false); + + const handleRemove = useCallback( + (fileId: string) => { + if (readOnly) return; + const updated = files.filter((f) => f.id !== fileId); + onCommit(updated.length > 0 ? updated : null); + }, + [readOnly, files, onCommit], + ); + + const handleUpload = useCallback( + async (fileList: FileList | null) => { + if (!fileList || fileList.length === 0) return; + setUploading(true); + + const newFiles: FileValue[] = [...files]; + + // Reuse the page-attachment upload pipeline: the base's pageId is passed + // to the standard /files/upload endpoint, which enforces the same edit + // access check as any other page attachment. + for (const file of Array.from(fileList)) { + try { + const attachment = await uploadFile(file, property.pageId); + newFiles.push({ + id: attachment.id, + fileName: attachment.fileName, + mimeType: attachment.mimeType, + fileSize: attachment.fileSize, + url: `/api/files/${attachment.id}/${encodeURIComponent(attachment.fileName)}`, + }); + } catch (err) { + console.error("File upload failed:", err); + } + } + + setUploading(false); + onCommit(newFiles.length > 0 ? newFiles : null); + }, + [files, property.pageId, onCommit], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onCancel(); + } + }, + [onCancel], + ); + + const MAX_VISIBLE = 2; + + if (isEditing) { + return ( + { + if (!o) onCancel(); + }} + onClose={onCancel} + position="bottom-start" + width={280} + trapFocus + closeOnClickOutside + closeOnEscape + hideDetached={false} + > + +
+ +
+
+ + {!readOnly && files.length === 0 && !uploading && ( + + No files attached + + )} + + {files.map((file) => ( + + ))} + + {!readOnly && ( + <> + { + handleUpload(e.target.files); + e.target.value = ""; + }} + /> + + fileInputRef.current?.click()} + disabled={uploading} + className={cellClasses.fileUploadBtn} + style={{ + color: uploading + ? "var(--mantine-color-gray-5)" + : "var(--mantine-color-blue-6)", + }} + > + + {uploading ? "Uploading..." : "Add file"} + + + )} + +
+ ); + } + + if (files.length === 0) { + return ; + } + + return ; +} + +function FileList({ + files, + maxVisible, +}: { + files: FileValue[]; + maxVisible: number; +}) { + const visible = files.slice(0, maxVisible); + const overflow = files.length - maxVisible; + + return ( +
+ {visible.map((file) => ( + + + {file.fileName} + + ))} + {overflow > 0 && ( + +{overflow} + )} +
+ ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-formula.tsx b/apps/client/src/ee/base/components/cells/cell-formula.tsx new file mode 100644 index 000000000..bdaaf11cf --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-formula.tsx @@ -0,0 +1,38 @@ +import { Badge, Tooltip } from "@mantine/core"; +import { + IBaseProperty, + isFormulaErrorCell, +} from "@/ee/base/types/base.types"; +import { CellText } from "./cell-text"; +import { CellNumber } from "./cell-number"; +import { CellCheckbox } from "./cell-checkbox"; +import { CellDate } from "./cell-date"; + +type Props = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function CellFormula(props: Props) { + const { value, property } = props; + if (isFormulaErrorCell(value)) { + return ( + + + #ERROR + + + ); + } + const opts = (property.typeOptions ?? {}) as { resultType?: string }; + const resultType = opts.resultType ?? "null"; + const readOnlyProps = { ...props, isEditing: false }; + if (resultType === "number") return ; + if (resultType === "boolean") return ; + if (resultType === "date") return ; + return ; +} diff --git a/apps/client/src/ee/base/components/cells/cell-last-edited-at.tsx b/apps/client/src/ee/base/components/cells/cell-last-edited-at.tsx new file mode 100644 index 000000000..efad990de --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-last-edited-at.tsx @@ -0,0 +1,22 @@ +import { IBaseProperty } from "@/ee/base/types/base.types"; +import { formatTimestamp } from "@/ee/base/formatters/cell-formatters"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellLastEditedAtProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function CellLastEditedAt({ value }: CellLastEditedAtProps) { + const formatted = formatTimestamp(typeof value === "string" ? value : null); + + if (!formatted) { + return ; + } + + return {formatted}; +} diff --git a/apps/client/src/ee/base/components/cells/cell-last-edited-by.tsx b/apps/client/src/ee/base/components/cells/cell-last-edited-by.tsx new file mode 100644 index 000000000..b6dc90c52 --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-last-edited-by.tsx @@ -0,0 +1,41 @@ +import { Group, Tooltip } from "@mantine/core"; +import { IBaseProperty } from "@/ee/base/types/base.types"; +import { useReferenceStore } from "@/ee/base/reference/reference-store"; +import { CustomAvatar } from "@/components/ui/custom-avatar"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellLastEditedByProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onCancel: () => void; +}; + +export function CellLastEditedBy({ value, property }: CellLastEditedByProps) { + const userId = typeof value === "string" ? value : null; + + const store = useReferenceStore(property.pageId); + const user = userId ? store.users[userId] ?? null : null; + + if (!userId) { + return ; + } + + const name = user?.name ?? userId.substring(0, 8); + + return ( + + + + {name} + + + ); +} diff --git a/apps/client/src/ee/base/components/cells/cell-long-text.tsx b/apps/client/src/ee/base/components/cells/cell-long-text.tsx new file mode 100644 index 000000000..2c3900562 --- /dev/null +++ b/apps/client/src/ee/base/components/cells/cell-long-text.tsx @@ -0,0 +1,151 @@ +import { useEffect, useRef, useState } from "react"; +import { Popover, Textarea, Group, CloseButton, Tooltip } from "@mantine/core"; +import { useDebouncedCallback } from "@mantine/hooks"; +import { IBaseProperty } from "@/ee/base/types/base.types"; +import { formatLongTextPreview } from "@/ee/base/formatters/cell-formatters"; +import cellClasses from "@/ee/base/styles/cells.module.css"; + +type CellLongTextProps = { + value: unknown; + property: IBaseProperty; + rowId: string; + isEditing: boolean; + onCommit: (value: unknown) => void; + onValueChange: (value: unknown) => void; + onCancel: () => void; + onTabNavigate?: (shiftKey: boolean) => void; +}; + +const toText = (value: unknown) => (typeof value === "string" ? value : ""); +const normalize = (s: string) => { + const trimmed = s.trim(); + return trimmed.length ? trimmed : null; +}; + +export function CellLongText({ + value, + isEditing, + onCommit, + onValueChange, + onCancel, + onTabNavigate, +}: CellLongTextProps) { + const [draft, setDraft] = useState(() => toText(value)); + const cancelledRef = useRef(false); + const committedRef = useRef(false); + const wasEditingRef = useRef(false); + const textareaRef = useRef(null); + + // Seed draft and focus on the false->true editing transition only; ignore + // value changes mid-edit so the user's typing is not clobbered. + useEffect(() => { + if (isEditing && !wasEditingRef.current) { + cancelledRef.current = false; + committedRef.current = false; + setDraft(toText(value)); + requestAnimationFrame(() => { + const el = textareaRef.current; + if (!el) return; + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); + }); + } + wasEditingRef.current = isEditing; + }, [isEditing, value]); + + // Autosave after a typing pause; commit/cancel clear the pending fire so + // a closed editor can never write a stale or discarded draft. + const debouncedAutosave = useDebouncedCallback(() => { + onValueChange(normalize(draft)); + }, 10_000); + + const commit = () => { + if (committedRef.current) return; + committedRef.current = true; + debouncedAutosave.cancel(); + onCommit(normalize(draft)); + }; + const cancel = () => { + cancelledRef.current = true; + debouncedAutosave.cancel(); + onCancel(); + }; + + const preview = formatLongTextPreview(toText(value)); + + return ( + { + if (opened) return; + // Programmatic close after cancel must not re-commit. + if (cancelledRef.current) { + cancelledRef.current = false; + return; + } + commit(); + }} + position="bottom-start" + width={320} + shadow="md" + withinPortal + closeOnClickOutside + closeOnEscape={false} + trapFocus + > + +
+ {preview ? ( + + {preview} + + ) : ( + + )} +
+
+ e.stopPropagation()} + className={cellClasses.longTextDropdown} + > + {isEditing && ( + <> + + + + - - props.deleteNode()} /> + props.deleteNode()} + > + diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index 8f6269060..a957adb17 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useImperativeHandle, + useMemo, useRef, useState, } from "react"; @@ -15,6 +16,7 @@ import { ScrollArea, Text, UnstyledButton, + VisuallyHidden, } from "@mantine/core"; import clsx from "clsx"; import classes from "./mention.module.css"; @@ -30,6 +32,7 @@ import { MentionSuggestionItem, } from "@/features/editor/components/mention/mention.type.ts"; import { IPage } from "@/features/page/types/page.types"; +import { getPageTitle } from "@/features/page/page.utils"; import { useCreatePageMutation, usePageQuery, @@ -45,6 +48,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; const MentionList = forwardRef((props, ref) => { const [selectedIndex, setSelectedIndex] = useState(1); const viewportRef = useRef(null); + const [countAnnouncement, setCountAnnouncement] = useState(""); + const [selectionAnnouncement, setSelectionAnnouncement] = useState(""); const { pageSlug, spaceSlug } = useParams(); const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) }); const { data: space } = useSpaceQuery(spaceSlug); @@ -99,7 +104,7 @@ const MentionList = forwardRef((props, ref) => { items = items.concat( suggestion.pages.map((page) => ({ id: uuid7(), - label: page.title || t("Untitled"), + label: getPageTitle(page.title, page.isBase, t), entityType: "page", entityId: page.id, slugId: page.slugId, @@ -182,6 +187,45 @@ const MentionList = forwardRef((props, ref) => { setSelectedIndex(1); }, [suggestion]); + const selectableCount = useMemo( + () => renderItems.filter((item) => item.entityType !== "header").length, + [renderItems], + ); + + useEffect(() => { + if (renderItems.length === 0) { + setCountAnnouncement(t("No results")); + return; + } + setCountAnnouncement( + t("{{count}} result available", { count: selectableCount }), + ); + }, [renderItems.length, selectableCount, t]); + + useEffect(() => { + const item = renderItems[selectedIndex]; + if (!item || item.entityType === "header") { + setSelectionAnnouncement(""); + return; + } + if (item.entityType === "user") { + setSelectionAnnouncement(`${t("People")}: ${item.label}`); + return; + } + if (item.entityType === "page") { + if (item.id === null) { + setSelectionAnnouncement(`${t("Create page")}: ${item.label}`); + return; + } + const pageLabel = item.label || t("Untitled"); + setSelectionAnnouncement( + item.spaceName + ? `${t("Pages")}: ${pageLabel}, ${item.spaceName}` + : `${t("Pages")}: ${pageLabel}`, + ); + } + }, [selectedIndex, renderItems, t]); + useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => { if (event.key === "ArrowUp") { @@ -235,7 +279,7 @@ const MentionList = forwardRef((props, ref) => { props.command({ id: uuid7(), - label: createdPage.title || "Untitled", + label: getPageTitle(createdPage.title, createdPage.isBase, t), entityType: "page", entityId: createdPage.id, slugId: createdPage.slugId, @@ -269,6 +313,9 @@ const MentionList = forwardRef((props, ref) => { if (renderItems.length === 0) { return ( + + {countAnnouncement} + {t("No results")} @@ -295,6 +342,12 @@ const MentionList = forwardRef((props, ref) => { aria-label={t("Mention suggestions")} aria-activedescendant={`mention-option-${selectedIndex}`} > + + {countAnnouncement} + + + {selectionAnnouncement} + { - if (!editor) return; + if (!isEditorReady(editor)) return; const { results, resultIndex } = editor.storage.searchAndReplace; //TODO: check type error @@ -90,27 +91,32 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo }; const next = () => { + if (!isEditorReady(editor)) return; editor.commands.nextSearchResult(); goToSelection(); }; const previous = () => { + if (!isEditorReady(editor)) return; editor.commands.previousSearchResult(); goToSelection(); }; const replace = () => { + if (!isEditorReady(editor)) return; editor.commands.setReplaceTerm(replaceText); editor.commands.replace(); goToSelection(); }; const replaceAll = () => { + if (!isEditorReady(editor)) return; editor.commands.setReplaceTerm(replaceText); editor.commands.replaceAll(); }; useEffect(() => { + if (!isEditorReady(editor)) return; editor.commands.setSearchTerm(searchText); editor.commands.resetIndex(); editor.commands.selectCurrentItem(); @@ -118,6 +124,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo const handleOpenEvent = (e) => { setPageFindState({ isOpen: true }); + if (!isEditorReady(editor)) return; const selectedText = editor.state.doc.textBetween( editor.state.selection.from, editor.state.selection.to, @@ -149,6 +156,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo }, [pageFindState.isOpen]); useEffect(() => { + if (!isEditorReady(editor)) return; editor.commands.setCaseSensitive(caseSensitive.isCaseSensitive); editor.commands.resetIndex(); goToSelection(); diff --git a/apps/client/src/features/editor/components/slash-menu/command-list.tsx b/apps/client/src/features/editor/components/slash-menu/command-list.tsx index ebc06660f..d645e404f 100644 --- a/apps/client/src/features/editor/components/slash-menu/command-list.tsx +++ b/apps/client/src/features/editor/components/slash-menu/command-list.tsx @@ -5,15 +5,19 @@ import { } from "@/features/editor/components/slash-menu/types"; import { ActionIcon, + Badge, Group, Paper, ScrollArea, Text, UnstyledButton, + VisuallyHidden, } from "@mantine/core"; import classes from "./slash-menu.module.css"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { Feature } from "@/ee/features"; const CommandList = ({ items, @@ -29,6 +33,15 @@ const CommandList = ({ const { t } = useTranslation(); const [selectedIndex, setSelectedIndex] = useState(0); const viewportRef = useRef(null); + const [countAnnouncement, setCountAnnouncement] = useState(""); + const [selectionAnnouncement, setSelectionAnnouncement] = useState(""); + + const hasBases = useHasFeature(Feature.BASES); + // Title must match the "Base (Inline)" item in menu-items.ts. Without the + // bases entitlement the item stays visible but disabled; an expired license + // the client can't detect falls through to a handled create failure. + const isItemDisabled = (item: SlashMenuItemType) => + !hasBases && item.title === "Base (Inline)"; const flatItems = useMemo(() => { return Object.values(items).flat(); @@ -37,11 +50,11 @@ const CommandList = ({ const selectItem = useCallback( (index: number) => { const item = flatItems[index]; - if (item) { + if (item && !isItemDisabled(item)) { command(item); } }, - [command, flatItems], + [command, flatItems, hasBases], ); useEffect(() => { @@ -79,6 +92,25 @@ const CommandList = ({ setSelectedIndex(0); }, [flatItems]); + useEffect(() => { + if (flatItems.length === 0) { + setCountAnnouncement(""); + return; + } + setCountAnnouncement( + t("{{count}} command available", { count: flatItems.length }), + ); + }, [flatItems.length, t]); + + useEffect(() => { + const item = flatItems[selectedIndex]; + if (!item) { + setSelectionAnnouncement(""); + return; + } + setSelectionAnnouncement(`${t(item.title)}, ${t(item.description)}`); + }, [selectedIndex, flatItems, t]); + useEffect(() => { viewportRef.current ?.querySelector(`[data-item-index="${selectedIndex}"]`) @@ -95,6 +127,12 @@ const CommandList = ({ aria-label={t("Slash commands")} aria-activedescendant={`slash-command-option-${selectedIndex}`} > + + {countAnnouncement} + + + {selectionAnnouncement} + { flatIndex += 1; const itemIndex = flatIndex; + const disabled = isItemDisabled(item); return ( selectItem(itemIndex)} className={clsx(classes.menuBtn, { [classes.selectedItem]: itemIndex === selectedIndex, + [classes.disabledItem]: disabled, })} > - + @@ -138,6 +180,12 @@ const CommandList = ({ {t(item.description)} + + {disabled && ( + + {t("Upgrade")} + + )} ); diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index cddddc35f..352bdb84c 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -7,6 +7,7 @@ import { IconH2, IconH3, IconInfoCircle, + IconLayoutKanban, IconList, IconListNumbers, IconMath, @@ -21,6 +22,7 @@ import { IconMenu4, IconPageBreak, IconCalendar, + IconClock, IconAppWindow, IconSitemap, IconColumns3, @@ -43,6 +45,7 @@ import IconMermaid from "@/components/icons/icon-mermaid"; import IconDrawio from "@/components/icons/icon-drawio"; import { IconColumns4 } from "@/components/icons/icon-columns-4"; import { IconColumns5 } from "@/components/icons/icon-columns-5"; +import i18n from "@/i18n.ts"; import { AirtableIcon, FigmaIcon, @@ -55,6 +58,7 @@ import { VimeoIcon, YoutubeIcon, } from "@/components/icons"; +import { insertBaseEmbedBlock } from "@/features/editor/components/base-embed/insert-base-embed"; const CommandGroups: SlashMenuGroupedItemsType = { basic: [ @@ -357,6 +361,24 @@ const CommandGroups: SlashMenuGroupedItemsType = { .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) .run(), }, + { + title: "Base (Inline)", + description: "Insert an inline base on this page", + searchTerms: ["base", "database", "table", "grid", "spreadsheet"], + icon: IconTable, + command: ({ editor, range }: CommandProps) => { + insertBaseEmbedBlock(editor, { range }); + }, + }, + { + title: "Kanban", + description: "Insert a kanban board on this page", + searchTerms: ["kanban", "board", "cards", "status", "task", "database"], + icon: IconLayoutKanban, + command: ({ editor, range }: CommandProps) => { + insertBaseEmbedBlock(editor, { range, template: "kanban" }); + }, + }, { title: "Toggle block", description: "Insert collapsible block.", @@ -459,7 +481,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { searchTerms: ["date", "today"], icon: IconCalendar, command: ({ editor, range }: CommandProps) => { - const currentDate = new Date().toLocaleDateString("en-US", { + const currentDate = new Date().toLocaleDateString(i18n.language, { year: "numeric", month: "long", day: "numeric", @@ -473,6 +495,25 @@ const CommandGroups: SlashMenuGroupedItemsType = { .run(); }, }, + { + title: "Time", + description: "Insert current time", + searchTerms: ["time", "now", "clock"], + icon: IconClock, + command: ({ editor, range }: CommandProps) => { + const currentTime = new Date().toLocaleTimeString(i18n.language, { + hour: "numeric", + minute: "numeric", + }); + + editor + .chain() + .focus() + .deleteRange(range) + .insertContent(currentTime) + .run(); + }, + }, { title: "Status", description: "Insert inline status badge.", @@ -766,18 +807,34 @@ export const getSuggestionItems = ({ for (const [group, items] of Object.entries(CommandGroups)) { const filteredItems = items.filter((item) => { if (excludeItems?.has(item.title)) return false; + const translatedTitle = i18n.t(item.title); + const translatedDescription = i18n.t(item.description); return ( fuzzyMatch(search, item.title) || + fuzzyMatch(search, translatedTitle) || item.description.toLowerCase().includes(search) || + translatedDescription.toLowerCase().includes(search) || (item.searchTerms && - item.searchTerms.some((term: string) => term.includes(search))) + item.searchTerms.some( + (term: string) => + term.includes(search) || + i18n.t(term).toLowerCase().includes(search), + )) ); }); if (filteredItems.length) { filteredGroups[group] = filteredItems.sort((a, b) => { - const aTitle = a.title.toLowerCase().includes(search) ? 0 : 1; - const bTitle = b.title.toLowerCase().includes(search) ? 0 : 1; + const aTitle = + a.title.toLowerCase().includes(search) || + i18n.t(a.title).toLowerCase().includes(search) + ? 0 + : 1; + const bTitle = + b.title.toLowerCase().includes(search) || + i18n.t(b.title).toLowerCase().includes(search) + ? 0 + : 1; return aTitle - bTitle; }); } diff --git a/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css b/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css index 25f9a110e..24ad3ecc0 100644 --- a/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css +++ b/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css @@ -25,3 +25,8 @@ background: var(--mantine-color-gray-light); } } + +.disabledItem { + opacity: 0.45; + cursor: not-allowed; +} diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index a626e1ee2..776568037 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -1,7 +1,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; -import React, { useCallback } from "react"; +import React, { useCallback, type JSX } from "react"; import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx index 804a274a9..ba2bed40e 100644 --- a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx +++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx @@ -3,7 +3,7 @@ import { TextSelection } from "@tiptap/pm/state"; import React, { FC, useEffect, useRef, useState } from "react"; import classes from "./table-of-contents.module.css"; import clsx from "clsx"; -import { Box, Text } from "@mantine/core"; +import { Box, Text, Title } from "@mantine/core"; import { useTranslation } from "react-i18next"; type TableOfContentsProps = { @@ -25,7 +25,7 @@ const recalculateLinks = (nodePos: NodePos[]) => { (acc, item) => { const label = item.node.textContent; const level = Number(item.node.attrs.level); - if (label.length && level <= 4) { + if (label.length && level <= 6) { acc.push({ label, level, @@ -50,6 +50,7 @@ export const TableOfContents: FC = (props) => { const headerPaddingRef = useRef(null); const handleScrollToHeading = (position: number) => { + if (!props.editor || props.editor.isDestroyed) return; const { view } = props.editor; const headerOffset = parseInt( @@ -73,16 +74,21 @@ export const TableOfContents: FC = (props) => { }; const handleUpdate = () => { - const result = recalculateLinks(props.editor?.$nodes("heading")); + if (!props.editor || props.editor.isDestroyed) return; + + const result = recalculateLinks(props.editor.$nodes("heading")); setLinks(result.links); setHeadingDOMNodes(result.nodes); }; useEffect(() => { + // "create" repopulates once the editor view mounts after this component + props.editor?.on("create", handleUpdate); props.editor?.on("update", handleUpdate); return () => { + props.editor?.off("create", handleUpdate); props.editor?.off("update", handleUpdate); }; }, [props.editor]); @@ -156,9 +162,9 @@ export const TableOfContents: FC = (props) => { return ( <> {props.isShare && ( - + {t("Table of contents")} - </Text> + )}
{links.map((item, idx) => ( diff --git a/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx index db79844e8..4fde678cb 100644 --- a/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx +++ b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx @@ -9,7 +9,7 @@ import { Menu, UnstyledButton } from "@mantine/core"; import { IconChevronDown } from "@tabler/icons-react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; -import { isCellSelection } from "@docmost/editor-ext"; +import { isCellSelection, isEditorReady } from "@docmost/editor-ext"; import { CellChevronMenu } from "./menus/cell-chevron-menu"; import classes from "./handle.module.css"; @@ -27,7 +27,9 @@ export const CellChevron = React.memo(function CellChevron({ tablePos, }: CellChevronProps) { const { t } = useTranslation(); - const cellDom = editor.view.nodeDOM(cellPos) as HTMLElement | null; + const cellDom = isEditorReady(editor) + ? (editor.view.nodeDOM(cellPos) as HTMLElement | null) + : null; const { refs, floatingStyles, middlewareData } = useFloating({ placement: "top-end", @@ -61,6 +63,7 @@ export const CellChevron = React.memo(function CellChevron({ }); const onOpen = useCallback(() => { + if (!isEditorReady(editor)) return; const current = editor.state.selection; // Preserve an existing multi-cell CellSelection that already covers @@ -86,6 +89,7 @@ export const CellChevron = React.memo(function CellChevron({ }, [editor, cellPos]); const onClose = useCallback(() => { + if (!isEditorReady(editor)) return; editor.commands.unfreezeHandles(); }, [editor]); diff --git a/apps/client/src/features/editor/components/table/handle/column-handle.tsx b/apps/client/src/features/editor/components/table/handle/column-handle.tsx index a46ac50d5..ee29a7d99 100644 --- a/apps/client/src/features/editor/components/table/handle/column-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/column-handle.tsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; import { ColumnHandleMenu } from "./menus/column-handle-menu"; +import { isEditorReady } from "@docmost/editor-ext"; import classes from "./handle.module.css"; interface ColumnHandleProps { @@ -35,7 +36,9 @@ export const ColumnHandle = React.memo(function ColumnHandle({ // an external drop reflows the doc before the plugin re-emits // hoveringCell), it can resolve to a Text node, on which `.closest` is // undefined. Filter to HTMLElement so downstream consumers stay safe. - const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupDom = isEditorReady(editor) + ? editor.view.nodeDOM(anchorPos) + : null; const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts index a30595597..7a9bfc8f7 100644 --- a/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts @@ -1,6 +1,7 @@ import { useCallback } from "react"; import type { Editor } from "@tiptap/react"; import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { buildRowOrColumnSelection, Orientation } from "../lib/select-row-column"; interface Args { @@ -19,6 +20,7 @@ export function useColumnRowMenuLifecycle({ tablePos, }: Args) { const onOpen = useCallback(() => { + if (!isEditorReady(editor)) return; const selection = buildRowOrColumnSelection( editor.state, tableNode, @@ -33,6 +35,7 @@ export function useColumnRowMenuLifecycle({ }, [editor, orientation, index, tableNode, tablePos]); const onClose = useCallback(() => { + if (!isEditorReady(editor)) return; editor.commands.unfreezeHandles(); }, [editor]); diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts index 1bd4cb209..308c7d98b 100644 --- a/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts @@ -2,6 +2,7 @@ import { useCallback } from "react"; import type { Editor } from "@tiptap/react"; import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { TableMap } from "@tiptap/pm/tables"; +import { isEditorReady } from "@docmost/editor-ext"; type Scope = | { kind: "col"; index: number } @@ -15,6 +16,7 @@ export function useTableClear( scope: Scope, ) { return useCallback(() => { + if (!isEditorReady(editor)) return; const tr = editor.state.tr; const tableStart = tablePos + 1; const map = TableMap.get(tableNode); diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts index 476c68f8d..be337cc85 100644 --- a/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import type { Editor } from "@tiptap/react"; import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { TableMap } from "@tiptap/pm/tables"; -import { moveColumn, moveRow } from "@docmost/editor-ext"; +import { isEditorReady, moveColumn, moveRow } from "@docmost/editor-ext"; export type MoveDirection = "left" | "right" | "up" | "down"; @@ -25,7 +25,7 @@ export function useTableMoveRowColumn( const canMove = target >= 0 && target <= maxIndex; const handleMove = useCallback(() => { - if (!canMove) return; + if (!canMove || !isEditorReady(editor)) return; const tr = editor.state.tr; const moved = orientation === "col" diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts index afc6a2774..cc395a3ed 100644 --- a/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts @@ -4,6 +4,7 @@ import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { convertArrayOfRowsToTableNode, convertTableNodeToArrayOfRows, + isEditorReady, transpose, } from "@docmost/editor-ext"; import { @@ -63,7 +64,7 @@ export function useTableSort({ }, [tableNode, orientation, index]); const handleSort = useCallback(() => { - if (!canSort) return; + if (!canSort || !isEditorReady(editor)) return; const rows = convertTableNodeToArrayOfRows(tableNode); const axes = orientation === "col" ? rows : transpose(rows); diff --git a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx index 84f904ca7..6bc85608f 100644 --- a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx +++ b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx @@ -101,14 +101,14 @@ export const CellChevronMenu = React.memo(function CellChevronMenu({ } onClick={() => editor.chain().focus().mergeCells().run()} - disabled={!editor.can().mergeCells()} + disabled={!editor?.can().mergeCells()} > {t("Merge cells")} } onClick={() => editor.chain().focus().splitCell().run()} - disabled={!editor.can().splitCell()} + disabled={!editor?.can().splitCell()} > {t("Split cell")} diff --git a/apps/client/src/features/editor/components/table/handle/row-handle.tsx b/apps/client/src/features/editor/components/table/handle/row-handle.tsx index 1f3e3cc51..18ba480cb 100644 --- a/apps/client/src/features/editor/components/table/handle/row-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; import { RowHandleMenu } from "./menus/row-handle-menu"; +import { isEditorReady } from "@docmost/editor-ext"; import classes from "./handle.module.css"; interface RowHandleProps { @@ -33,7 +34,9 @@ export const RowHandle = React.memo(function RowHandle({ // an external drop reflows the doc before the plugin re-emits // hoveringCell), it can resolve to a Text node, on which `.closest` is // undefined. Filter to HTMLElement so downstream consumers stay safe. - const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupDom = isEditorReady(editor) + ? editor.view.nodeDOM(anchorPos) + : null; const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index 34ac61562..ba577183a 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, type JSX } from "react"; import { EditorMenuProps, ShouldShowProps, diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 92cc318e9..679d68d8f 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -1,6 +1,6 @@ import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; -import React, { useCallback } from "react"; +import React, { useCallback, type JSX } from "react"; import { EditorMenuProps, ShouldShowProps, diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx index e50793149..06d05bd21 100644 --- a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx +++ b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx @@ -105,6 +105,7 @@ function TransclusionReferenceBody({ sourcePageId, transclusionId, }); + if (editor.isDestroyed) return; const pos = getPos(); if (typeof pos !== "number") return; const from = pos; diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 429e02f87..bfbaf27ad 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -18,6 +18,7 @@ import { } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { getFileUrl } from "@/lib/config.ts"; +import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import classes from "../common/toolbar-menu.module.css"; export function VideoMenu({ editor }: EditorMenuProps) { @@ -38,6 +39,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { isAlignCenter: ctx.editor.isActive("video", { align: "center" }), isAlignRight: ctx.editor.isActive("video", { align: "right" }), src: videoAttrs?.src || null, + alt: videoAttrs?.alt || "", }; }, }); @@ -112,6 +114,16 @@ export function VideoMenu({ editor }: EditorMenuProps) { editor.commands.deleteSelection(); }, [editor]); + const { + button: altTextButton, + panel: altTextPanel, + isEditing: isEditingAlt, + } = useAltTextControl({ + editor, + nodeName: "video", + currentAlt: editorState?.alt || "", + }); + return ( -
+ {isEditingAlt ? ( + altTextPanel + ) : ( +
+ {altTextButton} + +
+ -
+
+ )} ); } diff --git a/apps/client/src/features/editor/components/video/video-view.tsx b/apps/client/src/features/editor/components/video/video-view.tsx index 9a67533b1..d6c37a0cd 100644 --- a/apps/client/src/features/editor/components/video/video-view.tsx +++ b/apps/client/src/features/editor/components/video/video-view.tsx @@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next"; export default function VideoView(props: NodeViewProps) { const { t } = useTranslation(); const { editor, node, selected } = props; - const { src, width, align, aspectRatio, placeholder } = node.attrs; + const { src, width, align, alt, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; @@ -47,7 +47,7 @@ export default function VideoView(props: NodeViewProps) { preload="metadata" controls src={getFileUrl(src)} - aria-label={placeholder?.name || t("Video")} + aria-label={alt || undefined} /> )} {!src && previewSrc && ( diff --git a/apps/client/src/features/editor/extensions/clean-styles.ts b/apps/client/src/features/editor/extensions/clean-styles.ts new file mode 100644 index 000000000..f36e9f500 --- /dev/null +++ b/apps/client/src/features/editor/extensions/clean-styles.ts @@ -0,0 +1,20 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; + +export const CleanStyles = Extension.create({ + name: "cleanStyles", + priority: 80, + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("cleanStyles"), + props: { + transformPastedHTML(html) { + return html.replace(/\s+style="[^"]*"/gi, ""); + }, + }, + }), + ]; + }, +}); diff --git a/apps/client/src/features/editor/extensions/drag-handle.ts b/apps/client/src/features/editor/extensions/drag-handle.ts index 6b10678a1..8794b585b 100644 --- a/apps/client/src/features/editor/extensions/drag-handle.ts +++ b/apps/client/src/features/editor/extensions/drag-handle.ts @@ -34,6 +34,8 @@ export interface GlobalDragHandleOptions { * Custom nodes to be included for drag handle */ customNodes: string[]; + + atomNodes: string[]; } function absoluteRect(node: Element) { const data = node.getBoundingClientRect(); @@ -76,6 +78,10 @@ function nodeDOMAtCoords( `[data-type=${node}] p`, `.node-${node} p`, ]); + const atomSelectors = options.atomNodes.flatMap((node) => [ + `[data-type=${node}]`, + `.node-${node}`, + ]); const selectors = [ "li", @@ -95,8 +101,9 @@ function nodeDOMAtCoords( ".tableWrapper", ...customParagraphSelectors, ...customSelectors, + ...atomSelectors, ].join(", "); - return document + const found = document .elementsFromPoint(coords.x, coords.y) .find((elem: Element) => { // Skip elements that belong to a nested editor (e.g. transclusion @@ -108,6 +115,11 @@ function nodeDOMAtCoords( elem.matches(selectors) ); }); + if (found && atomSelectors.length > 0) { + const atomWrapper = found.closest(atomSelectors.join(", ")); + if (atomWrapper) return atomWrapper; + } + return found; } function nodePosAtDOM( node: Element, @@ -127,7 +139,7 @@ function isCustomNodeDOM( options: GlobalDragHandleOptions, ): boolean { if (!elem) return false; - for (const name of options.customNodes) { + for (const name of [...options.customNodes, ...options.atomNodes]) { if ( elem.getAttribute("data-type") === name || elem.classList.contains(`node-${name}`) @@ -210,7 +222,10 @@ export function DragHandlePlugin( // The drag landed on a custom-node container (transclusion etc.). // Walk up to the matching node so the drag moves the whole // container, not whatever inner element the click landed on. - const customTypes = new Set(options.customNodes); + const customTypes = new Set([ + ...options.customNodes, + ...options.atomNodes, + ]); for (let d = $sel.depth; d > 0; d--) { if (customTypes.has($sel.node(d).type.name)) { selection = NodeSelection.create( @@ -264,7 +279,23 @@ export function DragHandlePlugin( event.dataTransfer.setData("text/plain", text); event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setDragImage(node, 0, 0); + const previewTemplate = + node.querySelector("[data-drag-preview]"); + if (previewTemplate) { + const preview = previewTemplate.cloneNode(true) as HTMLElement; + preview.removeAttribute("hidden"); + preview.style.position = "fixed"; + preview.style.top = "0"; + preview.style.left = "-10000px"; + preview.style.pointerEvents = "none"; + document.body.appendChild(preview); + event.dataTransfer.setDragImage(preview, 0, 0); + document.addEventListener("dragend", () => preview.remove(), { + once: true, + }); + } else { + event.dataTransfer.setDragImage(node, 0, 0); + } view.dragging = { slice, move: event.ctrlKey }; } @@ -497,6 +528,7 @@ const GlobalDragHandle = Extension.create({ scrollThreshold: 100, excludedTags: [], customNodes: [], + atomNodes: [], }; }, @@ -509,6 +541,7 @@ const GlobalDragHandle = Extension.create({ dragHandleSelector: this.options.dragHandleSelector, excludedTags: this.options.excludedTags, customNodes: this.options.customNodes, + atomNodes: this.options.atomNodes, }), ]; }, diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 9857b0551..040347d0b 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -3,7 +3,7 @@ import { StarterKit } from "@tiptap/starter-kit"; import { Code } from "@tiptap/extension-code"; import { TextAlign } from "@tiptap/extension-text-align"; import { TaskList, TaskItem } from "@tiptap/extension-list"; -import { Placeholder, CharacterCount } from "@tiptap/extensions"; +import { Placeholder, CharacterCount, UndoRedo } from "@tiptap/extensions"; import { Superscript } from "@tiptap/extension-superscript"; import SubScript from "@tiptap/extension-subscript"; import { Typography } from "@tiptap/extension-typography"; @@ -61,6 +61,7 @@ import { TransclusionSource, TransclusionReference, TableView, + BaseEmbed as BaseEmbedNode, } from "@docmost/editor-ext"; import { randomElement, @@ -91,6 +92,7 @@ import PdfView from "@/features/editor/components/pdf/pdf-view.tsx"; import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx"; import TransclusionView from "@/features/editor/components/transclusion/transclusion-view.tsx"; import TransclusionReferenceView from "@/features/editor/components/transclusion/transclusion-reference-view.tsx"; +import { BaseEmbedView } from "@/features/editor/components/base-embed/base-embed-view.tsx"; import { common, createLowlight } from "lowlight"; import plaintext from "highlight.js/lib/languages/plaintext"; import powershell from "highlight.js/lib/languages/powershell"; @@ -112,6 +114,7 @@ import EmojiCommand from "./emoji-command"; import { countWords } from "alfaaz"; import AutoJoiner from "@/features/editor/extensions/autojoiner.ts"; import GlobalDragHandle from "@/features/editor/extensions/drag-handle.ts"; +import { CleanStyles } from "@/features/editor/extensions/clean-styles.ts"; const lowlight = createLowlight(common); lowlight.register("mermaid", plaintext); @@ -230,6 +233,7 @@ export const mainExtensions = [ TrailingNode, GlobalDragHandle.configure({ customNodes: ["transclusionSource", "transclusionReference"], + atomNodes: ["base"], }), TextStyle, Color, @@ -380,9 +384,15 @@ export const mainExtensions = [ TransclusionReference.configure({ view: TransclusionReferenceView, }), + BaseEmbedNode.extend({ + addNodeView() { + return ReactNodeViewRenderer(BaseEmbedView); + }, + }), MarkdownClipboard.configure({ transformPastedText: true, }), + CleanStyles, CharacterCount.configure({ wordCounter: (text) => countWords(text), }), @@ -416,7 +426,11 @@ const TEMPLATE_EXCLUDED_SLASH_ITEMS = new Set([ "Video", "File attachment", "Draw.io (diagrams.net)", - "Excalidraw diagram", + "Excalidraw (Whiteboard)", + "Audio", + "Synced block", + "Base (Inline)", + "Kanban" ]); const TemplateSlashCommand = Command.configure({ @@ -433,6 +447,7 @@ const TemplateSlashCommand = Command.configure({ export const templateExtensions = [ ...mainExtensions.filter((ext: any) => ext !== SlashCommand), TemplateSlashCommand, + UndoRedo, ] as any; export const collabExtensions: CollabExtensions = (provider, user) => [ diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index 412a3b3de..f41fe0f7f 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -22,10 +22,11 @@ import { useTranslation } from "react-i18next"; import { IContributor } from "@/features/page/types/page.types.ts"; import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-toolbar"; import { PageEditMode } from "@/features/user/types/user.types.ts"; -import useToggleAside from "@/hooks/use-toggle-aside.tsx"; +import { useAsideTriggerProps } from "@/hooks/use-toggle-aside.tsx"; import { DeletedPageBanner } from "@/features/page/trash/components/deleted-page-banner.tsx"; import clsx from "clsx"; import { currentPageEditModeAtom } from "@/features/editor/atoms/editor-atoms.ts"; +import { EmptyPageGetStarted } from "@/features/editor/components/empty-page/empty-page-get-started"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); @@ -90,6 +91,7 @@ export function FullEditor({ fluid={fullPageWidth} size={!fullPageWidth && 900} className={classes.editor} + style={{ display: "flex", flexDirection: "column" }} > {editorToolbarEnabled && editable && isEditMode && ( @@ -113,6 +115,7 @@ export function FullEditor({ content={content} canComment={canComment} /> + ); } @@ -125,7 +128,7 @@ type PageBylineProps = { function PageByline({ creator, contributors, readOnly }: PageBylineProps) { const { t } = useTranslation(); - const toggleAside = useToggleAside(); + const detailsTriggerProps = useAsideTriggerProps("details"); const otherContributors = (contributors ?? []).filter( (c) => c.id !== creator?.id, @@ -141,7 +144,9 @@ function PageByline({ creator, contributors, readOnly }: PageBylineProps) { {creator && ( - + toggleAside("details")} + {...detailsTriggerProps} > diff --git a/apps/client/src/features/editor/hooks/use-editor-scroll.ts b/apps/client/src/features/editor/hooks/use-editor-scroll.ts index cfd5a6921..0f1759cf6 100644 --- a/apps/client/src/features/editor/hooks/use-editor-scroll.ts +++ b/apps/client/src/features/editor/hooks/use-editor-scroll.ts @@ -42,6 +42,10 @@ export const useEditorScroll = ({ return; } + if (editor.isDestroyed) { + resolve(false); + return; + } const dom = editor.view.dom.querySelector(`[id="${targetId}"], [data-id="${targetId}"]`); if (dom) { dom.scrollIntoView({ behavior: 'smooth', block: 'start' }); diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 8521356ec..2b3daa39c 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -14,6 +14,7 @@ import { WebSocketStatus, HocuspocusProviderWebsocket, onSyncedParameters, + onStatelessParameters, } from "@hocuspocus/provider"; import { Editor, @@ -33,6 +34,7 @@ import { currentPageEditModeAtom, pageEditorAtom, yjsConnectionStatusAtom, + yjsSyncedAtom, } from "@/features/editor/atoms/editor-atoms"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom"; import { @@ -43,7 +45,6 @@ import { import CommentDialog from "@/features/comment/components/comment-dialog"; import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu"; import { ReadonlyBubbleMenu } from "@/features/editor/components/bubble-menu/readonly-bubble-menu"; -import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx"; import TableMenu from "@/features/editor/components/table/table-menu.tsx"; import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer"; import ImageMenu from "@/features/editor/components/image/image-menu.tsx"; @@ -74,6 +75,7 @@ import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu"; import { EditorLinkMenu } from "@/features/editor/components/link/link-menu"; import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx"; import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context"; +import { useTranslation } from "react-i18next"; interface PageEditorProps { pageId: string; @@ -88,6 +90,7 @@ export default function PageEditor({ content, canComment, }: PageEditorProps) { + const { t } = useTranslation(); const collaborationURL = useCollaborationUrl(); const isComponentMounted = useRef(false); const editorRef = useRef(null); @@ -107,6 +110,7 @@ export default function PageEditor({ const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( yjsConnectionStatusAtom, ); + const [, setYjsSynced] = useAtom(yjsSyncedAtom); const menuContainerRef = useRef(null); const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken(); const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false }); @@ -144,6 +148,24 @@ export default function PageEditor({ const onSyncedHandler = (event: onSyncedParameters) => { setIsRemoteSynced(event.state); }; + const onStatelessHandler = ({ payload }: onStatelessParameters) => { + try { + const message = JSON.parse(payload); + if (message?.type !== "page.updated" || !message.updatedAt) return; + const pageData = queryClient.getQueryData(["pages", slugId]); + if (pageData) { + queryClient.setQueryData(["pages", slugId], { + ...pageData, + updatedAt: message.updatedAt, + ...(message.lastUpdatedBy && { + lastUpdatedBy: message.lastUpdatedBy, + }), + }); + } + } catch { + // ignore unrelated stateless messages + } + }; const onAuthenticationFailedHandler = () => { const payload = jwtDecode(collabQuery?.token); const now = Date.now().valueOf() / 1000; @@ -168,6 +190,7 @@ export default function PageEditor({ onAuthenticationFailed: onAuthenticationFailedHandler, onStatus: onStatusHandler, onSynced: onSyncedHandler, + onStateless: onStatelessHandler, }); local.on("synced", onLocalSyncedHandler); @@ -232,20 +255,15 @@ export default function PageEditor({ editorProps: { scrollThreshold: 80, scrollMargin: 80, + attributes: { + "aria-label": t("Page content"), + }, handleDOMEvents: { keydown: (_view, event) => { if (platformModifierKey(event) && event.code === "KeyS") { event.preventDefault(); return true; } - if (event.key === "Tab") { - const editor = editorRef.current; - if (!editor) return false; - event.preventDefault(); - return editor.view.someProp("handleKeyDown", (f) => - f(editor.view, event) - ); - } if (platformModifierKey(event) && event.code === "KeyK") { searchSpotlight.open(); return true; @@ -322,7 +340,6 @@ export default function PageEditor({ queryClient.setQueryData(["pages", slugId], { ...pageData, content: newContent, - updatedAt: new Date(), }); } }, 3000); @@ -363,6 +380,14 @@ export default function PageEditor({ const isSynced = isLocalSynced && isRemoteSynced; + useEffect(() => { + setYjsSynced(isSynced); + }, [isSynced, setYjsSynced]); + + useEffect(() => { + return () => setYjsSynced(false); + }, [setYjsSynced]); + useEffect(() => { const timeout = setTimeout(() => { if (yjsConnectionStatus === WebSocketStatus.Connecting || !isSynced) { @@ -399,6 +424,11 @@ export default function PageEditor({ immediatelyRender={true} extensions={mainExtensions} content={content} + editorProps={{ + attributes: { + "aria-label": t("Page content"), + }, + }} /> ) : (
@@ -429,9 +459,7 @@ export default function PageEditor({ {editor && !editorIsEditable && (editable || canComment) && - providersRef.current && ( - - )} + providersRef.current && } {showCommentPopup && ( )} @@ -440,7 +468,9 @@ export default function PageEditor({ )}
editor.commands.focus("end")} + onClick={() => { + if (editor && !editor.isDestroyed) editor.commands.focus("end"); + }} style={{ paddingBottom: "20vh" }} >
diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx index cd4878a9b..4b28bec9d 100644 --- a/apps/client/src/features/editor/readonly-page-editor.tsx +++ b/apps/client/src/features/editor/readonly-page-editor.tsx @@ -15,6 +15,7 @@ interface PageEditorProps { title: string; content: any; pageId?: string; + printMode?: boolean; /** * When rendering inside a public share, pass the share's id (or key). Lookups * for transclusion content then resolve against the share graph instead of @@ -28,6 +29,7 @@ export default function ReadonlyPageEditor({ title, content, pageId, + printMode = false, shareId, }: PageEditorProps) { const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom); @@ -48,8 +50,12 @@ export default function ReadonlyPageEditor({ }, []); const extensions = useMemo(() => { + const excludedExtensions = new Set([ + "uniqueID", + ...(printMode ? ["tableHeaderPin", "tableReadonlySort"] : []), + ]); const filteredExtensions = mainExtensions.filter( - (ext) => ext.name !== "uniqueID", + (ext) => !excludedExtensions.has(ext.name), ); return [ @@ -59,7 +65,7 @@ export default function ReadonlyPageEditor({ updateDocument: false, }), ]; - }, []); + }, [printMode]); const titleExtensions = [ Document.extend({ diff --git a/apps/client/src/features/editor/styles/base-embed.css b/apps/client/src/features/editor/styles/base-embed.css new file mode 100644 index 000000000..6c5e90472 --- /dev/null +++ b/apps/client/src/features/editor/styles/base-embed.css @@ -0,0 +1,17 @@ +.ProseMirror { + .node-base { + /* Suppress the default ProseMirror atom-node selection outline — + * the embed reads as a document block, not a focused widget. */ + &.ProseMirror-selectednode { + outline: none; + } + + /* Page-reference pills are self-contained chips. The editor's `a` rule + * would otherwise add a link underline + bold weight on top of the chip, + * making it look different from a standalone base. Keep it a plain chip. */ + a.pagePill { + border-bottom: none; + font-weight: 400; + } + } +} diff --git a/apps/client/src/features/editor/styles/code.css b/apps/client/src/features/editor/styles/code.css index e84a71ff4..fba5db91d 100644 --- a/apps/client/src/features/editor/styles/code.css +++ b/apps/client/src/features/editor/styles/code.css @@ -103,13 +103,13 @@ margin: 0; @mixin where-light { - background-color: var(--code-bg, var(--mantine-color-gray-1)); - color: var(--mantine-color-pink-7); + background-color: var(--mantine-color-gray-1); + color: var(--mantine-color-text); } @mixin where-dark { - background-color: var(--mantine-color-dark-8); - color: var(--mantine-color-pink-7); + background-color: var(--mantine-color-dark-5) !important; + color: var(--mantine-color-text); } } } diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 077570fb5..ef61425a9 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -1,3 +1,11 @@ +:root { + /* Height of the fixed PageHeader at the top of every page. Used by + * standalone base layout (paddingTop) and by sticky bands inside + * inline base embeds (top offset). One source of truth; if the + * page header ever changes height, edit only this. */ + --page-header-height: 45px; +} + .ProseMirror { background-color: light-dark( var(--mantine-color-white), @@ -286,3 +294,24 @@ margin-top: 0; margin-bottom: 0.75em; } + +/* The full-page base view positions its title with its own outer + * wrapper padding (so it can align with the table below). The global + * 3rem .ProseMirror padding-x would push the title further in than + * the table — drop it inside the base title wrapper only. */ +.base-page-title .ProseMirror { + padding-left: 0; + padding-right: 0; +} + +/* Definite height so the inner .tableScrollport scrolls and the sticky table + * header pins. Flow on print, else it emits a trailing blank page. */ +.base-page-root { + height: calc(100dvh - var(--app-shell-header-height, 45px)); +} + +@media print { + .base-page-root { + height: 100%; + } +} diff --git a/apps/client/src/features/editor/styles/editor.module.css b/apps/client/src/features/editor/styles/editor.module.css index ed5f86432..93f377dc7 100644 --- a/apps/client/src/features/editor/styles/editor.module.css +++ b/apps/client/src/features/editor/styles/editor.module.css @@ -1,11 +1,13 @@ .editor { height: 100%; + min-height: calc(100dvh - var(--app-shell-header-height, 45px) - 96px); padding: 8px 0; margin: 48px auto; @media print { padding: 0; margin: 0; + min-height: 0; } } diff --git a/apps/client/src/features/editor/styles/index.css b/apps/client/src/features/editor/styles/index.css index 52d9268e1..7b1ce93e8 100644 --- a/apps/client/src/features/editor/styles/index.css +++ b/apps/client/src/features/editor/styles/index.css @@ -17,3 +17,4 @@ @import "./indent.css"; @import "./columns.css"; @import "./status.css"; +@import "./base-embed.css"; diff --git a/apps/client/src/features/editor/styles/placeholder.css b/apps/client/src/features/editor/styles/placeholder.css index be5225bf6..9d42f22b0 100644 --- a/apps/client/src/features/editor/styles/placeholder.css +++ b/apps/client/src/features/editor/styles/placeholder.css @@ -1,7 +1,7 @@ .ProseMirror .is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; - color: #adb5bd; + color: var(--mantine-color-placeholder); pointer-events: none; height: 0; @@ -13,7 +13,7 @@ .ProseMirror .is-empty::before { content: attr(data-placeholder); float: left; - color: #adb5bd; + color: var(--mantine-color-placeholder); pointer-events: none; height: 0; diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index 5d802e4ab..32a427936 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -163,8 +163,13 @@ @media print { .tableWrapper.tableHeaderPinned table tr:first-child { - position: static; - transform: none; + position: static !important; + top: auto !important; + transform: none !important; + } + + .tableReadonlySortChevron { + display: none !important; } } @@ -204,10 +209,6 @@ opacity: 1; } -.ProseMirror table th:has(.tableReadonlySortChevron) { - padding-right: 30px; -} - .tableReadonlySortChevron:hover { background: light-dark( rgba(55, 53, 47, 0.16), @@ -272,4 +273,4 @@ .prosemirror-dropcursor-inline { display: none; } -} \ No newline at end of file +} diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index 3ff2d7614..04317099d 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -35,6 +35,7 @@ export interface TitleEditorProps { title: string; spaceSlug: string; editable: boolean; + isBase?: boolean; } export function TitleEditor({ @@ -43,6 +44,7 @@ export function TitleEditor({ title, spaceSlug, editable, + isBase, }: TitleEditorProps) { const { t } = useTranslation(); const { mutateAsync: updateTitlePageMutationAsync } = @@ -64,7 +66,7 @@ export function TitleEditor({ }), Text, Placeholder.configure({ - placeholder: t("Untitled"), + placeholder: isBase ? t("Untitled base") : t("Untitled"), showOnlyWhenEditable: false, }), History.configure({ @@ -87,6 +89,9 @@ export function TitleEditor({ immediatelyRender: true, shouldRerenderOnTransaction: false, editorProps: { + attributes: { + "aria-label": t("Page title"), + }, handleDOMEvents: { keydown: (_view, event) => { if (platformModifierKey(event) && event.code === "KeyS") { @@ -103,11 +108,17 @@ export function TitleEditor({ }); useEffect(() => { - const anchorId = window.location.hash - ? window.location.hash.substring(1) - : undefined; - const pageSlug = buildPageUrl(spaceSlug, slugId, title, anchorId); - navigate(pageSlug, { replace: true }); + // Canonicalize only the path slug; keep query params (?row=, ?view= + // deep links) and the hash anchor intact. + const pageSlug = buildPageUrl(spaceSlug, slugId, title); + navigate( + { + pathname: pageSlug, + search: window.location.search, + hash: window.location.hash, + }, + { replace: true }, + ); }, [title]); const saveTitle = useCallback(() => { @@ -149,7 +160,11 @@ export function TitleEditor({ const debounceUpdate = useDebouncedCallback(saveTitle, 500); useEffect(() => { - if (titleEditor && title !== titleEditor.getText()) { + if ( + titleEditor && + !titleEditor.isDestroyed && + title !== titleEditor.getText() + ) { titleEditor.commands.setContent(title); } }, [pageId, title, titleEditor]); diff --git a/apps/client/src/features/favorite/components/star-button.tsx b/apps/client/src/features/favorite/components/star-button.tsx index 7ff8ff77d..2b341c736 100644 --- a/apps/client/src/features/favorite/components/star-button.tsx +++ b/apps/client/src/features/favorite/components/star-button.tsx @@ -1,4 +1,5 @@ import { ActionIcon, Tooltip } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; import { IconStar, IconStarFilled } from "@tabler/icons-react"; import { useFavoriteIds, @@ -14,6 +15,8 @@ type StarButtonProps = { pageId?: string; spaceId?: string; templateId?: string; + /** Name of the item being favorited, used to make the button's accessible name descriptive. */ + name?: string; size?: number; }; @@ -25,7 +28,7 @@ function getEntityId(props: StarButtonProps): string | undefined { } export default function StarButton(props: StarButtonProps) { - const { type, size = 18 } = props; + const { type, name, size = 18 } = props; const { t } = useTranslation(); const favoriteIds = useFavoriteIds(type); const addMutation = useAddFavoriteMutation(); @@ -47,22 +50,46 @@ export default function StarButton(props: StarButtonProps) { }; if (isFavorited) { - removeMutation.mutate(params); + removeMutation.mutate(params, { + onSuccess: () => { + notifications.show({ + message: name + ? t("Removed {{name}} from favorites", { name }) + : t("Removed from favorites"), + }); + }, + }); } else { - addMutation.mutate(params); + addMutation.mutate(params, { + onSuccess: () => { + notifications.show({ + message: name + ? t("Added {{name}} to favorites", { name }) + : t("Added to favorites"), + }); + }, + }); } }; - const label = isFavorited + // Tooltip label stays short. Accessible name expands to include the item + // so screen reader users can distinguish stars on different rows. + const tooltipLabel = isFavorited ? t("Remove from favorites") : t("Add to favorites"); + const ariaLabel = name + ? isFavorited + ? t("Remove {{name}} from favorites", { name }) + : t("Add {{name}} to favorites", { name }) + : tooltipLabel; + return ( - + - + diff --git a/apps/client/src/features/group/components/create-group-modal.tsx b/apps/client/src/features/group/components/create-group-modal.tsx index 1cff53fbd..307a38f3c 100644 --- a/apps/client/src/features/group/components/create-group-modal.tsx +++ b/apps/client/src/features/group/components/create-group-modal.tsx @@ -11,7 +11,12 @@ export default function CreateGroupModal() { <> - + diff --git a/apps/client/src/features/group/components/edit-group-form.tsx b/apps/client/src/features/group/components/edit-group-form.tsx index f8b1671cd..68b6ba60c 100644 --- a/apps/client/src/features/group/components/edit-group-form.tsx +++ b/apps/client/src/features/group/components/edit-group-form.tsx @@ -9,6 +9,7 @@ import { z } from "zod/v4"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { zod4Resolver } from "mantine-form-zod-resolver"; +import { IGroup } from "@/features/group/types/group.types.ts"; const formSchema = z.object({ name: z.string().min(2).max(100), @@ -18,13 +19,16 @@ const formSchema = z.object({ type FormValues = z.infer; interface EditGroupFormProps { onClose?: () => void; + group?: IGroup; } -export function EditGroupForm({ onClose }: EditGroupFormProps) { +export function EditGroupForm({ onClose, group: groupProp }: EditGroupFormProps) { const { t } = useTranslation(); const updateGroupMutation = useUpdateGroupMutation(); const { isSuccess } = updateGroupMutation; - const { groupId } = useParams(); - const { data: group } = useGroupQuery(groupId); + const { groupId: routeGroupId } = useParams(); + const groupId = groupProp?.id ?? routeGroupId; + const { data: queriedGroup } = useGroupQuery(groupProp ? undefined : groupId); + const group = groupProp ?? queriedGroup; useEffect(() => { if (isSuccess) { @@ -66,6 +70,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) { label={t("Group name")} placeholder={t("e.g Developers")} variant="filled" + data-autofocus {...form.getInputProps("name")} /> diff --git a/apps/client/src/features/group/components/edit-group-modal.tsx b/apps/client/src/features/group/components/edit-group-modal.tsx index 4da933f25..a2fc28f2f 100644 --- a/apps/client/src/features/group/components/edit-group-modal.tsx +++ b/apps/client/src/features/group/components/edit-group-modal.tsx @@ -1,23 +1,31 @@ import { Divider, Modal } from "@mantine/core"; import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx"; import { useTranslation } from "react-i18next"; +import { IGroup } from "@/features/group/types/group.types.ts"; interface EditGroupModalProps { opened: boolean; onClose: () => void; + group?: IGroup; } export default function EditGroupModal({ opened, onClose, + group, }: EditGroupModalProps) { const { t } = useTranslation(); return ( <> - + - + ); diff --git a/apps/client/src/features/group/components/group-action-menu.tsx b/apps/client/src/features/group/components/group-action-menu.tsx index 8c3dccb05..cb50f4bfa 100644 --- a/apps/client/src/features/group/components/group-action-menu.tsx +++ b/apps/client/src/features/group/components/group-action-menu.tsx @@ -10,18 +10,28 @@ import { useDisclosure } from "@mantine/hooks"; import EditGroupModal from "@/features/group/components/edit-group-modal.tsx"; import { modals } from "@mantine/modals"; import { useTranslation } from "react-i18next"; +import { IGroup } from "@/features/group/types/group.types.ts"; -export default function GroupActionMenu() { +interface GroupActionMenuProps { + group?: IGroup; +} + +export default function GroupActionMenu(props: GroupActionMenuProps = {}) { const { t } = useTranslation(); - const { groupId } = useParams(); - const { data: group, isLoading } = useGroupQuery(groupId); + const { groupId: routeGroupId } = useParams(); + const groupId = props.group?.id ?? routeGroupId; + const { data: queriedGroup } = useGroupQuery(props.group ? undefined : groupId); + const group = props.group ?? queriedGroup; const deleteGroupMutation = useDeleteGroupMutation(); const navigate = useNavigate(); const [opened, { open, close }] = useDisclosure(false); const onDelete = async () => { await deleteGroupMutation.mutateAsync(groupId); - navigate("/settings/groups"); + // Only navigate away if we're currently viewing this group's detail page. + if (routeGroupId === groupId) { + navigate("/settings/groups"); + } }; const openDeleteModal = () => @@ -53,7 +63,11 @@ export default function GroupActionMenu() { arrowPosition="center" > - + @@ -76,7 +90,7 @@ export default function GroupActionMenu() { )} - + ); } diff --git a/apps/client/src/features/group/components/group-list.tsx b/apps/client/src/features/group/components/group-list.tsx index d88e1ec02..5f5fed472 100644 --- a/apps/client/src/features/group/components/group-list.tsx +++ b/apps/client/src/features/group/components/group-list.tsx @@ -1,4 +1,4 @@ -import { Table, Group, Text, Anchor } from "@mantine/core"; +import { Table, Group, Text, Anchor, VisuallyHidden } from "@mantine/core"; import { useGetGroupsQuery } from "@/features/group/queries/group-query"; import { Link } from "react-router-dom"; import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; @@ -12,6 +12,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; import { SearchInput } from "@/components/common/search-input.tsx"; import NoTableResults from "@/components/common/no-table-results.tsx"; import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; +import GroupActionMenu from "@/features/group/components/group-action-menu.tsx"; export default function GroupList() { const { t } = useTranslation(); @@ -34,13 +36,16 @@ export default function GroupList() { {t("Group")} {t("Members")} + + {t("Actions")} + {data?.items.length > 0 ? ( data?.items.map((group: IGroup, index: number) => ( - + prefetchGroupMembers(group.id)}> @@ -80,10 +86,13 @@ export default function GroupList() { {formatMemberCount(group.memberCount, t)} + + + )) ) : ( - + )} diff --git a/apps/client/src/features/group/components/group-members.tsx b/apps/client/src/features/group/components/group-members.tsx index 56807bf8d..3bf04b5ac 100644 --- a/apps/client/src/features/group/components/group-members.tsx +++ b/apps/client/src/features/group/components/group-members.tsx @@ -88,7 +88,13 @@ export default function GroupMembersList() { arrowPosition="center" > - + diff --git a/apps/client/src/features/home/components/created-by-me.tsx b/apps/client/src/features/home/components/created-by-me.tsx index 99051357e..3b09efb49 100644 --- a/apps/client/src/features/home/components/created-by-me.tsx +++ b/apps/client/src/features/home/components/created-by-me.tsx @@ -4,19 +4,20 @@ import { UnstyledButton, Badge, Table, - ThemeIcon, Button, } from "@mantine/core"; import { Link } from "react-router-dom"; import PageListSkeleton from "@/components/ui/page-list-skeleton"; -import { buildPageUrl } from "@/features/page/page.utils"; +import { buildPageUrl, getPageTitle } from "@/features/page/page.utils"; import { formattedDate } from "@/lib/time"; import { useCreatedByQuery } from "@/features/page/queries/page-query"; -import { IconFileDescription, IconFiles } from "@tabler/icons-react"; +import { PageListIcon } from "@/components/common/page-list-icon"; +import { IconFiles } from "@tabler/icons-react"; import { EmptyState } from "@/components/ui/empty-state"; import { getSpaceUrl } from "@/lib/config"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; type Props = { spaceId?: string; @@ -49,9 +50,10 @@ export default function CreatedByMe({ spaceId }: Props) { {pages.map((page) => ( - + - {page.icon || ( - - - - )} + - {page.title || t("Untitled")} + {getPageTitle(page.title, page.isBase, t)} diff --git a/apps/client/src/features/home/components/favorites-pages.tsx b/apps/client/src/features/home/components/favorites-pages.tsx index aed8e653a..875666f48 100644 --- a/apps/client/src/features/home/components/favorites-pages.tsx +++ b/apps/client/src/features/home/components/favorites-pages.tsx @@ -4,19 +4,20 @@ import { UnstyledButton, Badge, Table, - ThemeIcon, Button, } from "@mantine/core"; import { Link } from "react-router-dom"; import PageListSkeleton from "@/components/ui/page-list-skeleton"; -import { buildPageUrl } from "@/features/page/page.utils"; +import { buildPageUrl, getPageTitle } from "@/features/page/page.utils"; import { formattedDate } from "@/lib/time"; import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query"; -import { IconFileDescription, IconStar } from "@tabler/icons-react"; +import { PageListIcon } from "@/components/common/page-list-icon"; +import { IconStar } from "@tabler/icons-react"; import { EmptyState } from "@/components/ui/empty-state"; import { getSpaceUrl } from "@/lib/config"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; interface Props { spaceId?: string; @@ -50,9 +51,10 @@ export default function FavoritesPages({ spaceId }: Props) { {favorites.map((fav) => fav.page ? ( - + - {fav.page.icon || ( - - - - )} + - {fav.page.title || t("Untitled")} + {getPageTitle(fav.page.title, fav.page.isBase, t)} diff --git a/apps/client/src/features/label/utils/format-label-date.ts b/apps/client/src/features/label/utils/format-label-date.ts index 1221c8ad8..af26fac91 100644 --- a/apps/client/src/features/label/utils/format-label-date.ts +++ b/apps/client/src/features/label/utils/format-label-date.ts @@ -1,15 +1,27 @@ -import { format, isThisYear, isToday, isYesterday } from "date-fns"; +import { isThisYear, isToday, isYesterday } from "date-fns"; import i18n from "@/i18n.ts"; +import { formatLocalized, getDateFnsLocale } from "@/lib/date-locale.ts"; export function formatLabelListDate(date: Date): string { + const locale = getDateFnsLocale(); if (isToday(date)) { - return i18n.t("Today, {{time}}", { time: format(date, "h:mma") }); + return i18n.t("Today, {{time}}", { + time: formatLocalized(date, "h:mma", "p", locale), + }); } if (isYesterday(date)) { - return i18n.t("Yesterday, {{time}}", { time: format(date, "h:mma") }); + return i18n.t("Yesterday, {{time}}", { + time: formatLocalized(date, "h:mma", "p", locale), + }); } if (isThisYear(date)) { - return format(date, "MMM dd"); + if (locale.code?.startsWith("en")) { + return formatLocalized(date, "MMM dd", "MMM dd", locale); + } + return new Intl.DateTimeFormat(i18n.language, { + month: "short", + day: "numeric", + }).format(date); } - return format(date, "MMM dd, yyyy"); + return formatLocalized(date, "MMM dd, yyyy", "PP", locale); } diff --git a/apps/client/src/features/notification/components/notification-item.tsx b/apps/client/src/features/notification/components/notification-item.tsx index 75e73d235..418647375 100644 --- a/apps/client/src/features/notification/components/notification-item.tsx +++ b/apps/client/src/features/notification/components/notification-item.tsx @@ -18,7 +18,7 @@ import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { useState } from "react"; import { useMarkReadMutation } from "../queries/notification-query"; -import { buildPageUrl } from "@/features/page/page.utils"; +import { buildPageUrl, getPageTitle } from "@/features/page/page.utils"; import { formatRelativeTime } from "../notification.utils"; import classes from "../notification.module.css"; @@ -143,7 +143,7 @@ export function NotificationItem({ /> )} - {notification.page.title || t("Untitled")} + {getPageTitle(notification.page.title, undefined, t)} )} diff --git a/apps/client/src/features/notification/components/notification-popover.tsx b/apps/client/src/features/notification/components/notification-popover.tsx index 751b9edf1..5a5068de8 100644 --- a/apps/client/src/features/notification/components/notification-popover.tsx +++ b/apps/client/src/features/notification/components/notification-popover.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useId, useState } from "react"; import { ActionIcon, Group, @@ -7,7 +7,7 @@ import { Popover, ScrollArea, Tabs, - Text, + Title, Tooltip, } from "@mantine/core"; import { @@ -31,14 +31,18 @@ import classes from "../notification.module.css"; export function NotificationPopover() { const { t } = useTranslation(); + const titleId = useId(); const [opened, setOpened] = useState(false); const [tab, setTab] = useState("direct"); const [filter, setFilter] = useState("all"); + const [filterMenuOpened, setFilterMenuOpened] = useState(false); + const [moreMenuOpened, setMoreMenuOpened] = useState(false); const { data: unreadData } = useUnreadCountQuery(); const markAllRead = useMarkAllReadMutation(); const unreadCount = unreadData?.count ?? 0; + const isSubMenuOpen = filterMenuOpened || moreMenuOpened; const handleMarkAllRead = () => { markAllRead.mutate(); @@ -51,6 +55,9 @@ export function NotificationPopover() { opened={opened} onChange={setOpened} withArrow + trapFocus + returnFocus + closeOnEscape={!isSubMenuOpen} > @@ -77,17 +84,29 @@ export function NotificationPopover() { - + {t("Notifications")} - </Text> + - + - + @@ -113,10 +132,21 @@ export function NotificationPopover() { - + - + diff --git a/apps/client/src/features/notification/notification.utils.ts b/apps/client/src/features/notification/notification.utils.ts index 266bfc278..83b1b2891 100644 --- a/apps/client/src/features/notification/notification.utils.ts +++ b/apps/client/src/features/notification/notification.utils.ts @@ -1,3 +1,4 @@ +import i18n from "@/i18n.ts"; import { INotification } from "./types/notification.types"; export function formatRelativeTime(dateStr: string): string { @@ -8,15 +9,15 @@ export function formatRelativeTime(dateStr: string): string { const diffHours = Math.floor(diffMs / 3_600_000); const diffDays = Math.floor(diffMs / 86_400_000); - if (diffMin < 1) return "now"; + if (diffMin < 1) return i18n.t("now"); if (diffMin < 60) return `${diffMin}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; - return date.toLocaleDateString(undefined, { + return new Intl.DateTimeFormat(i18n.language, { month: "short", day: "numeric", - }); + }).format(date); } type TimeGroup = "today" | "yesterday" | "this_week" | "older"; diff --git a/apps/client/src/features/page-details/components/backlinks-list.tsx b/apps/client/src/features/page-details/components/backlinks-list.tsx index 90547f2ca..55fac0a9f 100644 --- a/apps/client/src/features/page-details/components/backlinks-list.tsx +++ b/apps/client/src/features/page-details/components/backlinks-list.tsx @@ -14,7 +14,7 @@ import { BacklinkDirection, IBacklinkPageItem, } from "@/features/page-details/types/backlink.types.ts"; -import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { buildPageUrl, getPageTitle } from "@/features/page/page.utils.ts"; import { getPageIcon } from "@/lib"; interface BacklinksListProps { @@ -86,7 +86,7 @@ export function BacklinksList({ {getPageIcon(item.icon ?? "")} - {item.title || t("Untitled")} + {getPageTitle(item.title, undefined, t)} {item.space?.name && ( diff --git a/apps/client/src/features/page-details/components/backlinks-modal.tsx b/apps/client/src/features/page-details/components/backlinks-modal.tsx index 83fc31147..1fbd771a3 100644 --- a/apps/client/src/features/page-details/components/backlinks-modal.tsx +++ b/apps/client/src/features/page-details/components/backlinks-modal.tsx @@ -23,7 +23,7 @@ export function BacklinksModal({ {t("Backlinks")} - + diff --git a/apps/client/src/features/page-details/components/page-details-aside.tsx b/apps/client/src/features/page-details/components/page-details-aside.tsx index 84209d7a6..89c51027c 100644 --- a/apps/client/src/features/page-details/components/page-details-aside.tsx +++ b/apps/client/src/features/page-details/components/page-details-aside.tsx @@ -16,7 +16,8 @@ import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { useBacklinksCountQuery } from "@/features/page-details/queries/backlinks-query.ts"; import { BacklinksModal } from "./backlinks-modal"; -import { formattedDate, timeAgo } from "@/lib/time.ts"; +import { formattedDate } from "@/lib/time.ts"; +import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { LabelsSection } from "@/features/label/components/labels-section.tsx"; @@ -139,6 +140,7 @@ function StatsSection({ updatedAt: Date | string; }) { const { t } = useTranslation(); + const lastUpdated = useTimeAgo(updatedAt); return ( @@ -150,10 +152,7 @@ function StatsSection({ label={t("Created")} value={formattedDate(new Date(createdAt))} /> - + ); } diff --git a/apps/client/src/features/page-history/components/history-editor.tsx b/apps/client/src/features/page-history/components/history-editor.tsx index d071abc3e..c7fa07036 100644 --- a/apps/client/src/features/page-history/components/history-editor.tsx +++ b/apps/client/src/features/page-history/components/history-editor.tsx @@ -34,7 +34,7 @@ export function HistoryEditor({ }); useEffect(() => { - if (!editor || !content) return; + if (!editor || editor.isDestroyed || !content) return; let decorationSet = DecorationSet.empty; let addedCount = 0; diff --git a/apps/client/src/features/page-history/components/history-modal.tsx b/apps/client/src/features/page-history/components/history-modal.tsx index 08f05c9e9..05768638a 100644 --- a/apps/client/src/features/page-history/components/history-modal.tsx +++ b/apps/client/src/features/page-history/components/history-modal.tsx @@ -32,7 +32,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) { {t("Page history")} - + - + diff --git a/apps/client/src/features/page-history/hooks/use-history-restore.tsx b/apps/client/src/features/page-history/hooks/use-history-restore.tsx index fbeb4e6ed..f457c696a 100644 --- a/apps/client/src/features/page-history/hooks/use-history-restore.tsx +++ b/apps/client/src/features/page-history/hooks/use-history-restore.tsx @@ -42,6 +42,14 @@ export function useHistoryRestore() { const handleRestore = useCallback(() => { if (!activeHistoryData) return; + if ( + !mainEditor || + mainEditor.isDestroyed || + !mainEditorTitle || + mainEditorTitle.isDestroyed + ) { + return; + } mainEditorTitle .chain() diff --git a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx index d02ba6e91..c55a9ecf7 100644 --- a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx +++ b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx @@ -15,15 +15,17 @@ import { IconCornerDownRightDouble, IconDots } from "@tabler/icons-react"; import { Link, useParams } from "react-router-dom"; import classes from "./breadcrumb.module.css"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; -import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { buildPageUrl, getPageTitle } from "@/features/page/page.utils.ts"; +import type { TFunction } from "i18next"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { extractPageSlugId } from "@/lib"; import { useMediaQuery } from "@mantine/hooks"; import { useTranslation } from "react-i18next"; -function getTitle(name: string, icon: string) { - if (icon) { - return `${icon} ${name}`; +function getTitle(node: SpaceTreeNode, t: TFunction) { + const name = getPageTitle(node.name, node.isBase, t); + if (node.icon) { + return `${node.icon} ${name}`; } return name; } @@ -58,7 +60,7 @@ export default function Breadcrumb() { style={{ border: "none" }} > - {getTitle(node.name, node.icon)} + {getTitle(node, t)} @@ -75,7 +77,7 @@ export default function Breadcrumb() { style={{ border: "none" }} > - {getTitle(node.name, node.icon)} + {getTitle(node, t)} @@ -83,7 +85,7 @@ export default function Breadcrumb() { const renderAnchor = useCallback( (node: SpaceTreeNode, isCurrent = false) => ( - + - {getTitle(node.name, node.icon)} + {getTitle(node, t)} ), - [spaceSlug], + [spaceSlug, t], ); const getBreadcrumbItems = () => { diff --git a/apps/client/src/features/page/components/copy-page-modal.tsx b/apps/client/src/features/page/components/copy-page-modal.tsx index 4745f731c..b03bd1e5e 100644 --- a/apps/client/src/features/page/components/copy-page-modal.tsx +++ b/apps/client/src/features/page/components/copy-page-modal.tsx @@ -80,7 +80,7 @@ export default function CopyPageModal({ {t("Copy page")} - + diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index 75b113eaa..e011e9ec4 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -18,7 +18,7 @@ import { IconWifiOff, } from "@tabler/icons-react"; import React, { useEffect, useRef, useState } from "react"; -import useToggleAside from "@/hooks/use-toggle-aside.tsx"; +import { useAsideTriggerProps } from "@/hooks/use-toggle-aside.tsx"; import { useAtom, useAtomValue } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; import { useDisclosure, useHotkeys } from "@mantine/hooks"; @@ -64,7 +64,8 @@ interface PageHeaderMenuProps { } export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const { t } = useTranslation(); - const toggleAside = useToggleAside(); + const commentsTriggerProps = useAsideTriggerProps("comments"); + const tocTriggerProps = useAsideTriggerProps("toc"); const { pageSlug } = useParams(); const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug), @@ -100,7 +101,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { <> - {!readOnly && } + {!readOnly && !page?.isBase && } @@ -109,22 +110,24 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { variant="subtle" color="dark" aria-label={t("Comments")} - onClick={() => toggleAside("comments")} + {...commentsTriggerProps} > - - toggleAside("toc")} - > - - - + {!page?.isBase && ( + + + + + + )} @@ -233,12 +236,14 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { {t("Copy link")} - } - onClick={handleCopyAsMarkdown} - > - {t("Copy as Markdown")} - + {!page?.isBase && ( + } + onClick={handleCopyAsMarkdown} + > + {t("Copy as Markdown")} + + )} )} - + {!page?.isBase && } - }> - - - - + {!page?.isBase && ( + }> + + + + + )} - } - onClick={openHistoryModal} - > - {t("Page history")} - + {!page?.isBase && ( + } + onClick={openHistoryModal} + > + {t("Page history")} + + )} - {!readOnly && ( + {!readOnly && !page?.isBase && ( {t("Move page")} - + diff --git a/apps/client/src/features/page/page.utils.ts b/apps/client/src/features/page/page.utils.ts index 06d0ca8d7..8b0111a28 100644 --- a/apps/client/src/features/page/page.utils.ts +++ b/apps/client/src/features/page/page.utils.ts @@ -1,4 +1,18 @@ import slugify from "@sindresorhus/slugify"; +import type { TFunction } from "i18next"; + +/** + * Display title for a page, with a base-aware empty-title fallback: bases + * fall back to "Untitled base", normal pages to "Untitled". Single chokepoint + * so the fallback stays consistent across the UI. + */ +export function getPageTitle( + title: string | null | undefined, + isBase: boolean | undefined, + t: TFunction, +): string { + return title || (isBase ? t("Untitled base") : t("Untitled")); +} const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => { const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", { diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts index 11ba7f32d..236d0b419 100644 --- a/apps/client/src/features/page/queries/page-query.ts +++ b/apps/client/src/features/page/queries/page-query.ts @@ -193,6 +193,7 @@ export function useRestorePageMutation() { spaceId: restoredPage.spaceId, parentPageId: restoredPage.parentPageId, hasChildren: restoredPage.hasChildren || false, + isBase: restoredPage.isBase, children: [], }; @@ -454,7 +455,11 @@ export function invalidateOnUpdatePage( ...page, items: page.items.map((sidebarPage: IPage) => sidebarPage.id === id - ? { ...sidebarPage, title: title, icon: icon } + ? { + ...sidebarPage, + ...(title !== undefined ? { title } : {}), + ...(icon !== undefined ? { icon } : {}), + } : sidebarPage, ), })), diff --git a/apps/client/src/features/page/services/page-service.ts b/apps/client/src/features/page/services/page-service.ts index 146da7dd1..07ffc824c 100644 --- a/apps/client/src/features/page/services/page-service.ts +++ b/apps/client/src/features/page/services/page-service.ts @@ -132,6 +132,25 @@ export async function exportPage(data: IExportPageParams): Promise { saveAs(req.data, decodedFileName); } +export async function exportPageToDocx(data: { pageId: string }): Promise { + const req = await api.post("/docx-export", data, { + responseType: "blob", + }); + + const fileName = req?.headers["content-disposition"] + .split("filename=")[1] + .replace(/"/g, ""); + + let decodedFileName = fileName; + try { + decodedFileName = decodeURIComponent(fileName); + } catch (err) { + // fallback to raw filename + } + + saveAs(req.data, decodedFileName); +} + export async function importPage(file: File, spaceId: string) { const formData = new FormData(); formData.append("spaceId", spaceId); diff --git a/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx b/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx index c9aad622c..6d1a2b4eb 100644 --- a/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx +++ b/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx @@ -1,12 +1,15 @@ import { Modal, Text, ScrollArea } from "@mantine/core"; +import { IconTable } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx"; +import { EmptyState } from "@/components/ui/empty-state.tsx"; interface Props { opened: boolean; onClose: () => void; pageTitle: string; pageContent: any; + isBase?: boolean; } export default function TrashPageContentModal({ @@ -14,6 +17,7 @@ export default function TrashPageContentModal({ onClose, pageTitle, pageContent, + isBase, }: Props) { const { t } = useTranslation(); const title = pageTitle || t("Untitled"); @@ -28,11 +32,19 @@ export default function TrashPageContentModal({ {t("Preview")} - + - + {isBase ? ( + + ) : ( + + )} diff --git a/apps/client/src/features/page/trash/components/trash.tsx b/apps/client/src/features/page/trash/components/trash.tsx index da33d828f..6863e8921 100644 --- a/apps/client/src/features/page/trash/components/trash.tsx +++ b/apps/client/src/features/page/trash/components/trash.tsx @@ -14,7 +14,6 @@ import { IconDots, IconRestore, IconTrash, - IconFileDescription, } from "@tabler/icons-react"; import { TrashBanner } from "@/features/page/trash/components/trash-banner.tsx"; import { @@ -31,6 +30,7 @@ import { UserInfo } from "@/components/common/user-info.tsx"; import Paginate from "@/components/common/paginate.tsx"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useRestorePageModal } from "@/features/page/hooks/use-restore-page-modal.tsx"; +import { PageListIcon } from "@/components/common/page-list-icon"; export default function Trash() { const { t } = useTranslation(); @@ -47,6 +47,7 @@ export default function Trash() { const [selectedPage, setSelectedPage] = useState<{ title: string; content: any; + isBase?: boolean; } | null>(null); const [modalOpened, setModalOpened] = useState(false); @@ -79,7 +80,11 @@ export default function Trash() { const hasPages = deletedPages && deletedPages.items.length > 0; const handlePageClick = (page: any) => { - setSelectedPage({ title: page.title, content: page.content }); + setSelectedPage({ + title: page.title, + content: page.content, + isBase: page.isBase, + }); setModalOpened(true); }; @@ -118,15 +123,7 @@ export default function Trash() { style={{ cursor: "pointer" }} onClick={() => handlePageClick(page)} > - {page.icon || ( - - - - )} +
{page.title || t("Untitled")} @@ -150,7 +147,11 @@ export default function Trash() { - + @@ -203,6 +204,7 @@ export default function Trash() { onClose={() => setModalOpened(false)} pageTitle={selectedPage.title} pageContent={selectedPage.content} + isBase={selectedPage.isBase} /> )} diff --git a/apps/client/src/features/page/tree/components/doc-tree-row.tsx b/apps/client/src/features/page/tree/components/doc-tree-row.tsx index 347f1f3e7..e3aebe9e9 100644 --- a/apps/client/src/features/page/tree/components/doc-tree-row.tsx +++ b/apps/client/src/features/page/tree/components/doc-tree-row.tsx @@ -6,6 +6,7 @@ import { useState, type ReactNode, } from 'react'; +import { flushSync } from 'react-dom'; import { createRoot } from 'react-dom/client'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { @@ -145,7 +146,15 @@ function DocTreeRowInner(props: Props) { getOffset: pointerOutsideOfPreview({ x: '16px', y: '8px' }), render: ({ container }) => { const root = createRoot(container); - root.render(); + // flushSync forces the preview to paint into `container` + // synchronously, before pragmatic-dnd snapshots it for the + // native drag image. Without it, createRoot's async render + // leaves the container empty at snapshot time, so the browser + // falls back to a default snapshot of the source row (and the + // stale image can linger on screen). + flushSync(() => { + root.render(); + }); return () => root.unmount(); }, }); diff --git a/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx b/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx index 91a21b052..0eccb723c 100644 --- a/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx +++ b/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx @@ -20,6 +20,7 @@ import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import CopyPageModal from "@/features/page/components/copy-page-modal.tsx"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { getPageTitle } from "@/features/page/page.utils"; import { duplicatePage } from "@/features/page/services/page-service.ts"; import { useClipboard } from "@/hooks/use-clipboard"; import { getAppUrl } from "@/lib/config.ts"; @@ -34,6 +35,7 @@ import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts"; import { treeModel } from "@/features/page/tree/model/tree-model"; import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts"; import type { SpaceTreeNode } from "@/features/page/tree/types.ts"; +import classes from "@/features/page/tree/styles/tree.module.css"; export interface NodeMenuProps { node: SpaceTreeNode; @@ -123,9 +125,10 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) { { e.preventDefault(); diff --git a/apps/client/src/features/page/tree/components/space-tree-row.tsx b/apps/client/src/features/page/tree/components/space-tree-row.tsx index 1ed148350..07187cec0 100644 --- a/apps/client/src/features/page/tree/components/space-tree-row.tsx +++ b/apps/client/src/features/page/tree/components/space-tree-row.tsx @@ -9,11 +9,13 @@ import { IconFileDescription, IconPlus, IconPointFilled, + IconTable, } from "@tabler/icons-react"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; import { queryClient } from "@/main.tsx"; import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { getPageTitle } from "@/features/page/page.utils"; import { getPageById } from "@/features/page/services/page-service.ts"; import { useUpdatePageMutation, @@ -161,7 +163,13 @@ export function SpaceTreeRow({ + node.icon ? ( + node.icon + ) : node.isBase ? ( + + ) : ( + + ) } readOnly={!canEdit} removeEmojiAction={handleRemoveEmoji} @@ -169,7 +177,7 @@ export function SpaceTreeRow({ />
- {node.name || t("untitled")} + {getPageTitle(node.name, node.isBase, t)}
@@ -201,13 +209,13 @@ function PageArrow({ isOpen, hasChildren, onToggle }: PageArrowProps) { return ( @@ -220,7 +228,8 @@ function PageArrow({ isOpen, hasChildren, onToggle }: PageArrowProps) { { e.preventDefault(); diff --git a/apps/client/src/features/page/tree/components/space-tree.tsx b/apps/client/src/features/page/tree/components/space-tree.tsx index 1c3aab8ec..3b85d8ea0 100644 --- a/apps/client/src/features/page/tree/components/space-tree.tsx +++ b/apps/client/src/features/page/tree/components/space-tree.tsx @@ -18,6 +18,7 @@ import { mergeRootTrees, } from "@/features/page/tree/utils/utils.ts"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; +import { getPageTitle } from "@/features/page/page.utils"; import { treeModel } from "@/features/page/tree/model/tree-model"; import { getPageBreadcrumbs } from "@/features/page/services/page-service.ts"; import { IPage } from "@/features/page/types/page.types.ts"; @@ -200,7 +201,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) { [], ); const getDragLabel = useCallback( - (n: SpaceTreeNode) => n.name || t("untitled"), + (n: SpaceTreeNode) => getPageTitle(n.name, n.isBase, t), [t], ); diff --git a/apps/client/src/features/page/tree/styles/tree.module.css b/apps/client/src/features/page/tree/styles/tree.module.css index e116a1352..edb88c815 100644 --- a/apps/client/src/features/page/tree/styles/tree.module.css +++ b/apps/client/src/features/page/tree/styles/tree.module.css @@ -57,6 +57,10 @@ flex-shrink: 0; } +.actionIcon { + color: light-dark(var(--mantine-color-dark-3), var(--mantine-color-gray-4)); +} + .text { flex: 1; /* min-width: 0 lets a flex child shrink below its content size — required @@ -87,8 +91,6 @@ padding: 0; } -/* ---- pragmatic-tree additions ---- */ - .rowWrapper { position: relative; display: flex; diff --git a/apps/client/src/features/page/tree/types.ts b/apps/client/src/features/page/tree/types.ts index 6c60b157d..5d43df27e 100644 --- a/apps/client/src/features/page/tree/types.ts +++ b/apps/client/src/features/page/tree/types.ts @@ -7,6 +7,7 @@ export type SpaceTreeNode = { spaceId: string; parentPageId: string; hasChildren: boolean; + isBase?: boolean; canEdit?: boolean; children: SpaceTreeNode[]; }; diff --git a/apps/client/src/features/page/tree/utils/utils.ts b/apps/client/src/features/page/tree/utils/utils.ts index 0c42f9b92..71a2b26ce 100644 --- a/apps/client/src/features/page/tree/utils/utils.ts +++ b/apps/client/src/features/page/tree/utils/utils.ts @@ -24,6 +24,7 @@ export function buildTree(pages: IPage[]): SpaceTreeNode[] { hasChildren: page.hasChildren, spaceId: page.spaceId, parentPageId: page.parentPageId, + isBase: page.isBase, canEdit: page.canEdit ?? page.permissions?.canEdit, children: [], }; @@ -42,10 +43,6 @@ export function findBreadcrumbPath( path: SpaceTreeNode[] = [], ): SpaceTreeNode[] | null { for (const node of tree) { - if (!node.name || node.name.trim() === "") { - node.name = "untitled"; - } - if (node.id === pageId) { return [...path, node]; } diff --git a/apps/client/src/features/page/types/page.types.ts b/apps/client/src/features/page/types/page.types.ts index 0bba09ff1..baa4ef413 100644 --- a/apps/client/src/features/page/types/page.types.ts +++ b/apps/client/src/features/page/types/page.types.ts @@ -12,6 +12,7 @@ export interface IPage { spaceId: string; workspaceId: string; isLocked: boolean; + isBase: boolean; lastUpdatedById: string; createdAt: Date; updatedAt: Date; @@ -98,4 +99,5 @@ export interface IExportPageParams { export enum ExportFormat { HTML = "html", Markdown = "markdown", + Docx = "docx", } diff --git a/apps/client/src/features/search/components/search-spotlight-filters.tsx b/apps/client/src/features/search/components/search-spotlight-filters.tsx index 4502c755a..0b2bcc48c 100644 --- a/apps/client/src/features/search/components/search-spotlight-filters.tsx +++ b/apps/client/src/features/search/components/search-spotlight-filters.tsx @@ -17,6 +17,7 @@ import { import { useTranslation } from "react-i18next"; import { useGetSpacesQuery } from "@/features/space/queries/space-query"; import { SpaceFilterMenu } from "@/features/space/components/space-filter-menu"; +import { RadioMenuItem } from "@/components/ui/radio-menu-item"; import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; import classes from "./search-spotlight-filters.module.css"; @@ -175,6 +176,8 @@ export function SearchSpotlightFilters({ {contentTypeOptions.map((option) => ( !option.disabled && contentType !== option.value && @@ -200,7 +203,7 @@ export function SearchSpotlightFilters({ )}
- {contentType === option.value && } + {contentType === option.value && }
))} diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx index 5a2980a38..4c5269f15 100644 --- a/apps/client/src/features/search/components/search-spotlight.tsx +++ b/apps/client/src/features/search/components/search-spotlight.tsx @@ -1,6 +1,6 @@ import { Spotlight } from "@mantine/spotlight"; import { IconSearch, IconSparkles } from "@tabler/icons-react"; -import { Group, Button } from "@mantine/core"; +import { Group, Button, VisuallyHidden } from "@mantine/core"; import React, { useState, useMemo, useEffect } from "react"; import { useDebouncedValue } from "@mantine/hooks"; import { useTranslation } from "react-i18next"; @@ -126,6 +126,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { } style={{ flex: 1 }} onKeyDown={(e) => { @@ -161,6 +162,18 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { /> + + {isAiMode + ? query.length > 0 && !isAiLoading && !aiSearchResult + ? t("No answer available") + : "" + : query.length > 0 && !isLoading + ? resultItems.length === 0 + ? t("No results found") + : t("{{count}} results found", { count: resultItems.length }) + : ""} + + {isAiMode ? ( <> diff --git a/apps/client/src/features/search/components/share-search-spotlight.tsx b/apps/client/src/features/search/components/share-search-spotlight.tsx index dd0d5181f..eccaeb4db 100644 --- a/apps/client/src/features/search/components/share-search-spotlight.tsx +++ b/apps/client/src/features/search/components/share-search-spotlight.tsx @@ -74,6 +74,7 @@ export function ShareSearchSpotlight({ shareId }: ShareSearchSpotlightProps) { > } /> diff --git a/apps/client/src/features/session/components/session-list.tsx b/apps/client/src/features/session/components/session-list.tsx index 6549a6e5f..9e519942c 100644 --- a/apps/client/src/features/session/components/session-list.tsx +++ b/apps/client/src/features/session/components/session-list.tsx @@ -7,6 +7,7 @@ import { Stack, Table, Text, + VisuallyHidden, } from "@mantine/core"; import { IconDevices } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; @@ -33,11 +34,16 @@ export default function SessionList() { if (isLoading) { return (
+ + {t("Active sessions")} + {t("Device Name")} {t("Last Active")} - + + {t("Action")} + @@ -90,11 +96,18 @@ export default function SessionList() { )}
+ + {t("Active sessions")} + {t("Device Name")} {t("Last Active")} - {otherSessions.length > 0 && } + {otherSessions.length > 0 && ( + + {t("Action")} + + )} diff --git a/apps/client/src/features/share/components/share-list.tsx b/apps/client/src/features/share/components/share-list.tsx index 147c8bb90..aab214cdd 100644 --- a/apps/client/src/features/share/components/share-list.tsx +++ b/apps/client/src/features/share/components/share-list.tsx @@ -7,18 +7,20 @@ import Paginate from "@/components/common/paginate.tsx"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useGetSharesQuery } from "@/features/share/queries/share-query.ts"; import { ISharedItem } from "@/features/share/types/share.types.ts"; -import { format } from "date-fns"; import ShareActionMenu from "@/features/share/components/share-action-menu.tsx"; +import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts"; import { buildSharedPageUrl } from "@/features/page/page.utils.ts"; import { getPageIcon } from "@/lib"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { EmptyState } from "@/components/ui/empty-state.tsx"; import classes from "./share.module.css"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; export default function ShareList() { const { t } = useTranslation(); const { cursor, goNext, goPrev } = useCursorPaginate(); const { data, isLoading } = useGetSharesQuery({ cursor }); + const locale = useDateFnsLocale(); if (!isLoading && data?.items.length === 0) { return ; @@ -38,7 +40,7 @@ export default function ShareList() { {data?.items.map((share: ISharedItem, index: number) => ( - + - {format(new Date(share.createdAt), "MMM dd, yyyy")} + {formatLocalized( + share.createdAt, + "MMM dd, yyyy", + "PP", + locale, + )} diff --git a/apps/client/src/features/share/components/share-shell.tsx b/apps/client/src/features/share/components/share-shell.tsx index 1bd559258..54ddbe347 100644 --- a/apps/client/src/features/share/components/share-shell.tsx +++ b/apps/client/src/features/share/components/share-shell.tsx @@ -42,6 +42,7 @@ import { import { ShareSearchSpotlight } from "@/features/search/components/share-search-spotlight.tsx"; import { shareSearchSpotlight } from "@/features/search/constants"; import ShareBranding from '@/features/share/components/share-branding.tsx'; +import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx"; const MemoizedSharedTree = React.memo(SharedTree); @@ -63,7 +64,7 @@ export default function ShareShell({ const [fullWidth, setFullWidth] = useAtom(sharedPageFullWidthAtom); const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom); const [isResizing, setIsResizing] = useState(false); - const sidebarRef = useRef(null); + const sidebarRef = useRef(null); const startResizing = useCallback((e: React.MouseEvent) => { e.preventDefault(); @@ -122,7 +123,9 @@ export default function ShareShell({ }, [data, treeData, setSharedPageTree, setSharedTreeData]); return ( - + + 1 && { navbar: { @@ -242,7 +245,7 @@ export default function ShareShell({ )} - + {children} {data && shareId && !(data.features?.length > 0) && } @@ -264,5 +267,6 @@ export default function ShareShell({ + ); } diff --git a/apps/client/src/features/space/components/add-space-members-modal.tsx b/apps/client/src/features/space/components/add-space-members-modal.tsx index 5efd32f6b..79910ae3a 100644 --- a/apps/client/src/features/space/components/add-space-members-modal.tsx +++ b/apps/client/src/features/space/components/add-space-members-modal.tsx @@ -1,6 +1,6 @@ import { Button, Divider, Group, Modal, Stack } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import React, { useState } from "react"; +import React, { useId, useState } from "react"; import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.ts"; import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx"; import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx"; @@ -14,6 +14,7 @@ export default function AddSpaceMembersModal({ spaceId, }: AddSpaceMemberModalProps) { const { t } = useTranslation(); + const titleId = useId(); const [opened, { open, close }] = useDisclosure(false); const [memberIds, setMemberIds] = useState([]); const [role, setRole] = useState(SpaceRole.WRITER); @@ -51,24 +52,33 @@ export default function AddSpaceMembersModal({ return ( <> - - + + + + + {t("Add space members")} + + + + - - - - + + + + - - - - + + + + + + ); } diff --git a/apps/client/src/features/space/components/create-space-form.tsx b/apps/client/src/features/space/components/create-space-form.tsx index 3d4804704..b156704a3 100644 --- a/apps/client/src/features/space/components/create-space-form.tsx +++ b/apps/client/src/features/space/components/create-space-form.tsx @@ -1,4 +1,4 @@ -import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core"; +import { Group, Box, Button, TextInput, Stack, Textarea, Text } from "@mantine/core"; import React, { useEffect } from "react"; import { useForm } from "@mantine/form"; import { zod4Resolver } from "mantine-form-zod-resolver"; @@ -17,8 +17,8 @@ const formSchema = z.object({ .min(2) .max(100) .regex( - /^[a-zA-Z0-9-]+$/, - "Space slug can only contain letters, numbers, and hyphens", + /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, + "Space slug must start with a letter or number and may contain hyphens and underscores", ), description: z.string().max(500), }); @@ -69,10 +69,25 @@ export function CreateSpaceForm() { navigate(getSpaceUrl(createdSpace.slug)); }; + function handleValidationFailure(errors: Record) { + const firstInvalidId = Object.keys(errors)[0]; + if (firstInvalidId) { + document.getElementById(firstInvalidId)?.focus(); + } + } + return ( <> -
handleSubmit(values))}> + handleSubmit(values), + handleValidationFailure, + )} + > + + {t("* indicates required fields")} + @@ -89,6 +106,7 @@ export function CreateSpaceForm() { label={t("Space slug")} placeholder={t("e.g product")} variant="filled" + errorProps={{ role: "alert" }} {...form.getInputProps("slug")} /> @@ -100,6 +118,7 @@ export function CreateSpaceForm() { autosize minRows={2} maxRows={8} + errorProps={{ role: "alert" }} {...form.getInputProps("description")} /> diff --git a/apps/client/src/features/space/components/create-space-modal.tsx b/apps/client/src/features/space/components/create-space-modal.tsx index 152e1950c..22f3e40c3 100644 --- a/apps/client/src/features/space/components/create-space-modal.tsx +++ b/apps/client/src/features/space/components/create-space-modal.tsx @@ -11,7 +11,12 @@ export default function CreateSpaceModal() { <> - + diff --git a/apps/client/src/features/space/components/edit-space-form.tsx b/apps/client/src/features/space/components/edit-space-form.tsx index 72aae7565..5abe4144c 100644 --- a/apps/client/src/features/space/components/edit-space-form.tsx +++ b/apps/client/src/features/space/components/edit-space-form.tsx @@ -15,8 +15,8 @@ const formSchema = z.object({ .min(2) .max(100) .regex( - /^[a-zA-Z0-9-]+$/, - "Space slug can only contain letters, numbers, and hyphens", + /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, + "Space slug must start with a letter or number and may contain hyphens and underscores", ), }); diff --git a/apps/client/src/features/space/components/settings-modal.tsx b/apps/client/src/features/space/components/settings-modal.tsx index fb8d24f63..3d24d100c 100644 --- a/apps/client/src/features/space/components/settings-modal.tsx +++ b/apps/client/src/features/space/components/settings-modal.tsx @@ -48,7 +48,7 @@ export default function SpaceSettingsModal({ {space?.name} - +
diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx index 51e529ba4..f5c6ea0e7 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx @@ -18,6 +18,7 @@ import { IconSettings, IconStar, IconStarFilled, + IconTemplate, IconTrash, } from "@tabler/icons-react"; import { @@ -53,6 +54,11 @@ import { import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import { searchSpotlight } from "@/features/search/constants"; +import TemplatePickerModal from "@/ee/template/components/template-picker-modal"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; +import { Feature } from "@/ee/features"; +import { ErrorBoundary } from "react-error-boundary"; export function SpaceSidebar() { const { t } = useTranslation(); @@ -246,6 +252,12 @@ function SpaceMenu({ useDisclosure(false); const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); + const [ + templatePickerOpened, + { open: openTemplatePicker, close: closeTemplatePicker }, + ] = useDisclosure(false); + const hasTemplates = useHasFeature(Feature.TEMPLATES); + const upgradeLabel = useUpgradeLabel(); const { data: watchStatus } = useSpaceWatchStatusQuery(spaceId); const watchMutation = useWatchSpaceMutation(); @@ -315,6 +327,27 @@ function SpaceMenu({ {isWatching ? t("Stop watching space") : t("Watch space")} + {canManagePages && ( + <> + + + } + data-disabled={!hasTemplates || undefined} + aria-disabled={!hasTemplates || undefined} + > + {t("Templates")} + + + + )} + {canManagePages && ( <> @@ -370,6 +403,16 @@ function SpaceMenu({ /> )} + + {hasTemplates && templatePickerOpened && ( + null}> + + + )} ); } diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index 89e0f64ee..7531a5926 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -38,6 +38,8 @@ export function SwitchSpace({ shadow="md" opened={opened} onChange={toggle} + trapFocus + returnFocus >
+ + + {t("List of spaces in this workspace")} + + {t("Space")} {t("Members")} - + + {t("Action")} + {spaces.length > 0 ? ( spaces.map((space) => ( - + prefetchSpace(space.slug, space.id)} > - + diff --git a/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx b/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx index 131de2995..a2a2ec263 100644 --- a/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx +++ b/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx @@ -1,4 +1,4 @@ -import { Text, SimpleGrid, Card, rem, Group, Box, Button } from "@mantine/core"; +import { Text, SimpleGrid, Card, rem, Group, Box, Button, Title } from "@mantine/core"; import { useState } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -30,9 +30,9 @@ export default function FavoriteSpacesGrid() { return ( - + {t("Favorite spaces")} - </Text> + {visibleSpaces.map((fav) => ( @@ -53,6 +53,7 @@ export default function FavoriteSpacesGrid() { diff --git a/apps/client/src/features/space/permissions/permissions.type.ts b/apps/client/src/features/space/permissions/permissions.type.ts index 8e4b4de95..cf5dbc355 100644 --- a/apps/client/src/features/space/permissions/permissions.type.ts +++ b/apps/client/src/features/space/permissions/permissions.type.ts @@ -11,6 +11,9 @@ export enum SpaceCaslSubject { Page = "page", } +// Bases are pages and inherit Page permissions — a separate Base +// subject was redundant and has been dropped from the server's casl +// rules too. Anything that used to check Base now checks Page. export type SpaceAbility = | [SpaceCaslAction, SpaceCaslSubject.Settings] | [SpaceCaslAction, SpaceCaslSubject.Member] diff --git a/apps/client/src/features/space/types/space.types.ts b/apps/client/src/features/space/types/space.types.ts index c856d88a8..6937b233c 100644 --- a/apps/client/src/features/space/types/space.types.ts +++ b/apps/client/src/features/space/types/space.types.ts @@ -24,6 +24,7 @@ export interface ISpace { description: string; logo?: string; slug: string; + isPersonal?: boolean; hostname: string; creatorId: string; createdAt: Date; diff --git a/apps/client/src/features/user/components/change-email.tsx b/apps/client/src/features/user/components/change-email.tsx index 16f74b427..8c841b401 100644 --- a/apps/client/src/features/user/components/change-email.tsx +++ b/apps/client/src/features/user/components/change-email.tsx @@ -36,7 +36,13 @@ export default function ChangeEmail() { */} - + {t( "To change your email, you have to enter your password and new email.", @@ -80,6 +86,11 @@ function ChangeEmailForm() { placeholder={t("Enter your password")} variant="filled" mb="md" + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("password")} /> diff --git a/apps/client/src/features/user/components/change-password.tsx b/apps/client/src/features/user/components/change-password.tsx index 0925cdc48..9c0b4cb3d 100644 --- a/apps/client/src/features/user/components/change-password.tsx +++ b/apps/client/src/features/user/components/change-password.tsx @@ -95,6 +95,11 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) { variant="filled" mb="md" data-autofocus + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("oldPassword")} /> @@ -103,6 +108,11 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) { placeholder={t("Enter your new password")} variant="filled" mb="md" + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("newPassword")} /> diff --git a/apps/client/src/features/user/components/notification-pref.tsx b/apps/client/src/features/user/components/notification-pref.tsx index e8a983ede..cce519f91 100644 --- a/apps/client/src/features/user/components/notification-pref.tsx +++ b/apps/client/src/features/user/components/notification-pref.tsx @@ -3,7 +3,7 @@ import { updateUser } from "@/features/user/services/user-service.ts"; import { IUser, IUserSettings } from "@/features/user/types/user.types.ts"; import { Switch, Text, Title, Stack } from "@mantine/core"; import { useAtom } from "jotai"; -import React, { useState } from "react"; +import React, { useId, useState } from "react"; import { useTranslation } from "react-i18next"; import { ResponsiveSettingsRow, @@ -64,6 +64,8 @@ function NotificationToggle({ description: string; }) { const { t } = useTranslation(); + const switchId = useId(); + const descriptionId = useId(); const [user, setUser] = useAtom(userAtom); const [checked, setChecked] = useState( user.settings?.notifications?.[settingKey] !== false, @@ -83,14 +85,21 @@ function NotificationToggle({ return ( - {t(label)} - + + {t(label)} + + {t(description)} - + ); @@ -101,7 +110,7 @@ export default function NotificationPref() { return ( - {t("Email notifications")} + {t("Email notifications")} {notificationItems.map((item) => ( { const data: WebSocketEvent = event; let entity = null; - let queryKeyId = null; switch (data.operation) { case "invalidate": @@ -106,21 +105,23 @@ export const useQuerySubscription = () => { case "deleteTreeNode": invalidateOnDeletePage(data.payload.node.id); break; - case "updateOne": + case "updateOne": { entity = data.entity[0]; - if (entity === "pages") { - // we have to do this because the usePageQuery cache key is the slugId. - queryKeyId = data.payload.slugId; - } else { - queryKeyId = data.id; - } + const keyIds = + entity === "pages" ? [data.payload.slugId, data.id] : [data.id]; - // only update if data was already in cache - if (queryClient.getQueryData([...data.entity, queryKeyId])) { - queryClient.setQueryData([...data.entity, queryKeyId], { - ...queryClient.getQueryData([...data.entity, queryKeyId]), - ...data.payload, - }); + for (const keyId of keyIds) { + if (!keyId) continue; + const cached = queryClient.getQueryData>([ + ...data.entity, + keyId, + ]); + if (cached) { + queryClient.setQueryData([...data.entity, keyId], { + ...cached, + ...data.payload, + }); + } } if (entity === "pages") { @@ -132,20 +133,8 @@ export const useQuerySubscription = () => { data.payload.icon, ); } - - /* - queryClient.setQueriesData( - { queryKey: [data.entity, data.id] }, - (oldData: any) => { - const update = (entity: Record) => - entity.id === data.id ? { ...entity, ...data.payload } : entity; - return Array.isArray(oldData) - ? oldData.map(update) - : update(oldData as Record); - }, - ); - */ break; + } case "refetchRootTreeNodeEvent": { const spaceId = data.spaceId; queryClient.refetchQueries({ diff --git a/apps/client/src/features/websocket/use-tree-socket.ts b/apps/client/src/features/websocket/use-tree-socket.ts index c4cd083b4..e1c19f29f 100644 --- a/apps/client/src/features/websocket/use-tree-socket.ts +++ b/apps/client/src/features/websocket/use-tree-socket.ts @@ -48,6 +48,11 @@ export const useTreeSocket = () => { icon: event.payload.icon, } as Partial); } + if (event.payload?.isBase !== undefined) { + next = treeModel.update(next, event.id, { + isBase: event.payload.isBase, + } as Partial); + } return next; }); } diff --git a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx index 5569f8e89..e87e95ee9 100644 --- a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx @@ -73,7 +73,11 @@ export default function InviteActionMenu({ invitationId }: Props) { arrowPosition="center" > - + diff --git a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx index ce1c588a0..f8fd035fb 100644 --- a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx @@ -12,9 +12,14 @@ import useUserRole from "@/hooks/use-user-role.tsx"; interface Props { userId: string; + name: string; deactivatedAt: Date | null; } -export default function MemberActionMenu({ userId, deactivatedAt }: Props) { +export default function MemberActionMenu({ + userId, + name, + deactivatedAt, +}: Props) { const { t } = useTranslation(); const deleteWorkspaceMemberMutation = useDeleteWorkspaceMemberMutation(); const deactivateMutation = useDeactivateWorkspaceMemberMutation(); @@ -86,7 +91,7 @@ export default function MemberActionMenu({ userId, deactivatedAt }: Props) { diff --git a/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx b/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx index aa9d3d90a..b9278c8aa 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx @@ -56,6 +56,12 @@ export function WorkspaceInviteForm({ onClose }: Props) { maxDropdownHeight={200} maxTags={50} onChange={setEmails} + data-autofocus + autoComplete="off" + data-1p-ignore + data-lpignore="true" + data-bwignore + data-form-type="other" />