Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9aea1a9e0 | |||
| 536dbf5e49 | |||
| 3881d62b6b | |||
| 6bdb0516b2 | |||
| 4730dc2fb9 | |||
| f7a9e82037 | |||
| aca63b7185 | |||
| 5557759f0b | |||
| 08986e701f | |||
| 9abbf12864 |
+5
-5
@@ -1,6 +1,6 @@
|
||||
**/node_modules
|
||||
**/.git
|
||||
**/dist
|
||||
node_modules
|
||||
.git
|
||||
dist
|
||||
/data
|
||||
**/.env*
|
||||
**/.nx
|
||||
.env*
|
||||
.nx
|
||||
|
||||
+1
-16
@@ -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 | azure
|
||||
# options: local | s3
|
||||
STORAGE_DRIVER=local
|
||||
|
||||
# S3 driver config
|
||||
@@ -21,11 +21,6 @@ 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,18 +43,8 @@ POSTMARK_TOKEN=
|
||||
# for custom drawio server
|
||||
DRAWIO_URL=
|
||||
|
||||
# Gotenberg URL for server-side PDF export
|
||||
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
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
.env.prod
|
||||
data
|
||||
# compiled output
|
||||
dist
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
|
||||
@@ -28,8 +28,6 @@ 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
|
||||
|
||||
+65
-78
@@ -1,98 +1,85 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.90.1",
|
||||
"version": "0.70.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.8.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5",
|
||||
"@atlaskit/pragmatic-drag-and-drop-flourish": "2.0.15",
|
||||
"@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:*",
|
||||
"@casl/react": "^5.0.1",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
|
||||
"@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",
|
||||
"@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",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tanstack/react-query": "5.90.17",
|
||||
"@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",
|
||||
"clsx": "2.1.1",
|
||||
"file-saver": "2.0.5",
|
||||
"highlightjs-sap-abap": "0.3.0",
|
||||
"i18next": "25.10.1",
|
||||
"i18next-http-backend": "3.0.6",
|
||||
"jotai": "2.20.1",
|
||||
"jotai-optics": "0.4.0",
|
||||
"js-cookie": "3.0.7",
|
||||
"jwt-decode": "4.0.0",
|
||||
"alfaaz": "^1.1.0",
|
||||
"axios": "^1.13.6",
|
||||
"blueimp-load-image": "^5.16.0",
|
||||
"clsx": "^2.1.1",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlightjs-sap-abap": "^0.3.0",
|
||||
"i18next": "^25.10.1",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jotai": "^2.18.1",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"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",
|
||||
"nanoid": "3.3.8",
|
||||
"posthog-js": "1.391.2",
|
||||
"react": "19.2.7",
|
||||
"lowlight": "^3.3.0",
|
||||
"mantine-form-zod-resolver": "^1.3.0",
|
||||
"mermaid": "^11.13.0",
|
||||
"mitt": "^3.0.1",
|
||||
"posthog-js": "1.363.1",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "3.4.0",
|
||||
"react-clear-modal": "^2.0.18",
|
||||
"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.18.0",
|
||||
"semver": "7.7.4",
|
||||
"socket.io-client": "4.8.3",
|
||||
"zod": "4.3.6"
|
||||
"react-dom": "^18.3.1",
|
||||
"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",
|
||||
"semver": "^7.7.4",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.28.0",
|
||||
"@tanstack/eslint-plugin-query": "5.94.4",
|
||||
"@testing-library/jest-dom": "6.6.0",
|
||||
"@testing-library/react": "16.1.0",
|
||||
"@types/blueimp-load-image": "5.16.6",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/katex": "0.16.8",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.94.4",
|
||||
"@types/blueimp-load-image": "^5.16.6",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/katex": "^0.16.8",
|
||||
"@types/node": "22.19.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",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-refresh": "0.5.2",
|
||||
"globals": "15.13.0",
|
||||
"jsdom": "25.0.0",
|
||||
"optics-ts": "2.4.1",
|
||||
"postcss": "8.5.14",
|
||||
"postcss-preset-mantine": "1.18.0",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"vite": "8.0.16",
|
||||
"vitest": "4.1.6"
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^15.13.0",
|
||||
"optics-ts": "^2.4.1",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"Add members": "Mitglieder hinzufügen",
|
||||
"Add to groups": "Zu Gruppen hinzufügen",
|
||||
"Add space members": "Bereichsmitglieder hinzufügen",
|
||||
"Add to favorites": "Zu Favoriten hinzufügen",
|
||||
"Admin": "Administrator",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
@@ -45,22 +44,22 @@
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Details",
|
||||
"e.g ACME": "z. B. ACME",
|
||||
"e.g ACME Inc": "z. B. ACME GmbH",
|
||||
"e.g Developers": "z. B. Entwickler",
|
||||
"e.g Group for developers": "z. B. Gruppe für Entwickler",
|
||||
"e.g product": "z. B. Produkt",
|
||||
"e.g Product Team": "z. B. Produktteam",
|
||||
"e.g Sales": "z. B. Vertrieb",
|
||||
"e.g Space for product team": "z. B. Bereich für das Produktteam",
|
||||
"e.g Space for sales team to collaborate": "z. B. Bereich zur Zusammenarbeit für das Vertriebsteam",
|
||||
"e.g ACME": "z.B. ACME",
|
||||
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||
"e.g Developers": "z.B. Entwickler",
|
||||
"e.g Group for developers": "z.B. Gruppe für Entwickler",
|
||||
"e.g product": "z.B. Produkt",
|
||||
"e.g Product Team": "z.B. Produktteam",
|
||||
"e.g Sales": "z.B. Vertrieb",
|
||||
"e.g Space for product team": "z.B. Bereich für das Produktteam",
|
||||
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit",
|
||||
"Edit": "Bearbeiten",
|
||||
"Read": "Lesen",
|
||||
"Edit group": "Gruppe bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Enter a strong password": "Geben Sie ein starkes Passwort ein",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
|
||||
"enter valid emails addresses": "Geben Sie gültige E-Mail-Adressen ein",
|
||||
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben",
|
||||
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
|
||||
"enter your full name": "Geben Sie Ihren vollständigen Namen ein",
|
||||
"Enter your new password": "Geben Sie Ihr neues Passwort ein",
|
||||
@@ -71,14 +70,10 @@
|
||||
"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.",
|
||||
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
|
||||
"Favorite spaces": "Favorisierte Bereiche",
|
||||
"Favorite spaces appear here": "Favorisierte Bereiche werden hier angezeigt",
|
||||
"Favorites": "Favoriten",
|
||||
"Full access": "Voller Zugriff",
|
||||
"Full page width": "Volle Seitenbreite",
|
||||
"Full width": "Volle Breite",
|
||||
@@ -92,12 +87,11 @@
|
||||
"Import pages": "Seiten importieren",
|
||||
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
|
||||
"Importing pages": "Seiten werden importiert",
|
||||
"invalid invitation link": "Ungültiger Einladungslink",
|
||||
"invalid invitation link": "ungültiger Einladungslink",
|
||||
"Invitation signup": "Einladung zur Anmeldung",
|
||||
"Invite by email": "Einladen per E-Mail",
|
||||
"Invite members": "Mitglieder einladen",
|
||||
"Invite new members": "Neue Mitglieder einladen",
|
||||
"Invite People": "Personen einladen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
|
||||
"Join the workspace": "Dem Arbeitsbereich beitreten",
|
||||
@@ -112,7 +106,7 @@
|
||||
"Member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"Members": "Mitglieder",
|
||||
"My preferences": "Meine Einstellungen",
|
||||
"My preferences": "Meine Voreinstellungen",
|
||||
"My Profile": "Mein Profil",
|
||||
"My profile": "Mein Profil",
|
||||
"Name": "Name",
|
||||
@@ -140,12 +134,11 @@
|
||||
"People": "Personen",
|
||||
"Pending": "Ausstehend",
|
||||
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
|
||||
"Preferences": "Einstellungen",
|
||||
"Preferences": "Vorlieben",
|
||||
"Print PDF": "PDF drucken",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Kürzlich aktualisiert",
|
||||
"Remove": "Entfernen",
|
||||
"Remove from favorites": "Aus Favoriten entfernen",
|
||||
"Remove group member": "Gruppenmitglied entfernen",
|
||||
"Remove space member": "Bereichsmitglied entfernen",
|
||||
"Restore": "Wiederherstellen",
|
||||
@@ -158,12 +151,12 @@
|
||||
"Search...": "Suche...",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select role": "Rolle auswählen",
|
||||
"Select role to assign to all invited members": "Wählen Sie die Rolle aus, die allen eingeladenen Mitgliedern zugewiesen werden soll",
|
||||
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen",
|
||||
"Select theme": "Design auswählen",
|
||||
"Send invitation": "Einladung senden",
|
||||
"Invitation sent": "Einladung gesendet",
|
||||
"Settings": "Einstellungen",
|
||||
"Setup workspace": "Workspace einrichten",
|
||||
"Setup workspace": "Arbeitsbereich einrichten",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign Up": "Registrieren",
|
||||
"Slug": "Slug",
|
||||
@@ -172,17 +165,16 @@
|
||||
"Space menu": "Bereichsmenü",
|
||||
"Space name": "Bereichsname",
|
||||
"Space settings": "Bereichseinstellungen",
|
||||
"Space slug": "Bereichs-Slug",
|
||||
"Space slug": "Slug des Bereichs",
|
||||
"Spaces": "Bereiche",
|
||||
"Spaces you belong to": "Bereiche, zu denen Sie gehören",
|
||||
"No space found": "Kein Bereich gefunden",
|
||||
"Spaces you belong to": "Bereiche, denen Sie angehören",
|
||||
"No space found": "Keine Bereiche gefunden",
|
||||
"Search for spaces": "Nach Bereichen suchen",
|
||||
"Start typing to search...": "Anfangen zu tippen, um zu suchen...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Erfolgreich importiert",
|
||||
"Successfully restored": "Erfolgreich wiederhergestellt",
|
||||
"System settings": "Systemeinstellungen",
|
||||
"Templates": "Vorlagen",
|
||||
"Theme": "Design",
|
||||
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
|
||||
"Toggle full page width": "Volle Seitenbreite umschalten",
|
||||
@@ -191,9 +183,9 @@
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated successfully": "Erfolgreich aktualisiert",
|
||||
"User": "Benutzer",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspace-Name",
|
||||
"Workspace settings": "Workspace-Einstellungen",
|
||||
"Workspace": "Arbeitsbereich",
|
||||
"Workspace Name": "Arbeitsbereichsname",
|
||||
"Workspace settings": "Arbeitsbereich-Einstellungen",
|
||||
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
|
||||
"Your Email": "Ihre E-Mail",
|
||||
"Your import is complete.": "Ihr Import ist abgeschlossen.",
|
||||
@@ -223,8 +215,6 @@
|
||||
"Edit comment": "Kommentar bearbeiten",
|
||||
"Delete comment": "Kommentar löschen",
|
||||
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||
"Delete chat": "Chat löschen",
|
||||
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Sind Sie sicher, dass Sie '{{title}}' löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"Comment created successfully": "Kommentar erfolgreich erstellt",
|
||||
"Error creating comment": "Fehler beim Erstellen des Kommentars",
|
||||
"Comment updated successfully": "Kommentar erfolgreich aktualisiert",
|
||||
@@ -233,12 +223,12 @@
|
||||
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
|
||||
"Comment resolved successfully": "Kommentar erfolgreich gelöst",
|
||||
"Comment re-opened successfully": "Kommentar erfolgreich wieder geöffnet",
|
||||
"Comment unresolved successfully": "Kommentar erfolgreich als ungelöst markiert",
|
||||
"Comment unresolved successfully": "Kommentar erfolgreich ungelöst",
|
||||
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
|
||||
"Resolve comment": "Kommentar lösen",
|
||||
"Unresolve comment": "Kommentar als ungelöst markieren",
|
||||
"Unresolve comment": "Kommentar nicht lösen",
|
||||
"Resolve Comment Thread": "Kommentarthread lösen",
|
||||
"Unresolve Comment Thread": "Kommentarthread als ungelöst markieren",
|
||||
"Unresolve Comment Thread": "Kommentarthread nicht lösen",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Sind Sie sicher, dass Sie diesen Kommentarthread lösen möchten? Dies wird als abgeschlossen markiert.",
|
||||
"Are you sure you want to unresolve this comment thread?": "Sind Sie sicher, dass Sie diesen Kommentarthread nicht lösen möchten?",
|
||||
"Resolved": "Gelöst",
|
||||
@@ -251,7 +241,7 @@
|
||||
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
|
||||
"Invite link": "Einladungslink",
|
||||
"Copy": "Kopieren",
|
||||
"Copy to space": "In Bereich kopieren",
|
||||
"Copy to space": "In Raum kopieren",
|
||||
"Copied": "Kopiert",
|
||||
"Duplicate": "Duplizieren",
|
||||
"Select a user": "Benutzer auswählen",
|
||||
@@ -261,7 +251,7 @@
|
||||
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
|
||||
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
|
||||
"Confirm space name": "Bereichsnamen bestätigen",
|
||||
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Unterseiten einbeziehen",
|
||||
@@ -277,9 +267,6 @@
|
||||
"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",
|
||||
@@ -290,19 +277,6 @@
|
||||
"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",
|
||||
@@ -315,11 +289,6 @@
|
||||
"Save & Exit": "Speichern & Beenden",
|
||||
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
|
||||
"Paste link": "Link einfügen",
|
||||
"Paste link or search pages": "Link einfügen oder Seiten durchsuchen",
|
||||
"Link to web page": "Link zur Webseite",
|
||||
"Recents": "Zuletzt verwendet",
|
||||
"Page or URL": "Seite oder URL",
|
||||
"Link title": "Linktitel",
|
||||
"Edit link": "Link bearbeiten",
|
||||
"Remove link": "Link entfernen",
|
||||
"Add link": "Link hinzufügen",
|
||||
@@ -365,11 +334,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.",
|
||||
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||
"Uploading {{name}}": "Lade {{name}} hoch",
|
||||
"Uploading file": "Datei wird hochgeladen",
|
||||
@@ -380,26 +346,20 @@
|
||||
"Divider": "Trennlinie",
|
||||
"Quote": "Zitat",
|
||||
"Image": "Bild",
|
||||
"Audio": "Audio",
|
||||
"Embed PDF": "PDF einbetten",
|
||||
"Upload and embed a PDF file.": "Laden Sie eine PDF-Datei hoch und betten Sie sie ein.",
|
||||
"Embed as PDF": "Als PDF einbetten",
|
||||
"Failed to load PDF": "Fehler beim Laden der PDF",
|
||||
"Convert to attachment": "In Anhang umwandeln",
|
||||
"File attachment": "Dateianhang",
|
||||
"Toggle block": "Umschaltblock",
|
||||
"Callout": "Hinweisblock",
|
||||
"Toggle block": "Block umschalten",
|
||||
"Callout": "Hinweisbox",
|
||||
"Insert callout notice.": "Hinweisbox einfügen.",
|
||||
"Math inline": "Mathe inline",
|
||||
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
|
||||
"Math block": "Matheblock",
|
||||
"Insert math equation": "Mathematische Gleichung einfügen",
|
||||
"Insert math equation": "Mathe-Gleichung einfügen",
|
||||
"Mermaid diagram": "Mermaid-Diagramm",
|
||||
"Insert mermaid diagram": "Mermaid-Diagramm einfügen",
|
||||
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
|
||||
"Insert current date": "Aktuelles Datum einfügen",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
|
||||
"Multiple": "Mehrfach",
|
||||
"Multiple": "Mehrere",
|
||||
"Turn into": "In verwandeln",
|
||||
"Text align": "Text ausrichten",
|
||||
"This page may have been deleted, moved, or you may not have access.": "\"Diese Seite wurde möglicherweise gelöscht, verschoben oder Sie haben keinen Zugriff darauf.\"",
|
||||
@@ -407,14 +367,10 @@
|
||||
"Pages you create will show up here.": "\"Die von Ihnen erstellten Seiten werden hier angezeigt.\"",
|
||||
"Heading {{level}}": "Überschrift {{level}}",
|
||||
"Toggle title": "Titel umschalten",
|
||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie etwas. Geben Sie \"/\" für Befehle ein",
|
||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein",
|
||||
"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",
|
||||
"{{count}} Columns": "{count, plural, one {# Spalte} other {# Spalten}}",
|
||||
"Equal columns": "Gleich breite Spalten",
|
||||
"Left sidebar": "Linke Seitenleiste",
|
||||
"Right sidebar": "Rechte Seitenleiste",
|
||||
@@ -424,9 +380,9 @@
|
||||
"Names do not match": "Namen stimmen nicht überein",
|
||||
"Today, {{time}}": "Heute, {{time}}",
|
||||
"Yesterday, {{time}}": "Gestern, {{time}}",
|
||||
"Space created successfully": "Bereich erfolgreich erstellt",
|
||||
"Space updated successfully": "Bereich erfolgreich aktualisiert",
|
||||
"Space deleted successfully": "Bereich erfolgreich gelöscht",
|
||||
"Space created successfully": "Der Bereich wurde erfolgreich erstellt",
|
||||
"Space updated successfully": "Der Bereich wurde erfolgreich aktualisiert",
|
||||
"Space deleted successfully": "Der Bereich wurde erfolgreich gelöscht",
|
||||
"Members added successfully": "Mitglieder erfolgreich hinzugefügt",
|
||||
"Member removed successfully": "Mitglied erfolgreich entfernt",
|
||||
"Member role updated successfully": "Mitgliederrolle erfolgreich aktualisiert",
|
||||
@@ -434,12 +390,11 @@
|
||||
"Created at: {{time}}": "Erstellt am: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Zeichenanzahl: {{characterCount}}",
|
||||
"Character count: {{characterCount}}": "Zeichenzahl: {{characterCount}}",
|
||||
"New update": "Neues Update",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} ist verfügbar",
|
||||
"Default page edit mode": "Standard-Bearbeitungsmodus für Seiten",
|
||||
"Default page edit mode": "Standard-Seitenbearbeitungsmodus",
|
||||
"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",
|
||||
@@ -458,35 +413,33 @@
|
||||
"Table of contents": "Inhaltsverzeichnis",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "Fügen Sie Überschriften (H1, H2, H3) hinzu, um ein Inhaltsverzeichnis zu erstellen.",
|
||||
"Share": "Teilen",
|
||||
"Public sharing": "Öffentliche Freigabe",
|
||||
"Public sharing": "Öffentliches Teilen",
|
||||
"Shared by": "Geteilt von",
|
||||
"Shared at": "Geteilt am",
|
||||
"Inherits public sharing from": "Übernimmt öffentliche Freigabe von",
|
||||
"Inherits public sharing from": "Erbt das öffentliche Teilen von",
|
||||
"Share to web": "Im Web teilen",
|
||||
"Shared to web": "Im Web geteilt",
|
||||
"Anyone with the link can view this page": "Jeder mit dem Link kann diese Seite ansehen",
|
||||
"Make this page publicly accessible": "Diese Seite öffentlich zugänglich machen",
|
||||
"Include sub-pages": "Unterseiten einschließen",
|
||||
"Make sub-pages public too": "Unterseiten ebenfalls öffentlich machen",
|
||||
"Allow search engines to index page": "Suchmaschinen das Indexieren der Seite erlauben",
|
||||
"Include sub-pages": "Unterseiten einbeziehen",
|
||||
"Make sub-pages public too": "Unterseiten auch öffentlich machen",
|
||||
"Allow search engines to index page": "Suchmaschinen erlauben, die Seite zu indexieren",
|
||||
"Open page": "Seite öffnen",
|
||||
"Page": "Seite",
|
||||
"Delete public share link": "Öffentlichen Freigabelink löschen",
|
||||
"Delete share": "Freigabe löschen",
|
||||
"Are you sure you want to delete this shared link?": "Möchten Sie diesen Freigabelink wirklich löschen?",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Öffentlich freigegebene Seiten aus Bereichen, in denen Sie Mitglied sind, werden hier angezeigt",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Öffentlich geteilte Seiten aus Bereichen, in denen Sie Mitglied sind, erscheinen hier",
|
||||
"Share deleted successfully": "Freigabe erfolgreich gelöscht",
|
||||
"Share not found": "Freigabe nicht gefunden",
|
||||
"Failed to share page": "Seite konnte nicht geteilt werden",
|
||||
"Failed to share page": "Fehler beim Teilen der Seite",
|
||||
"Disable public sharing": "Öffentliches Teilen deaktivieren",
|
||||
"Prevent members from sharing pages publicly.": "Verhindern Sie, dass Mitglieder Seiten öffentlich teilen.",
|
||||
"Toggle public sharing": "Öffentliches Teilen umschalten",
|
||||
"Toggle space public sharing": "Öffentliches Teilen im Bereich umschalten",
|
||||
"Allow viewers to comment": "Zuschauern erlauben, Kommentare zu hinterlassen",
|
||||
"Allow viewers to add comments on pages in this space.": "Erlauben Sie Zuschauern, Kommentare auf Seiten in diesem Bereich hinzuzufügen.",
|
||||
"Toggle viewer comments": "Zuschauerkommentare umschalten",
|
||||
"Public sharing is disabled at the workspace level": "Öffentliches Teilen ist auf der Arbeitsbereichsebene deaktiviert",
|
||||
"Prevent pages in this space from being shared publicly.": "Verhindern Sie, dass Seiten in diesem Bereich öffentlich geteilt werden.",
|
||||
"Requires an enterprise license": "Erfordert eine Unternehmenslizenz",
|
||||
"Page permissions": "Seitenberechtigungen",
|
||||
"Control who can view and edit individual pages. Available with an enterprise license.": "Steuern Sie, wer einzelne Seiten ansehen und bearbeiten kann. Verfügbar mit einer Enterprise-Lizenz.",
|
||||
"Enable public sharing": "Öffentliches Teilen aktivieren",
|
||||
@@ -501,84 +454,83 @@
|
||||
"Copy page to a different space.": "Seite in einen anderen Bereich kopieren.",
|
||||
"Page copied successfully": "Seite erfolgreich kopiert",
|
||||
"Page duplicated successfully": "Seite erfolgreich dupliziert",
|
||||
"Find": "Suchen",
|
||||
"Find": "Finden",
|
||||
"Not found": "Nicht gefunden",
|
||||
"Previous Match (Shift+Enter)": "Vorheriger Treffer (Umschalt+Eingabe)",
|
||||
"Next match (Enter)": "Nächster Treffer (Eingabe)",
|
||||
"Previous Match (Shift+Enter)": "Vorheriger Treffer (Shift+Enter)",
|
||||
"Next match (Enter)": "Nächster Treffer (Enter)",
|
||||
"Match case (Alt+C)": "Groß-/Kleinschreibung beachten (Alt+C)",
|
||||
"Replace": "Ersetzen",
|
||||
"Close (Escape)": "Schließen (Escape)",
|
||||
"Replace (Enter)": "Ersetzen (Eingabe)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Alle ersetzen (Strg+Alt+Eingabe)",
|
||||
"Replace (Enter)": "Ersetzen (Enter)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Alle ersetzen (Ctrl+Alt+Enter)",
|
||||
"Replace all": "Alle ersetzen",
|
||||
"View all": "Alle anzeigen",
|
||||
"View all spaces": "Alle Bereiche anzeigen",
|
||||
"View all spaces": "Alle Räume anzeigen",
|
||||
"Error": "Fehler",
|
||||
"Failed to disable MFA": "MFA konnte nicht deaktiviert werden",
|
||||
"Failed to disable MFA": "Deaktivierung der MFA fehlgeschlagen",
|
||||
"Disable two-factor authentication": "Zwei-Faktor-Authentifizierung deaktivieren",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Die Deaktivierung der Zwei-Faktor-Authentifizierung macht Ihr Konto weniger sicher. Sie benötigen nur Ihr Passwort, um sich anzumelden.",
|
||||
"Please enter your password to disable two-factor authentication:": "Bitte geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren:",
|
||||
"Two-factor authentication has been enabled": "Die Zwei-Faktor-Authentifizierung wurde aktiviert",
|
||||
"Two-factor authentication has been disabled": "Die Zwei-Faktor-Authentifizierung wurde deaktiviert",
|
||||
"2-step verification": "Bestätigung in zwei Schritten",
|
||||
"Two-factor authentication has been enabled": "Zwei-Faktor-Authentifizierung wurde aktiviert",
|
||||
"Two-factor authentication has been disabled": "Zwei-Faktor-Authentifizierung wurde deaktiviert",
|
||||
"2-step verification": "2-Schritt-Verifizierung",
|
||||
"Protect your account with an additional verification layer when signing in.": "Schützen Sie Ihr Konto mit einer zusätzlichen Verifizierungsschicht beim Anmelden.",
|
||||
"Two-factor authentication is active on your account.": "Die Zwei-Faktor-Authentifizierung ist auf Ihrem Konto aktiv.",
|
||||
"Add 2FA method": "2FA-Methode hinzufügen",
|
||||
"Backup codes": "Backup-Codes",
|
||||
"Backup codes": "Sicherungscodes",
|
||||
"Disable": "Deaktivieren",
|
||||
"Invalid verification code": "Ungültiger Bestätigungscode",
|
||||
"New backup codes have been generated": "Neue Backup-Codes wurden erstellt",
|
||||
"Failed to regenerate backup codes": "Backup-Codes konnten nicht neu erstellt werden",
|
||||
"About backup codes": "Über Backup-Codes",
|
||||
"New backup codes have been generated": "Neue Sicherungscodes wurden generiert",
|
||||
"Failed to regenerate backup codes": "Fehler beim Generieren neuer Sicherungscodes",
|
||||
"About backup codes": "Über Sicherungscodes",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Sicherungscodes können verwendet werden, um auf Ihr Konto zuzugreifen, wenn Sie den Zugang zu Ihrer Authenticator-App verlieren. Jeder Code kann nur einmal verwendet werden.",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Sie können jederzeit neue Sicherungscodes generieren. Dies wird alle vorhandenen Codes ungültig machen.",
|
||||
"Confirm password": "Passwort bestätigen",
|
||||
"Generate new backup codes": "Neue Backup-Codes erstellen",
|
||||
"Save your new backup codes": "Speichern Sie Ihre neuen Backup-Codes",
|
||||
"Generate new backup codes": "Neue Sicherungscodes generieren",
|
||||
"Save your new backup codes": "Speichern Sie Ihre neuen Sicherungscodes",
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Speichern Sie diese Codes an einem sicheren Ort. Ihre alten Sicherungscodes sind nicht mehr gültig.",
|
||||
"Your new backup codes": "Ihre neuen Backup-Codes",
|
||||
"I've saved my backup codes": "Ich habe meine Backup-Codes gespeichert",
|
||||
"Failed to setup MFA": "MFA konnte nicht eingerichtet werden",
|
||||
"Setup & Verify": "Einrichten und bestätigen",
|
||||
"Your new backup codes": "Ihre neuen Sicherungscodes",
|
||||
"I've saved my backup codes": "Ich habe meine Sicherungscodes gespeichert",
|
||||
"Failed to setup MFA": "Fehler beim Einrichten der MFA",
|
||||
"Setup & Verify": "Einrichten & Überprüfen",
|
||||
"Add to authenticator": "Zum Authenticator hinzufügen",
|
||||
"1. Scan this QR code with your authenticator app": "1. Scannen Sie diesen QR-Code mit Ihrer Authenticator-App",
|
||||
"Can't scan the code?": "Code kann nicht gescannt werden?",
|
||||
"Enter this code manually in your authenticator app:": "Geben Sie diesen Code manuell in Ihrer Authenticator-App ein:",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. Geben Sie den 6-stelligen Code aus Ihrem Authenticator ein",
|
||||
"Verify and enable": "Bestätigen und aktivieren",
|
||||
"Verify and enable": "Überprüfen und aktivieren",
|
||||
"Failed to generate QR code. Please try again.": "Fehler beim Generieren des QR-Codes. Bitte versuchen Sie es erneut.",
|
||||
"Backup": "Backup",
|
||||
"Backup": "Sicherung",
|
||||
"Save codes": "Codes speichern",
|
||||
"Save your backup codes": "Speichern Sie Ihre Backup-Codes",
|
||||
"Save your backup codes": "Speichern Sie Ihre Sicherungscodes",
|
||||
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Diese Codes können verwendet werden, um auf Ihr Konto zuzugreifen, wenn Sie den Zugang zu Ihrer Authenticator-App verlieren. Jeder Code kann nur einmal verwendet werden.",
|
||||
"Print": "Drucken",
|
||||
"Two-factor authentication has been set up. Please log in again.": "Zwei-Faktor-Authentifizierung wurde eingerichtet. Bitte melden Sie sich erneut an.",
|
||||
"Two-Factor authentication required": "Zwei-Faktor-Authentifizierung erforderlich",
|
||||
"Your workspace requires two-factor authentication for all users": "Ihr Workspace erfordert Zwei-Faktor-Authentifizierung für alle Benutzer",
|
||||
"Your workspace requires two-factor authentication for all users": "Ihr Arbeitsbereich erfordert die Zwei-Faktor-Authentifizierung für alle Benutzer",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Um weiterhin auf Ihren Arbeitsbereich zuzugreifen, müssen Sie die Zwei-Faktor-Authentifizierung einrichten. Dies fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu.",
|
||||
"Set up two-factor authentication": "Zwei-Faktor-Authentifizierung einrichten",
|
||||
"Cancel and logout": "Abbrechen und abmelden",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "Ihr Arbeitsbereich erfordert eine Zwei-Faktor-Authentifizierung. Bitte richten Sie diese ein, um fortzufahren.",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Dadurch wird Ihrem Konto eine zusätzliche Sicherheitsebene hinzugefügt, indem ein Bestätigungscode von Ihrer Authenticator-App verlangt wird.",
|
||||
"Password is required": "Passwort ist erforderlich",
|
||||
"Password must be at least 8 characters": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
||||
"Password is required": "Passwort erforderlich",
|
||||
"Password must be at least 8 characters": "Passwort muss mindestens 8 Zeichen lang sein",
|
||||
"Please enter a 6-digit code": "Bitte geben Sie einen 6-stelligen Code ein",
|
||||
"Code must be exactly 6 digits": "Der Code muss genau 6 Ziffern haben",
|
||||
"Enter the 6-digit code found in your authenticator app": "Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein",
|
||||
"Code must be exactly 6 digits": "Code muss genau 6-stellig sein",
|
||||
"Enter the 6-digit code found in your authenticator app": "Geben Sie den 6-stelligen Code ein, der in Ihrer Authenticator-App zu finden ist",
|
||||
"Need help authenticating?": "Brauchen Sie Hilfe bei der Authentifizierung?",
|
||||
"MFA QR Code": "MFA-QR-Code",
|
||||
"MFA QR Code": "MFA QR-Code",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "Konto erfolgreich erstellt. Bitte melden Sie sich an, um die Zwei-Faktor-Authentifizierung einzurichten.",
|
||||
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an und führen Sie die Zwei-Faktor-Authentifizierung durch.",
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an, um die Zwei-Faktor-Authentifizierung einzurichten.",
|
||||
"Password reset was successful. Please log in with your new password.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
|
||||
"Two-factor authentication": "Zwei-Faktor-Authentifizierung",
|
||||
"Use authenticator app instead": "Stattdessen Authenticator-App verwenden",
|
||||
"Verify backup code": "Backup-Code bestätigen",
|
||||
"Use backup code": "Backup-Code verwenden",
|
||||
"Enter one of your backup codes": "Geben Sie einen Ihrer Backup-Codes ein",
|
||||
"Backup code": "Backup-Code",
|
||||
"Verify backup code": "Sicherungscode überprüfen",
|
||||
"Use backup code": "Sicherungscode verwenden",
|
||||
"Enter one of your backup codes": "Geben Sie einen Ihrer Sicherungscodes ein",
|
||||
"Backup code": "Sicherungscode",
|
||||
"Enter one of your backup codes. Each backup code can only be used once.": "Geben Sie einen Ihrer Sicherungscodes ein. Jeder Sicherungscode kann nur einmal verwendet werden.",
|
||||
"Verify": "Bestätigen",
|
||||
"Verify": "Überprüfen",
|
||||
"Trash": "Papierkorb",
|
||||
"Pages in trash will be permanently deleted after {{count}} days.": "Seiten im Papierkorb werden nach {{count}} Tagen endgültig gelöscht.",
|
||||
"Deleted": "Gelöscht",
|
||||
@@ -589,66 +541,68 @@
|
||||
"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",
|
||||
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> 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",
|
||||
"Deleted at": "Gelöscht am",
|
||||
"Preview": "Vorschau",
|
||||
"Subpages": "Unterseiten",
|
||||
"Failed to load subpages": "Unterseiten konnten nicht geladen werden",
|
||||
"Failed to load subpages": "Fehler beim Laden von Unterseiten",
|
||||
"No subpages": "Keine Unterseiten",
|
||||
"Subpages (Child pages)": "Unterseiten (untergeordnete Seiten)",
|
||||
"Subpages (Child pages)": "Unterseiten (Untergeordnete Seiten)",
|
||||
"List all subpages of the current page": "Alle Unterseiten der aktuellen Seite auflisten",
|
||||
"Attachments": "Anhänge",
|
||||
"All spaces": "Alle Bereiche",
|
||||
"Unknown": "Unbekannt",
|
||||
"Find a space": "Einen Bereich finden",
|
||||
"Search in all your spaces": "In all Ihren Bereichen suchen",
|
||||
"Type": "Typ",
|
||||
"Enterprise": "Enterprise",
|
||||
"Search in all your spaces": "In all deinen Bereichen suchen",
|
||||
"Type": "Art",
|
||||
"Enterprise": "Unternehmen",
|
||||
"Download attachment": "Anhang herunterladen",
|
||||
"Allowed email domains": "Erlaubte E-Mail-Domains",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Nur Benutzer mit E-Mail-Adressen aus diesen Domains können sich per SSO registrieren.",
|
||||
"Enter valid domain names separated by comma or space": "Geben Sie gültige Domainnamen ein, getrennt durch Komma oder Leerzeichen",
|
||||
"Enforce two-factor authentication": "Zwei-Faktor-Authentifizierung erzwingen",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Nur Benutzer mit E-Mail-Adressen aus diesen Domains können sich über SSO registrieren.",
|
||||
"Enter valid domain names separated by comma or space": "Geben Sie gültige Domainnamen ein, durch Kommas oder Leerzeichen getrennt",
|
||||
"Enforce two-factor authentication": "Erzwingen der Zwei-Faktor-Authentifizierung",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Sobald es erzwungen wird, müssen alle Mitglieder die Zwei-Faktor-Authentifizierung aktivieren, um auf den Arbeitsbereich zugreifen zu können.",
|
||||
"Toggle MFA enforcement": "MFA-Erzwingung umschalten",
|
||||
"Toggle MFA enforcement": "Umschalten der MFA-Erzwingung",
|
||||
"Display name": "Anzeigename",
|
||||
"Allow signup": "Registrierung erlauben",
|
||||
"Enabled": "Aktiviert",
|
||||
"Advanced Settings": "Erweiterte Einstellungen",
|
||||
"Enable TLS/SSL": "TLS/SSL aktivieren",
|
||||
"Use secure connection to LDAP server": "Sichere Verbindung zum LDAP-Server verwenden",
|
||||
"Group sync": "Gruppensynchronisierung",
|
||||
"Group sync": "Gruppensynchronisation",
|
||||
"No SSO providers found.": "Keine SSO-Anbieter gefunden.",
|
||||
"Delete SSO provider": "SSO-Anbieter löschen",
|
||||
"Are you sure you want to delete this SSO provider?": "Sind Sie sicher, dass Sie diesen SSO-Anbieter löschen möchten?",
|
||||
"Action": "Aktion",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration",
|
||||
"Icon": "Symbol",
|
||||
"Icon": "Icon",
|
||||
"Upload image": "Bild hochladen",
|
||||
"Remove image": "Bild entfernen",
|
||||
"Failed to remove image": "Fehler beim Entfernen des Bildes",
|
||||
"Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.",
|
||||
"Image removed successfully": "Bild erfolgreich entfernt",
|
||||
"API key": "API-Schlüssel",
|
||||
"API key created successfully": "API-Schlüssel erfolgreich erstellt",
|
||||
"API keys": "API-Schlüssel",
|
||||
"API management": "API-Verwaltung",
|
||||
"Are you sure you want to revoke this API key": "Sind Sie sicher, dass Sie diesen API-Schlüssel widerrufen möchten?",
|
||||
"Create API Key": "API-Schlüssel erstellen",
|
||||
"Custom expiration date": "Benutzerdefiniertes Ablaufdatum",
|
||||
"Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein",
|
||||
"Expiration": "Ablauf",
|
||||
"Expired": "Abgelaufen",
|
||||
"Expires": "Läuft ab",
|
||||
"I've saved my API key": "Ich habe meinen API-Schlüssel gespeichert",
|
||||
"Last use": "Zuletzt verwendet",
|
||||
"No API keys found": "Keine API-Schlüssel gefunden",
|
||||
"No expiration": "Kein Ablauf",
|
||||
"Revoke API key": "API-Schlüssel widerrufen",
|
||||
"Revoked successfully": "Erfolgreich widerrufen",
|
||||
"Select expiration date": "Ablaufdatum wählen",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.",
|
||||
"Update": "Aktualisieren",
|
||||
"Update {{credential}}": "{{credential}} aktualisieren",
|
||||
"Update API key": "API-Schlüssel aktualisieren",
|
||||
"Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich",
|
||||
"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.",
|
||||
@@ -659,7 +613,6 @@
|
||||
"AI Answer": "KI-Antwort",
|
||||
"Ask AI": "KI fragen",
|
||||
"AI is thinking...": "Die KI überlegt...",
|
||||
"Thinking": "Denkt nach",
|
||||
"Ask a question...": "Fragen stellen...",
|
||||
"AI Answers": "KI-Antworten",
|
||||
"AI-powered search (AI Answers)": "KI-unterstützte Suche (KI-Antworten)",
|
||||
@@ -668,9 +621,7 @@
|
||||
"Generative AI (Ask AI)": "Generative KI (KI fragen)",
|
||||
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Aktivieren Sie die KI-unterstützte Inhaltserstellung im Editor. Ermöglicht Benutzern das Erzeugen, Verbessern, Übersetzen und Transformieren von Text.",
|
||||
"Toggle generative AI": "Generative KI umschalten",
|
||||
"Upgrade your plan": "Upgrade Ihres Plans",
|
||||
"Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz",
|
||||
"Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.",
|
||||
"Enterprise feature": "Enterprise-Funktion",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "KI ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
|
||||
"AI & MCP": "KI & MCP",
|
||||
"AI": "KI",
|
||||
@@ -678,15 +629,17 @@
|
||||
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
|
||||
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Aktivieren Sie den MCP-Server, damit KI-Assistenten und -Tools mit den Inhalten Ihres Arbeitsbereichs interagieren können.",
|
||||
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
|
||||
"MCP documentation": "MCP-Dokumentation",
|
||||
"MCP Server URL": "MCP-Server-URL",
|
||||
"Use your API key for authentication. You can manage API keys in your account settings.": "Verwenden Sie Ihren API-Schlüssel zur Authentifizierung. API-Schlüssel können in Ihren Kontoeinstellungen verwaltet werden.",
|
||||
"Supported tools": "Unterstützte Tools",
|
||||
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In Ihrem Arbeitsbereich ist MCP aktiviert. Verwenden Sie Ihren API-Schlüssel, um KI-Assistenten anzubinden.",
|
||||
"MCP server URL:": "MCP-Server-URL:",
|
||||
"Learn more": "Mehr erfahren",
|
||||
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Verwalten Sie API-Schlüssel für alle Nutzer im Arbeitsbereich. Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
|
||||
"View the <anchor>API documentation</anchor> for usage details.": "Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
|
||||
"View the <anchor>MCP documentation</anchor>.": "Sehen Sie die <anchor>MCP-Dokumentation</anchor> ein.",
|
||||
"View the": "Anzeigen",
|
||||
"for usage details.": "für Informationen zur Nutzung.",
|
||||
"for setup instructions.": "für Einrichtungshinweise.",
|
||||
"API documentation": "API-Dokumentation",
|
||||
"Sources": "Quellen",
|
||||
"AI Answers not available for attachments": "KI-Antworten sind für Anhänge nicht verfügbar",
|
||||
"No answer available": "Keine Antwort verfügbar",
|
||||
@@ -701,34 +654,12 @@
|
||||
"Mark all as read": "Alle als gelesen markieren",
|
||||
"Mark as read": "Als gelesen markieren",
|
||||
"More options": "Weitere Optionen",
|
||||
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> hat Sie in einem Kommentar erwähnt",
|
||||
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> hat einen Kommentar auf einer Seite hinterlassen",
|
||||
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> hat einen Kommentar gelöst",
|
||||
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> hat Sie auf einer Seite erwähnt",
|
||||
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> hat Ihnen Bearbeitungszugriff auf eine Seite gegeben",
|
||||
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> hat Ihnen Ansichtszugriff auf eine Seite gegeben",
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> hat eine Seite aktualisiert",
|
||||
"Watch page": "Seite beobachten",
|
||||
"Stop watching": "Nicht mehr beobachten",
|
||||
"Watch space": "Bereich beobachten",
|
||||
"Stop watching space": "Bereich nicht mehr beobachten",
|
||||
"Email notifications": "E-Mail-Benachrichtigungen",
|
||||
"Page updates": "Seitenaktualisierungen",
|
||||
"Get notified when pages you watch are updated.": "Erhalten Sie eine Benachrichtigung, wenn Seiten, die Sie beobachten, aktualisiert werden.",
|
||||
"Page mentions": "Seiten-Erwähnungen",
|
||||
"Get notified when someone mentions you on a page.": "Erhalten Sie eine Benachrichtigung, wenn Sie jemand auf einer Seite erwähnt.",
|
||||
"Comment mentions": "Kommentar-Erwähnungen",
|
||||
"Get notified when someone mentions you in a comment.": "Erhalten Sie eine Benachrichtigung, wenn Sie jemand in einem Kommentar erwähnt.",
|
||||
"New comments": "Neue Kommentare",
|
||||
"Get notified about new comments on threads you participate in.": "Erhalten Sie eine Benachrichtigung über neue Kommentare in Threads, an denen Sie teilnehmen.",
|
||||
"Resolved comments": "Erledigte Kommentare",
|
||||
"Get notified when your comment is resolved.": "Erhalten Sie eine Benachrichtigung, wenn Ihr Kommentar erledigt wurde.",
|
||||
"You are now watching this page": "Sie beobachten diese Seite jetzt",
|
||||
"You are no longer watching this page": "Sie beobachten diese Seite nicht mehr",
|
||||
"You are now watching this space": "Sie beobachten diesen Bereich jetzt",
|
||||
"You are no longer watching this space": "Sie beobachten diesen Bereich nicht mehr",
|
||||
"Direct": "Direkt",
|
||||
"Updates": "Aktualisierungen",
|
||||
"mentioned you in a comment": "hat Sie in einem Kommentar erwähnt",
|
||||
"commented on a page": "hat auf einer Seite kommentiert",
|
||||
"resolved a comment": "hat einen Kommentar gelöst",
|
||||
"mentioned you on a page": "hat Sie auf einer Seite erwähnt",
|
||||
"gave you edit access to a page": "hat Ihnen Bearbeitungsrechte für eine Seite gegeben",
|
||||
"gave you view access to a page": "hat Ihnen Leserechte für eine Seite gewährt",
|
||||
"Today": "Heute",
|
||||
"Yesterday": "Gestern",
|
||||
"This week": "Diese Woche",
|
||||
@@ -762,327 +693,5 @@
|
||||
"Failed to update trash retention": "Aktualisierung der Aufbewahrungsdauer des Papierkorbs fehlgeschlagen",
|
||||
"Removed page restriction": "Seitenbeschränkung entfernt",
|
||||
"Added page permission": "Seitenberechtigung hinzugefügt",
|
||||
"Removed page permission": "Seitenberechtigung entfernt",
|
||||
"day": "Tag",
|
||||
"days": "Tage",
|
||||
"week": "Woche",
|
||||
"weeks": "Wochen",
|
||||
"month": "Monat",
|
||||
"months": "Monate",
|
||||
"year": "Jahr",
|
||||
"years": "Jahre",
|
||||
"Period": "Zeitraum",
|
||||
"Fixed date": "Festes Datum",
|
||||
"Indefinitely": "Unbegrenzt",
|
||||
"Days": "Tage",
|
||||
"Weeks": "Wochen",
|
||||
"Months": "Monate",
|
||||
"Years": "Jahre",
|
||||
"Pick a date": "Datum auswählen",
|
||||
"Maximum is {{max}} {{unit}} for this unit": "Das Maximum für diese Einheit beträgt {{max}} {{unit}}",
|
||||
"Never expires. Verifiers can re-verify at any time.": "Läuft nie ab. Prüfer können die Seite jederzeit erneut verifizieren.",
|
||||
"Verified": "Verifiziert",
|
||||
"Review needed": "Prüfung erforderlich",
|
||||
"Verification expired": "Verifizierung abgelaufen",
|
||||
"Draft": "Entwurf",
|
||||
"In Approval": "In Genehmigung",
|
||||
"In approval": "In Genehmigung",
|
||||
"Approved": "Genehmigt",
|
||||
"Obsolete": "Veraltet",
|
||||
"Expiring": "Läuft bald ab",
|
||||
"Set up verification": "Verifizierung einrichten",
|
||||
"Verify page": "Seite verifizieren",
|
||||
"Page verification": "Seitenverifizierung",
|
||||
"Add verification": "Verifizierung hinzufügen",
|
||||
"Edit verification": "Verifizierung bearbeiten",
|
||||
"Search by title": "Nach Titel suchen",
|
||||
"Choose how this page should stay accurate.": "Wählen Sie aus, wie diese Seite aktuell gehalten werden soll.",
|
||||
"Recurring verification": "Wiederkehrende Verifizierung",
|
||||
"Verifiers re-confirm this page on a schedule.": "Prüfer bestätigen diese Seite nach einem Zeitplan erneut.",
|
||||
"Re-verify on a schedule (e.g every 30 days )": "Nach einem Zeitplan erneut verifizieren (z. B. alle 30 Tage)",
|
||||
"Page stays editable at all times": "Die Seite bleibt jederzeit bearbeitbar",
|
||||
"Best for runbooks, FAQs, living documentation": "Am besten für Runbooks, FAQs und lebende Dokumentation geeignet",
|
||||
"Approval workflow": "Genehmigungsworkflow",
|
||||
"Formal document lifecycle with named approvers.": "Formaler Dokumentenlebenszyklus mit benannten Genehmigern.",
|
||||
"Draft → In approval → Approved → Obsolete": "Entwurf → In Genehmigung → Genehmigt → Veraltet",
|
||||
"Locked once approved, with full history": "Nach der Genehmigung gesperrt, mit vollständiger Historie",
|
||||
"Designed for ISO 9001, ISO 13485, and FDA": "Entwickelt für ISO 9001, ISO 13485 und FDA",
|
||||
"Best for SOPs and controlled documents": "Am besten für SOPs und kontrollierte Dokumente geeignet",
|
||||
"Back": "Zurück",
|
||||
"Quality management": "Qualitätsmanagement",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"Pages move through draft, approval, and approved stages.": "Seiten durchlaufen die Phasen Entwurf, Genehmigung und Genehmigt.",
|
||||
"Verifiers": "Prüfer",
|
||||
"Add verifier": "Prüfer hinzufügen",
|
||||
"I've reviewed this page for accuracy": "Ich habe diese Seite auf Richtigkeit geprüft",
|
||||
"Set up": "Einrichten",
|
||||
"Remove verification": "Verifizierung entfernen",
|
||||
"Are you sure you want to remove verification from this page?": "Möchten Sie die Verifizierung wirklich von dieser Seite entfernen?",
|
||||
"Assigned verifiers must periodically re-verify this page.": "Zugewiesene Prüfer müssen diese Seite regelmäßig erneut verifizieren.",
|
||||
"Last verified by {{name}} {{time}} (expired)": "Zuletzt von {{name}} {{time}} verifiziert (abgelaufen)",
|
||||
"The fixed expiration date has passed.": "Das feste Ablaufdatum ist überschritten.",
|
||||
"Verified by {{name}} {{time}}": "Verifiziert von {{name}} {{time}}",
|
||||
"Expires {{date}}": "Läuft ab am {{date}}",
|
||||
"Expired {{date}}": "Abgelaufen am {{date}}",
|
||||
"Mark as obsolete": "Als veraltet markieren",
|
||||
"Mark obsolete": "Als veraltet markieren",
|
||||
"Returned by {{name}} {{time}}": "Zurückgegeben von {{name}} {{time}}",
|
||||
"No approval has been requested yet.": "Es wurde noch keine Genehmigung angefordert.",
|
||||
"Submitted by {{name}} {{time}}": "Eingereicht von {{name}} {{time}}",
|
||||
"Someone": "Jemand",
|
||||
"Approved by {{name}} {{time}}": "Genehmigt von {{name}} {{time}}",
|
||||
"This document has been marked as obsolete.": "Dieses Dokument wurde als veraltet markiert.",
|
||||
"Rejection comment": "Ablehnungskommentar",
|
||||
"Reason for returning this document...": "Grund für die Rückgabe dieses Dokuments...",
|
||||
"Confirm rejection": "Ablehnung bestätigen",
|
||||
"Submit for approval": "Zur Genehmigung einreichen",
|
||||
"Reject": "Ablehnen",
|
||||
"Approve": "Genehmigen",
|
||||
"Re-submit for approval": "Erneut zur Genehmigung einreichen",
|
||||
"Verified until": "Verifiziert bis",
|
||||
"QMS": "QMS",
|
||||
"Verified pages": "Verifizierte Seiten",
|
||||
"Search pages...": "Seiten suchen...",
|
||||
"Filter by space": "Nach Bereich filtern",
|
||||
"Filter by type": "Nach Typ filtern",
|
||||
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> hat eine Seite verifiziert",
|
||||
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> hat eine Seite zu Ihrer Genehmigung eingereicht",
|
||||
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> hat eine Seite zur Überarbeitung zurückgegeben",
|
||||
"Page verification expires soon": "Die Seitenverifizierung läuft bald ab",
|
||||
"Page verification has expired": "Die Seitenverifizierung ist abgelaufen",
|
||||
"Verifying your email": "Ihre E-Mail wird bestätigt",
|
||||
"Please wait...": "Bitte warten...",
|
||||
"Verification failed. The link may have expired.": "Überprüfung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.",
|
||||
"Check your email": "Prüfen Sie Ihre E-Mails",
|
||||
"We sent a verification link to {{email}}.": "Wir haben einen Bestätigungslink an {{email}} gesendet.",
|
||||
"We sent a verification link to your email.": "Wir haben einen Bestätigungslink an Ihre E-Mail-Adresse gesendet.",
|
||||
"Click the link to verify your email and access your workspace.": "Klicken Sie auf den Link, um Ihre E-Mail zu bestätigen und auf Ihren Arbeitsbereich zuzugreifen.",
|
||||
"Resend verification email": "Bestätigungs-E-Mail erneut senden",
|
||||
"Verification email sent. Please check your inbox.": "Bestätigungs-E-Mail gesendet. Bitte überprüfen Sie Ihr Postfach.",
|
||||
"Failed to resend verification email. Please try again.": "Fehler beim erneuten Senden der Bestätigungs-E-Mail. Bitte versuchen Sie es erneut.",
|
||||
"We've sent you an email with your associated workspaces.": "Wir haben Ihnen eine E-Mail mit Ihren zugehörigen Arbeitsbereichen gesendet.",
|
||||
"Load more": "Mehr laden",
|
||||
"Log out of all devices": "Auf allen Geräten abmelden",
|
||||
"Log out of all sessions except this device": "Von allen Sitzungen außer diesem Gerät abmelden",
|
||||
"This Device": "Dieses Gerät",
|
||||
"Unknown device": "Unbekanntes Gerät",
|
||||
"No active sessions": "Keine aktiven Sitzungen",
|
||||
"Session revoked": "Sitzung widerrufen",
|
||||
"All other sessions revoked": "Alle anderen Sitzungen widerrufen",
|
||||
"Last used": "Zuletzt verwendet",
|
||||
"Created": "Erstellt",
|
||||
"Rename": "Umbenennen",
|
||||
"Publish": "Veröffentlichen",
|
||||
"Security": "Sicherheit",
|
||||
"Enforce SSO": "SSO erzwingen",
|
||||
"Once enforced, members will not be able to login with email and password.": "Sobald dies erzwungen wird, können sich Mitglieder nicht mehr mit E-Mail und Passwort anmelden.",
|
||||
"AI-generated content may not be accurate.": "KI-generierte Inhalte sind möglicherweise nicht korrekt.",
|
||||
"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.",
|
||||
"How can I help you today?": "Wie kann ich Ihnen heute helfen?",
|
||||
"New chat": "Neuer Chat",
|
||||
"No chat history": "Kein Chatverlauf",
|
||||
"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",
|
||||
"Translate this page": "Diese Seite übersetzen",
|
||||
"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?",
|
||||
"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"
|
||||
"Removed page permission": "Seitenberechtigung entfernt"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"Add members": "Add members",
|
||||
"Add to groups": "Add to groups",
|
||||
"Add space members": "Add space members",
|
||||
"Add to favorites": "Add to favorites",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||
@@ -41,8 +40,6 @@
|
||||
"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",
|
||||
@@ -73,32 +70,10 @@
|
||||
"Export": "Export",
|
||||
"Failed to create page": "Failed to create page",
|
||||
"Failed to delete page": "Failed to delete page",
|
||||
"Failed to restore page": "Failed to restore page",
|
||||
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||
"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",
|
||||
"Full access": "Full access",
|
||||
"Full page width": "Full page width",
|
||||
"Full width": "Full width",
|
||||
@@ -117,7 +92,6 @@
|
||||
"Invite by email": "Invite by email",
|
||||
"Invite members": "Invite members",
|
||||
"Invite new members": "Invite new members",
|
||||
"Invite People": "Invite People",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||
"Join the workspace": "Join the workspace",
|
||||
@@ -165,7 +139,6 @@
|
||||
"Profile": "Profile",
|
||||
"Recently updated": "Recently updated",
|
||||
"Remove": "Remove",
|
||||
"Remove from favorites": "Remove from favorites",
|
||||
"Remove group member": "Remove group member",
|
||||
"Remove space member": "Remove space member",
|
||||
"Restore": "Restore",
|
||||
@@ -202,7 +175,6 @@
|
||||
"Successfully imported": "Successfully imported",
|
||||
"Successfully restored": "Successfully restored",
|
||||
"System settings": "System settings",
|
||||
"Templates": "Templates",
|
||||
"Theme": "Theme",
|
||||
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||
"Toggle full page width": "Toggle full page width",
|
||||
@@ -243,8 +215,6 @@
|
||||
"Edit comment": "Edit comment",
|
||||
"Delete comment": "Delete comment",
|
||||
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||
"Delete chat": "Delete chat",
|
||||
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Are you sure you want to delete '{{title}}'? This action cannot be undone.",
|
||||
"Comment created successfully": "Comment created successfully",
|
||||
"Error creating comment": "Error creating comment",
|
||||
"Comment updated successfully": "Comment updated successfully",
|
||||
@@ -297,9 +267,6 @@
|
||||
"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",
|
||||
@@ -310,19 +277,6 @@
|
||||
"Add row above": "Add row above",
|
||||
"Add row below": "Add row below",
|
||||
"Delete table": "Delete table",
|
||||
"Add column left": "Add column left",
|
||||
"Add column right": "Add column right",
|
||||
"Clear cell": "Clear cell",
|
||||
"Clear cells": "Clear cells",
|
||||
"Toggle header cell": "Toggle header cell",
|
||||
"Toggle header column": "Toggle header column",
|
||||
"Toggle header row": "Toggle header row",
|
||||
"Move column left": "Move column left",
|
||||
"Move column right": "Move column right",
|
||||
"Move row down": "Move row down",
|
||||
"Move row up": "Move row up",
|
||||
"Sort A → Z": "Sort A → Z",
|
||||
"Sort Z → A": "Sort Z → A",
|
||||
"Info": "Info",
|
||||
"Note": "Note",
|
||||
"Success": "Success",
|
||||
@@ -385,11 +339,8 @@
|
||||
"Create block quote.": "Create block quote.",
|
||||
"Insert code snippet.": "Insert code snippet.",
|
||||
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||
"Page break": "Page break",
|
||||
"Insert a page break for printing.": "Insert a page break for printing.",
|
||||
"Upload any image from your device.": "Upload any image from your device.",
|
||||
"Upload any video from your device.": "Upload any video from your device.",
|
||||
"Upload any audio from your device.": "Upload any audio from your device.",
|
||||
"Upload any file from your device.": "Upload any file from your device.",
|
||||
"Uploading {{name}}": "Uploading {{name}}",
|
||||
"Uploading file": "Uploading file",
|
||||
@@ -400,12 +351,6 @@
|
||||
"Divider": "Divider",
|
||||
"Quote": "Quote",
|
||||
"Image": "Image",
|
||||
"Audio": "Audio",
|
||||
"Embed PDF": "Embed PDF",
|
||||
"Upload and embed a PDF file.": "Upload and embed a PDF file.",
|
||||
"Embed as PDF": "Embed as PDF",
|
||||
"Failed to load PDF": "Failed to load PDF",
|
||||
"Convert to attachment": "Convert to attachment",
|
||||
"File attachment": "File attachment",
|
||||
"Toggle block": "Toggle block",
|
||||
"Callout": "Callout",
|
||||
@@ -418,8 +363,6 @@
|
||||
"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",
|
||||
@@ -433,10 +376,6 @@
|
||||
"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",
|
||||
@@ -446,7 +385,6 @@
|
||||
"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",
|
||||
@@ -462,7 +400,6 @@
|
||||
"{{latestVersion}} is available": "{{latestVersion}} is available",
|
||||
"Default page edit mode": "Default page edit mode",
|
||||
"Choose your preferred page edit mode. Avoid accidental edits.": "Choose your preferred page edit mode. Avoid accidental edits.",
|
||||
"Choose {{format}} file": "Choose {{format}} file",
|
||||
"Reading": "Reading",
|
||||
"Delete member": "Delete member",
|
||||
"Member deleted successfully": "Member deleted successfully",
|
||||
@@ -505,9 +442,6 @@
|
||||
"Prevent members from sharing pages publicly.": "Prevent members from sharing pages publicly.",
|
||||
"Toggle public sharing": "Toggle public sharing",
|
||||
"Toggle space public sharing": "Toggle space public sharing",
|
||||
"Allow viewers to comment": "Allow viewers to comment",
|
||||
"Allow viewers to add comments on pages in this space.": "Allow viewers to add comments on pages in this space.",
|
||||
"Toggle viewer comments": "Toggle viewer comments",
|
||||
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
|
||||
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
|
||||
"Page permissions": "Page permissions",
|
||||
@@ -534,7 +468,6 @@
|
||||
"Replace (Enter)": "Replace (Enter)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)",
|
||||
"Replace all": "Replace all",
|
||||
"View all": "View all",
|
||||
"View all spaces": "View all spaces",
|
||||
"Error": "Error",
|
||||
"Failed to disable MFA": "Failed to disable MFA",
|
||||
@@ -612,15 +545,11 @@
|
||||
"Move to trash": "Move to trash",
|
||||
"Move this page to trash?": "Move this page to trash?",
|
||||
"Restore page": "Restore page",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> moved this page to Trash {{time}}.",
|
||||
"Page moved to trash": "Page moved to trash",
|
||||
"Page restored successfully": "Page restored successfully",
|
||||
"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",
|
||||
@@ -659,21 +588,25 @@
|
||||
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
|
||||
"Image removed successfully": "Image removed successfully",
|
||||
"API key": "API key",
|
||||
"API key created successfully": "API key created successfully",
|
||||
"API keys": "API keys",
|
||||
"API management": "API management",
|
||||
"Are you sure you want to revoke this API key": "Are you sure you want to revoke this API key",
|
||||
"Create API Key": "Create API Key",
|
||||
"Custom expiration date": "Custom expiration date",
|
||||
"Enter a descriptive token name": "Enter a descriptive token name",
|
||||
"Expiration": "Expiration",
|
||||
"Expired": "Expired",
|
||||
"Expires": "Expires",
|
||||
"I've saved my API key": "I've saved my API key",
|
||||
"Last use": "Last Used",
|
||||
"No API keys found": "No API keys found",
|
||||
"No expiration": "No expiration",
|
||||
"Revoke API key": "Revoke API key",
|
||||
"Revoked successfully": "Revoked successfully",
|
||||
"Select expiration date": "Select expiration date",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.",
|
||||
"Update": "Update",
|
||||
"Update {{credential}}": "Update {{credential}}",
|
||||
"Update API key": "Update API key",
|
||||
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace",
|
||||
"Restrict API key creation to admins": "Restrict API key creation to admins",
|
||||
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "Only admins and owners can create new API keys. Existing member keys will continue to work.",
|
||||
@@ -684,7 +617,6 @@
|
||||
"AI Answer": "AI Answer",
|
||||
"Ask AI": "Ask AI",
|
||||
"AI is thinking...": "AI is thinking...",
|
||||
"Thinking": "Thinking",
|
||||
"Ask a question...": "Ask a question...",
|
||||
"AI Answers": "AI Answers",
|
||||
"AI-powered search (AI Answers)": "AI-powered search (AI Answers)",
|
||||
@@ -732,28 +664,6 @@
|
||||
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> mentioned you on a page",
|
||||
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> gave you edit access to a page",
|
||||
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> gave you view access to a page",
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> updated a page",
|
||||
"Watch page": "Watch page",
|
||||
"Stop watching": "Stop watching",
|
||||
"Watch space": "Watch space",
|
||||
"Stop watching space": "Stop watching space",
|
||||
"Email notifications": "Email notifications",
|
||||
"Page updates": "Page updates",
|
||||
"Get notified when pages you watch are updated.": "Receive notifications when the pages you watch are updated.",
|
||||
"Page mentions": "Page mentions",
|
||||
"Get notified when someone mentions you on a page.": "Receive notifications when someone mentions you on a page.",
|
||||
"Comment mentions": "Comment mentions",
|
||||
"Get notified when someone mentions you in a comment.": "Receive notifications when someone mentions you in a comment.",
|
||||
"New comments": "New comments",
|
||||
"Get notified about new comments on threads you participate in.": "Receive notifications about new comments in threads you are participating in.",
|
||||
"Resolved comments": "Resolved comments",
|
||||
"Get notified when your comment is resolved.": "Receive a notification when your comment is resolved.",
|
||||
"You are now watching this page": "You’re now watching this page",
|
||||
"You are no longer watching this page": "You’re no longer watching this page",
|
||||
"You are now watching this space": "You’re now watching this space",
|
||||
"You are no longer watching this space": "You’re no longer watching this space",
|
||||
"Direct": "Direct",
|
||||
"Updates": "Updates",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"This week": "This week",
|
||||
@@ -788,93 +698,6 @@
|
||||
"Removed page restriction": "Removed page restriction",
|
||||
"Added page permission": "Added page permission",
|
||||
"Removed page permission": "Removed page permission",
|
||||
"day": "day",
|
||||
"days": "days",
|
||||
"week": "week",
|
||||
"weeks": "weeks",
|
||||
"month": "month",
|
||||
"months": "months",
|
||||
"year": "year",
|
||||
"years": "years",
|
||||
"Period": "Period",
|
||||
"Fixed date": "Fixed date",
|
||||
"Indefinitely": "Indefinitely",
|
||||
"Days": "Days",
|
||||
"Weeks": "Weeks",
|
||||
"Months": "Months",
|
||||
"Years": "Years",
|
||||
"Pick a date": "Pick a date",
|
||||
"Maximum is {{max}} {{unit}} for this unit": "Maximum is {{max}} {{unit}} for this unit",
|
||||
"Never expires. Verifiers can re-verify at any time.": "Never expires. Verifiers can re-verify at any time.",
|
||||
"Verified": "Verified",
|
||||
"Review needed": "Review needed",
|
||||
"Verification expired": "Verification expired",
|
||||
"Draft": "Draft",
|
||||
"In Approval": "In Approval",
|
||||
"In approval": "In approval",
|
||||
"Approved": "Approved",
|
||||
"Obsolete": "Obsolete",
|
||||
"Expiring": "Expiring",
|
||||
"Set up verification": "Set up verification",
|
||||
"Verify page": "Verify page",
|
||||
"Page verification": "Page verification",
|
||||
"Add verification": "Add verification",
|
||||
"Edit verification": "Edit verification",
|
||||
"Search by title": "Search by title",
|
||||
"Choose how this page should stay accurate.": "Choose how this page should stay accurate.",
|
||||
"Recurring verification": "Recurring verification",
|
||||
"Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.",
|
||||
"Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g every 30 days )",
|
||||
"Page stays editable at all times": "Page stays editable at all times",
|
||||
"Best for runbooks, FAQs, living documentation": "Best for runbooks, FAQs, living documentation",
|
||||
"Approval workflow": "Approval workflow",
|
||||
"Formal document lifecycle with named approvers.": "Formal document lifecycle with named approvers.",
|
||||
"Draft → In approval → Approved → Obsolete": "Draft → In approval → Approved → Obsolete",
|
||||
"Locked once approved, with full history": "Locked once approved, with full history",
|
||||
"Designed for ISO 9001, ISO 13485, and FDA": "Designed for ISO 9001, ISO 13485, and FDA",
|
||||
"Best for SOPs and controlled documents": "Best for SOPs and controlled documents",
|
||||
"Back": "Back",
|
||||
"Quality management": "Quality management",
|
||||
"Recurring": "Recurring",
|
||||
"Pages move through draft, approval, and approved stages.": "Pages move through draft, approval, and approved stages.",
|
||||
"Verifiers": "Verifiers",
|
||||
"Add verifier": "Add verifier",
|
||||
"I've reviewed this page for accuracy": "I've reviewed this page for accuracy",
|
||||
"Set up": "Set up",
|
||||
"Remove verification": "Remove verification",
|
||||
"Are you sure you want to remove verification from this page?": "Are you sure you want to remove verification from this page?",
|
||||
"Assigned verifiers must periodically re-verify this page.": "Assigned verifiers must periodically re-verify this page.",
|
||||
"Last verified by {{name}} {{time}} (expired)": "Last verified by {{name}} {{time}} (expired)",
|
||||
"The fixed expiration date has passed.": "The fixed expiration date has passed.",
|
||||
"Verified by {{name}} {{time}}": "Verified by {{name}} {{time}}",
|
||||
"Expires {{date}}": "Expires {{date}}",
|
||||
"Expired {{date}}": "Expired {{date}}",
|
||||
"Mark as obsolete": "Mark as obsolete",
|
||||
"Mark obsolete": "Mark obsolete",
|
||||
"Returned by {{name}} {{time}}": "Returned by {{name}} {{time}}",
|
||||
"No approval has been requested yet.": "No approval has been requested yet.",
|
||||
"Submitted by {{name}} {{time}}": "Submitted by {{name}} {{time}}",
|
||||
"Someone": "Someone",
|
||||
"Approved by {{name}} {{time}}": "Approved by {{name}} {{time}}",
|
||||
"This document has been marked as obsolete.": "This document has been marked as obsolete.",
|
||||
"Rejection comment": "Rejection comment",
|
||||
"Reason for returning this document...": "Reason for returning this document...",
|
||||
"Confirm rejection": "Confirm rejection",
|
||||
"Submit for approval": "Submit for approval",
|
||||
"Reject": "Reject",
|
||||
"Approve": "Approve",
|
||||
"Re-submit for approval": "Re-submit for approval",
|
||||
"Verified until": "Verified until",
|
||||
"QMS": "QMS",
|
||||
"Verified pages": "Verified pages",
|
||||
"Search pages...": "Search pages...",
|
||||
"Filter by space": "Filter by space",
|
||||
"Filter by type": "Filter by type",
|
||||
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> verified a page",
|
||||
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> submitted a page for your approval",
|
||||
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> returned a page for revision",
|
||||
"Page verification expires soon": "Page verification expires soon",
|
||||
"Page verification has expired": "Page verification has expired",
|
||||
"Verifying your email": "Verifying your email",
|
||||
"Please wait...": "Please wait...",
|
||||
"Verification failed. The link may have expired.": "Verification failed. The link may have expired.",
|
||||
@@ -885,273 +708,5 @@
|
||||
"Resend verification email": "Resend verification email",
|
||||
"Verification email sent. Please check your inbox.": "Verification email sent. Please check your inbox.",
|
||||
"Failed to resend verification email. Please try again.": "Failed to resend verification email. Please try again.",
|
||||
"We've sent you an email with your associated workspaces.": "We've sent you an email with your associated workspaces.",
|
||||
"Load more": "Load more",
|
||||
"Log out of all devices": "Log out of all devices",
|
||||
"Log out of all sessions except this device": "Log out of all sessions except this device",
|
||||
"This Device": "This Device",
|
||||
"Unknown device": "Unknown device",
|
||||
"No active sessions": "No active sessions",
|
||||
"Session revoked": "Session revoked",
|
||||
"All other sessions revoked": "All other sessions revoked",
|
||||
"Last used": "Last used",
|
||||
"Created": "Created",
|
||||
"Rename": "Rename",
|
||||
"Publish": "Publish",
|
||||
"Security": "Security",
|
||||
"Enforce SSO": "Enforce SSO",
|
||||
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password.",
|
||||
"AI-generated content may not be accurate.": "AI-generated content may not be accurate.",
|
||||
"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.",
|
||||
"How can I help you today?": "How can I help you today?",
|
||||
"New chat": "New chat",
|
||||
"No chat history": "No chat history",
|
||||
"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...",
|
||||
"Search chats": "Search chats",
|
||||
"Ask anything... Use @ to mention pages": "Ask anything... Use @ to mention pages",
|
||||
"Ask anything or search your workspace": "Ask anything or search your workspace",
|
||||
"Welcome to {{name}}": "Welcome to {{name}}",
|
||||
"Add files": "Add files",
|
||||
"Mention a page": "Mention a page",
|
||||
"Start a new chat to see it here.": "Start a new chat to see it here.",
|
||||
"Summarize this page": "Summarize this page",
|
||||
"Toggle AI Chat": "Toggle AI Chat",
|
||||
"Translate this page": "Translate this page",
|
||||
"Try a different search term.": "Try a different search term.",
|
||||
"Try again": "Try again",
|
||||
"Untitled chat": "Untitled chat",
|
||||
"What can I help you with?": "What can I help you with?",
|
||||
"Are you sure you want to revoke this {{credential}}": "Are you sure you want to revoke this {{credential}}",
|
||||
"Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.",
|
||||
"Configure your identity provider with this URL to provision users and groups.": "Configure your identity provider with this URL to provision users and groups.",
|
||||
"Create {{credential}}": "Create {{credential}}",
|
||||
"{{credential}} created": "{{credential}} created",
|
||||
"{{credential}} created successfully": "{{credential}} created successfully",
|
||||
"Created by": "Created by",
|
||||
"Custom": "Custom",
|
||||
"Enable SCIM": "Enable SCIM",
|
||||
"Enter a descriptive name": "Enter a descriptive name",
|
||||
"I've saved my {{credential}}": "I've saved my {{credential}}",
|
||||
"Important": "Important",
|
||||
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Make sure to copy your {{credential}} now. You won't be able to see it again!",
|
||||
"Never": "Never",
|
||||
"Revoke {{credential}}": "Revoke {{credential}}",
|
||||
"SCIM endpoint URL": "SCIM endpoint URL",
|
||||
"SCIM provisioning": "SCIM provisioning",
|
||||
"SCIM takes precedence over SSO group sync while enabled.": "SCIM takes precedence over SSO group sync while enabled.",
|
||||
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.",
|
||||
"SCIM token": "SCIM token",
|
||||
"SCIM tokens": "SCIM tokens",
|
||||
"This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.",
|
||||
"Toggle SCIM provisioning": "Toggle SCIM provisioning",
|
||||
"Token": "Token",
|
||||
"Page menu": "Page menu",
|
||||
"Expand": "Expand",
|
||||
"Collapse": "Collapse",
|
||||
"Comment menu": "Comment menu",
|
||||
"Group menu": "Group menu",
|
||||
"Show hidden breadcrumbs": "Show hidden breadcrumbs",
|
||||
"Breadcrumbs": "Breadcrumbs",
|
||||
"Page actions": "Page actions",
|
||||
"Pick emoji": "Pick emoji",
|
||||
"Template menu": "Template 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",
|
||||
"Only workspace admins can create global templates.": "Only workspace admins can create global templates.",
|
||||
"You need edit access to this space to create templates.": "You need edit access to this space to create templates.",
|
||||
"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",
|
||||
"Mention suggestions": "Mention suggestions",
|
||||
"Link suggestions": "Link suggestions",
|
||||
"Diagram editor": "Diagram editor",
|
||||
"Add comment": "Add comment",
|
||||
"Find and replace": "Find and replace",
|
||||
"Main navigation": "Main navigation",
|
||||
"Space navigation": "Space navigation",
|
||||
"Settings navigation": "Settings navigation",
|
||||
"AI navigation": "AI navigation",
|
||||
"Breadcrumb": "Breadcrumb",
|
||||
"Synced block": "Synced block",
|
||||
"Create a block that stays in sync across pages.": "Create a block that stays in sync across pages.",
|
||||
"Editing original": "Editing original",
|
||||
"Copy synced block": "Copy synced block",
|
||||
"Unsync": "Unsync",
|
||||
"Delete synced block": "Delete synced block",
|
||||
"Synced to {{count}} other page_one": "Synced to {{count}} other page",
|
||||
"Synced to {{count}} other page_other": "Synced to {{count}} other pages",
|
||||
"ORIGINAL": "ORIGINAL",
|
||||
"THIS PAGE": "THIS PAGE",
|
||||
"No pages": "No pages",
|
||||
"The original synced block no longer exists": "The original synced block no longer exists",
|
||||
"You don't have access to this synced block": "You don't have access to this synced block",
|
||||
"Failed to load this synced block": "Failed to load this synced block",
|
||||
"Fixed editor toolbar": "Fixed editor toolbar",
|
||||
"Show a formatting toolbar above the editor with quick access to common actions.": "Show a formatting toolbar above the editor with quick access to common actions.",
|
||||
"Toggle fixed editor toolbar": "Toggle fixed editor toolbar",
|
||||
"Normal text": "Normal text",
|
||||
"More inline formatting": "More inline formatting",
|
||||
"Subscript": "Subscript",
|
||||
"Superscript": "Superscript",
|
||||
"Inline code": "Inline code",
|
||||
"Insert media": "Insert media",
|
||||
"Mention": "Mention",
|
||||
"Emoji": "Emoji",
|
||||
"Columns": "Columns",
|
||||
"More inserts": "More inserts",
|
||||
"Embeds": "Embeds",
|
||||
"Diagrams": "Diagrams",
|
||||
"Advanced": "Advanced",
|
||||
"Utility": "Utility",
|
||||
"Decrease indent": "Decrease indent",
|
||||
"Increase indent": "Increase indent",
|
||||
"Clear formatting": "Clear formatting",
|
||||
"Code block": "Code block",
|
||||
"Experimental": "Experimental",
|
||||
"Strikethrough": "Strikethrough",
|
||||
"Undo": "Undo",
|
||||
"Redo": "Redo",
|
||||
"Backlinks": "Backlinks",
|
||||
"Last updated by": "Last updated by",
|
||||
"Last updated": "Last updated",
|
||||
"Stats": "Stats",
|
||||
"Word count": "Word count",
|
||||
"Characters": "Characters",
|
||||
"Incoming links": "Incoming links",
|
||||
"Outgoing links": "Outgoing links",
|
||||
"Incoming links ({{count}})": "Incoming links ({{count}})",
|
||||
"Outgoing links ({{count}})": "Outgoing links ({{count}})",
|
||||
"No pages link here yet.": "No pages link here yet.",
|
||||
"This page doesn't link to other pages yet.": "This page doesn't link to other pages yet.",
|
||||
"Verified until {{date}}": "Verified until {{date}}",
|
||||
"Labels": "Labels",
|
||||
"Add label": "Add label",
|
||||
"No labels yet": "No labels yet",
|
||||
"Already added": "Already added",
|
||||
"Invalid label name": "Invalid label name",
|
||||
"No matches": "No matches",
|
||||
"Search or create…": "Search or create…",
|
||||
"Remove label {{name}}": "Remove label {{name}}",
|
||||
"Failed to add label": "Failed to add label",
|
||||
"Failed to remove label": "Failed to remove label",
|
||||
"No pages with this label": "No pages with this label",
|
||||
"Pages tagged with this label will appear here.": "Pages tagged with this label will appear here.",
|
||||
"No pages match your search.": "No pages match your search.",
|
||||
"Updated {{date}}": "Updated {{date}}",
|
||||
"Cell actions": "Cell actions",
|
||||
"Column actions": "Column 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"
|
||||
"We've sent you an email with your associated workspaces.": "We've sent you an email with your associated workspaces."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@
|
||||
"Add members": "Aggiungi membri",
|
||||
"Add to groups": "Aggiungi ai gruppi",
|
||||
"Add space members": "Aggiungi membri allo spazio",
|
||||
"Add to favorites": "Aggiungi ai preferiti",
|
||||
"Admin": "Amministratore",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?",
|
||||
@@ -41,8 +40,6 @@
|
||||
"Dark": "Scuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Elimina",
|
||||
"Remove from page": "Rimuovi dalla pagina",
|
||||
"Base options": "Opzioni della base",
|
||||
"Delete group": "Elimina gruppo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sei sicuro di voler eliminare questa pagina? Verranno cancellate anche le sue sottopagine e la cronologia. Questa azione è irreversibile.",
|
||||
"Description": "Descrizione",
|
||||
@@ -50,19 +47,19 @@
|
||||
"e.g ACME": "es. ACME",
|
||||
"e.g ACME Inc": "es. ACME Inc",
|
||||
"e.g Developers": "es. Sviluppatori",
|
||||
"e.g Group for developers": "es. Gruppo per sviluppatori",
|
||||
"e.g Group for developers": "es. Gruppo per gli sviluppatori",
|
||||
"e.g product": "es. prodotto",
|
||||
"e.g Product Team": "es. Team di prodotto",
|
||||
"e.g Product Team": "es. Team di Prodotto",
|
||||
"e.g Sales": "es. Vendite",
|
||||
"e.g Space for product team": "es. Spazio per il team di prodotto",
|
||||
"e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team vendite",
|
||||
"e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team di vendita",
|
||||
"Edit": "Modifica",
|
||||
"Read": "Leggi",
|
||||
"Edit group": "Modifica gruppo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Inserisci una password sicura",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Inserisci degli indirizzi email validi separati da virgola o spazio [max: 50]",
|
||||
"enter valid emails addresses": "inserisci indirizzi email validi",
|
||||
"enter valid emails addresses": "inserisci degli indirizzi email validi",
|
||||
"Enter your current password": "Inserisci la tua password attuale",
|
||||
"enter your full name": "inserisci il tuo nome completo",
|
||||
"Enter your new password": "Inserisci la tua nuova password",
|
||||
@@ -73,32 +70,10 @@
|
||||
"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.",
|
||||
"Failed to update data": "Impossibile aggiornare i dati",
|
||||
"Failed to create base": "Impossibile creare la base",
|
||||
"Failed to update base": "Impossibile aggiornare la base",
|
||||
"Failed to delete base": "Impossibile eliminare la base",
|
||||
"Failed to create property": "Impossibile creare la proprietà",
|
||||
"Failed to update property": "Impossibile aggiornare la proprietà",
|
||||
"Failed to delete property": "Impossibile eliminare la proprietà",
|
||||
"Failed to reorder property": "Impossibile riordinare la proprietà",
|
||||
"Failed to create view": "Impossibile creare la vista",
|
||||
"Failed to update view": "Impossibile aggiornare la vista",
|
||||
"Failed to delete view": "Impossibile eliminare la vista",
|
||||
"Failed to create row": "Impossibile creare la riga",
|
||||
"Failed to update row": "Impossibile aggiornare la riga",
|
||||
"Failed to delete row": "Impossibile eliminare la riga",
|
||||
"Failed to delete rows": "Impossibile eliminare le righe",
|
||||
"Failed to reorder row": "Impossibile riordinare la riga",
|
||||
"Failed to move card": "Impossibile spostare la scheda",
|
||||
"Failed to add card": "Impossibile aggiungere la scheda",
|
||||
"Failed to export CSV": "Impossibile esportare il CSV",
|
||||
"Favorite spaces": "Spazi preferiti",
|
||||
"Favorite spaces appear here": "Gli spazi preferiti appariranno qui",
|
||||
"Favorites": "Preferiti",
|
||||
"Full access": "Accesso completo",
|
||||
"Full page width": "Pagina a larghezza intera",
|
||||
"Full width": "Larghezza intera",
|
||||
@@ -117,7 +92,6 @@
|
||||
"Invite by email": "Invita tramite email",
|
||||
"Invite members": "Invita membri",
|
||||
"Invite new members": "Invita nuovi membri",
|
||||
"Invite People": "Invita persone",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere",
|
||||
"Join the workspace": "Unisciti all'area di lavoro",
|
||||
@@ -165,7 +139,6 @@
|
||||
"Profile": "Profilo",
|
||||
"Recently updated": "Aggiornato di recente",
|
||||
"Remove": "Rimuovi",
|
||||
"Remove from favorites": "Rimuovi dai preferiti",
|
||||
"Remove group member": "Rimuovi membro dal gruppo",
|
||||
"Remove space member": "Rimuovi membro dallo spazio",
|
||||
"Restore": "Ripristina",
|
||||
@@ -176,56 +149,55 @@
|
||||
"Search for users": "Cerca un utente",
|
||||
"Search for users and groups": "Cerca un utente o un gruppo",
|
||||
"Search...": "Cerca...",
|
||||
"Select language": "Seleziona lingua",
|
||||
"Select role": "Seleziona ruolo",
|
||||
"Select language": "Seleziona una lingua",
|
||||
"Select role": "Seleziona un ruolo",
|
||||
"Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati",
|
||||
"Select theme": "Seleziona tema",
|
||||
"Select theme": "Seleziona un tema",
|
||||
"Send invitation": "Invia invito",
|
||||
"Invitation sent": "Invito inviato",
|
||||
"Settings": "Impostazioni",
|
||||
"Setup workspace": "Configura workspace",
|
||||
"Setup workspace": "Configura l'area di lavoro",
|
||||
"Sign In": "Accedi",
|
||||
"Sign Up": "Registrati",
|
||||
"Slug": "Slug",
|
||||
"Space": "Spazio",
|
||||
"Space description": "Descrizione dello spazio",
|
||||
"Space menu": "Menu dello spazio",
|
||||
"Space menu": "Menu spazio",
|
||||
"Space name": "Nome dello spazio",
|
||||
"Space settings": "Impostazioni dello spazio",
|
||||
"Space slug": "Slug dello spazio",
|
||||
"Spaces": "Spazi",
|
||||
"Spaces you belong to": "Spazi di cui fai parte",
|
||||
"Spaces you belong to": "Spazi a cui appartieni",
|
||||
"No space found": "Nessuno spazio trovato",
|
||||
"Search for spaces": "Cerca spazi",
|
||||
"Search for spaces": "Cerca uno spazio",
|
||||
"Start typing to search...": "Inizia a digitare per cercare...",
|
||||
"Status": "Stato",
|
||||
"Successfully imported": "Importato con successo",
|
||||
"Successfully restored": "Ripristinato con successo",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"Templates": "Modelli",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
|
||||
"Toggle full page width": "Attiva/disattiva larghezza completa della pagina",
|
||||
"Toggle full page width": "Attiva/disattiva pagina a larghezza intera",
|
||||
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
|
||||
"untitled": "senza titolo",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated successfully": "Aggiornato con successo",
|
||||
"User": "Utente",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Nome del workspace",
|
||||
"Workspace settings": "Impostazioni del workspace",
|
||||
"Workspace": "Area di lavoro",
|
||||
"Workspace Name": "Nome dell'area di lavoro",
|
||||
"Workspace settings": "Impostazioni dell'area di lavoro",
|
||||
"You can change your password here.": "Qui puoi cambiare la tua password.",
|
||||
"Your Email": "La tua email",
|
||||
"Your import is complete.": "La tua importazione è completata.",
|
||||
"Your name": "Il tuo nome",
|
||||
"Your Name": "Il tuo nome",
|
||||
"Your Name": "Il Tuo Nome",
|
||||
"Your password": "La tua password",
|
||||
"Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.",
|
||||
"Sidebar toggle": "Attiva/disattiva barra laterale",
|
||||
"Comments": "Commenti",
|
||||
"404 page not found": "404 pagina non trovata",
|
||||
"Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.",
|
||||
"Take me back to homepage": "Torna alla homepage",
|
||||
"Take me back to homepage": "Torna all'homepage",
|
||||
"Forgot password": "Password dimenticata",
|
||||
"Forgot your password?": "Hai dimenticato la password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.",
|
||||
@@ -243,8 +215,6 @@
|
||||
"Edit comment": "Modifica commento",
|
||||
"Delete comment": "Elimina commento",
|
||||
"Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?",
|
||||
"Delete chat": "Elimina chat",
|
||||
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Sei sicuro di voler eliminare '{{title}}'? Questa azione non può essere annullata.",
|
||||
"Comment created successfully": "Commento creato con successo",
|
||||
"Error creating comment": "Si è verificato un errore durante la creazione del commento",
|
||||
"Comment updated successfully": "Commento aggiornato con successo",
|
||||
@@ -253,12 +223,12 @@
|
||||
"Failed to delete comment": "Impossibile eliminare il commento",
|
||||
"Comment resolved successfully": "Commento risolto con successo",
|
||||
"Comment re-opened successfully": "Commento riaperto con successo",
|
||||
"Comment unresolved successfully": "Commento contrassegnato come non risolto con successo",
|
||||
"Comment unresolved successfully": "Commento non risolto con successo",
|
||||
"Failed to resolve comment": "Impossibile risolvere il commento",
|
||||
"Resolve comment": "Risolvi commento",
|
||||
"Unresolve comment": "Segna commento come non risolto",
|
||||
"Resolve Comment Thread": "Risolvi discussione del commento",
|
||||
"Unresolve Comment Thread": "Segna discussione del commento come non risolta",
|
||||
"Unresolve comment": "Annulla risoluzione commento",
|
||||
"Resolve Comment Thread": "Risolvi discussione commenti",
|
||||
"Unresolve Comment Thread": "Annulla risoluzione discussione commenti",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Sei sicuro di voler risolvere questa discussione di commenti? Questo la contrassegnerà come completata.",
|
||||
"Are you sure you want to unresolve this comment thread?": "Sei sicuro di voler annullare la risoluzione di questa discussione di commenti?",
|
||||
"Resolved": "Risolto",
|
||||
@@ -281,7 +251,7 @@
|
||||
"Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?",
|
||||
"Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi di questo spazio verranno eliminati irreversibilmente.",
|
||||
"Confirm space name": "Conferma nome dello spazio",
|
||||
"Confirm space name": "Conferma nome spazio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Includi sottopagine",
|
||||
@@ -297,9 +267,6 @@
|
||||
"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",
|
||||
@@ -310,19 +277,6 @@
|
||||
"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",
|
||||
@@ -335,11 +289,6 @@
|
||||
"Save & Exit": "Salva ed esci",
|
||||
"Double-click to edit Excalidraw diagram": "Fai doppio clic per modificare il diagramma di Excalidraw",
|
||||
"Paste link": "Incolla link",
|
||||
"Paste link or search pages": "Incolla il link o cerca le pagine",
|
||||
"Link to web page": "Collega a una pagina web",
|
||||
"Recents": "Recenti",
|
||||
"Page or URL": "Pagina o URL",
|
||||
"Link title": "Titolo del link",
|
||||
"Edit link": "Modifica link",
|
||||
"Remove link": "Rimuovi link",
|
||||
"Add link": "Aggiungi link",
|
||||
@@ -358,7 +307,7 @@
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grigio",
|
||||
"Embed link": "Incorpora collegamento",
|
||||
"Invalid {{provider}} embed link": "Link incorporato {{provider}} non valido",
|
||||
"Invalid {{provider}} embed link": "Link di incorporamento {{provider}} non valido",
|
||||
"Embed {{provider}}": "Incorpora {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare",
|
||||
"Bold": "Grassetto",
|
||||
@@ -385,11 +334,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.",
|
||||
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
|
||||
"Uploading {{name}}": "Caricamento di {{name}}",
|
||||
"Uploading file": "Caricamento file",
|
||||
@@ -397,30 +343,22 @@
|
||||
"Insert a table.": "Inserisci una tabella.",
|
||||
"Insert collapsible block.": "Inserisci blocco comprimibile.",
|
||||
"Video": "Video",
|
||||
"Divider": "Separatore",
|
||||
"Quote": "Citazione",
|
||||
"Divider": "Divisore",
|
||||
"Quote": "Preventivo",
|
||||
"Image": "Immagine",
|
||||
"Audio": "Audio",
|
||||
"Embed PDF": "Incorpora PDF",
|
||||
"Upload and embed a PDF file.": "Carica e incorpora un file PDF.",
|
||||
"Embed as PDF": "Incorpora come PDF",
|
||||
"Failed to load PDF": "Caricamento del PDF non riuscito",
|
||||
"Convert to attachment": "Converti in allegato",
|
||||
"File attachment": "Allegato file",
|
||||
"Toggle block": "Blocco a comparsa",
|
||||
"Callout": "Riquadro evidenziato",
|
||||
"Toggle block": "Attiva blocco",
|
||||
"Callout": "Avviso",
|
||||
"Insert callout notice.": "Inserisci avviso di richiamo.",
|
||||
"Math inline": "Formula matematica in linea",
|
||||
"Math inline": "Matematica in linea",
|
||||
"Insert inline math equation.": "Inserisci equazione matematica in linea.",
|
||||
"Math block": "Blocco matematico",
|
||||
"Insert math equation": "Inserisci equazione matematica",
|
||||
"Mermaid diagram": "Diagramma Mermaid",
|
||||
"Insert mermaid diagram": "Inserisci diagramma Mermaid",
|
||||
"Mermaid diagram": "Diagramma di Mermaid",
|
||||
"Insert mermaid diagram": "Inserisci un diagramma di Mermaid",
|
||||
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
|
||||
"Insert current date": "Inserisci data corrente",
|
||||
"Time": "Ora",
|
||||
"Insert current time": "Inserisci ora corrente",
|
||||
"Draw and sketch excalidraw diagrams": "Disegna e abbozza diagrammi Excalidraw",
|
||||
"Insert current date": "Inserisci la data corrente",
|
||||
"Draw and sketch excalidraw diagrams": "Disegna e schizza diagrammi excalidraw",
|
||||
"Multiple": "Multiplo",
|
||||
"Turn into": "Trasforma in",
|
||||
"Text align": "Allinea testo",
|
||||
@@ -429,14 +367,10 @@
|
||||
"Pages you create will show up here.": "Le pagine che crei appariranno qui.",
|
||||
"Heading {{level}}": "Intestazione {{level}}",
|
||||
"Toggle title": "Attiva/disattiva titolo",
|
||||
"Write anything. Enter \"/\" for commands": "Scrivi qualsiasi cosa. Digita \"/\" per i comandi",
|
||||
"Write anything. Enter \"/\" for commands": "Scrivi qualcosa. Digita \"/\" per i comandi",
|
||||
"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",
|
||||
@@ -446,7 +380,6 @@
|
||||
"Names do not match": "I nomi non corrispondono",
|
||||
"Today, {{time}}": "Oggi, {{time}}",
|
||||
"Yesterday, {{time}}": "Ieri, {{time}}",
|
||||
"now": "ora",
|
||||
"Space created successfully": "Spazio creato con successo",
|
||||
"Space updated successfully": "Spazio aggiornato con successo",
|
||||
"Space deleted successfully": "Spazio eliminato con successo",
|
||||
@@ -455,14 +388,13 @@
|
||||
"Member role updated successfully": "Ruolo del membro aggiornato con successo",
|
||||
"Created by: <b>{{creatorName}}</b>": "Creato da: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Creato il: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Modificato da {{name}} {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Modificato da {{name}} il {{time}}",
|
||||
"Word count: {{wordCount}}": "Conteggio parole: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Conteggio caratteri: {{characterCount}}",
|
||||
"New update": "Nuovo aggiornamento",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} è disponibile",
|
||||
"Default page edit mode": "Modalità di modifica predefinita della pagina",
|
||||
"Default page edit mode": "Modalità di modifica pagina predefinita",
|
||||
"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",
|
||||
@@ -478,38 +410,36 @@
|
||||
"Move page": "Sposta pagina",
|
||||
"Move page to a different space.": "Sposta la pagina in un altro spazio.",
|
||||
"Real-time editor connection lost. Retrying...": "Connessione all'editor in tempo reale persa. Riprovo...",
|
||||
"Table of contents": "Indice",
|
||||
"Table of contents": "Indice dei contenuti",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "Aggiungi intestazioni (H1, H2, H3) per generare un sommario.",
|
||||
"Share": "Condividi",
|
||||
"Public sharing": "Condivisione pubblica",
|
||||
"Shared by": "Condiviso da",
|
||||
"Shared at": "Condiviso il",
|
||||
"Inherits public sharing from": "Eredita la condivisione pubblica da",
|
||||
"Share to web": "Condividi sul web",
|
||||
"Shared to web": "Condiviso sul web",
|
||||
"Share to web": "Condividi su web",
|
||||
"Shared to web": "Condiviso su web",
|
||||
"Anyone with the link can view this page": "Chiunque abbia il link può visualizzare questa pagina",
|
||||
"Make this page publicly accessible": "Rendi questa pagina accessibile pubblicamente",
|
||||
"Include sub-pages": "Includi sottopagine",
|
||||
"Make sub-pages public too": "Rendi pubbliche anche le sottopagine",
|
||||
"Allow search engines to index page": "Consenti ai motori di ricerca di indicizzare la pagina",
|
||||
"Include sub-pages": "Includi sotto-pagine",
|
||||
"Make sub-pages public too": "Rendi pubbliche anche le sotto-pagine",
|
||||
"Allow search engines to index page": "Permetti ai motori di ricerca di indicizzare la pagina",
|
||||
"Open page": "Apri pagina",
|
||||
"Page": "Pagina",
|
||||
"Delete public share link": "Elimina link di condivisione pubblica",
|
||||
"Delete public share link": "Elimina il link di condivisione pubblica",
|
||||
"Delete share": "Elimina condivisione",
|
||||
"Are you sure you want to delete this shared link?": "Sei sicuro di voler eliminare questo link condiviso?",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Le pagine condivise pubblicamente degli spazi di cui fai parte appariranno qui",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Le pagine condivise pubblicamente dagli spazi di cui sei membro appariranno qui",
|
||||
"Share deleted successfully": "Condivisione eliminata con successo",
|
||||
"Share not found": "Condivisione non trovata",
|
||||
"Failed to share page": "Condivisione della pagina non riuscita",
|
||||
"Failed to share page": "Condivisione della pagina fallita",
|
||||
"Disable public sharing": "Disabilita la condivisione pubblica",
|
||||
"Prevent members from sharing pages publicly.": "Impedisci ai membri di condividere pubblicamente le pagine.",
|
||||
"Toggle public sharing": "Attiva/disattiva la condivisione pubblica",
|
||||
"Toggle space public sharing": "Attiva/disattiva la condivisione pubblica nello spazio",
|
||||
"Allow viewers to comment": "Consenti agli utenti di commentare",
|
||||
"Allow viewers to add comments on pages in this space.": "Consenti agli utenti di aggiungere commenti alle pagine in questo spazio.",
|
||||
"Toggle viewer comments": "Attiva/disattiva i commenti degli utenti",
|
||||
"Public sharing is disabled at the workspace level": "La condivisione pubblica è disabilitata a livello di area di lavoro",
|
||||
"Prevent pages in this space from being shared publicly.": "Impedisci che le pagine in questo spazio vengano condivise pubblicamente.",
|
||||
"Requires an enterprise license": "Richiede una licenza enterprise",
|
||||
"Page permissions": "Autorizzazioni della pagina.",
|
||||
"Control who can view and edit individual pages. Available with an enterprise license.": "Controlla chi può visualizzare e modificare le singole pagine. Disponibile con una licenza Enterprise.",
|
||||
"Enable public sharing": "Abilita la condivisione pubblica",
|
||||
@@ -526,32 +456,31 @@
|
||||
"Page duplicated successfully": "Pagina duplicata con successo",
|
||||
"Find": "Trova",
|
||||
"Not found": "Non trovato",
|
||||
"Previous Match (Shift+Enter)": "Corrispondenza precedente (Maiusc+Invio)",
|
||||
"Previous Match (Shift+Enter)": "Corrispondenza precedente (Shift+Invio)",
|
||||
"Next match (Enter)": "Corrispondenza successiva (Invio)",
|
||||
"Match case (Alt+C)": "Distingui maiuscole/minuscole (Alt+C)",
|
||||
"Match case (Alt+C)": "Maiuscole/minuscole (Alt+C)",
|
||||
"Replace": "Sostituisci",
|
||||
"Close (Escape)": "Chiudi (Esc)",
|
||||
"Replace (Enter)": "Sostituisci (Invio)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Sostituisci tutto (Ctrl+Alt+Invio)",
|
||||
"Replace all": "Sostituisci tutto",
|
||||
"View all": "Visualizza tutto",
|
||||
"View all spaces": "Visualizza tutti gli spazi",
|
||||
"Error": "Errore",
|
||||
"Failed to disable MFA": "Disattivazione MFA non riuscita",
|
||||
"Disable two-factor authentication": "Disattiva autenticazione a due fattori",
|
||||
"Failed to disable MFA": "Disabilitazione MFA non riuscita",
|
||||
"Disable two-factor authentication": "Disabilita autenticazione a due fattori",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Disabilitare l'autenticazione a due fattori renderà il tuo account meno sicuro. Avrai bisogno solo della tua password per accedere.",
|
||||
"Please enter your password to disable two-factor authentication:": "Inserisci la tua password per disabilitare l'autenticazione a due fattori:",
|
||||
"Two-factor authentication has been enabled": "L'autenticazione a due fattori è stata abilitata",
|
||||
"Two-factor authentication has been disabled": "L'autenticazione a due fattori è stata disabilitata",
|
||||
"Two-factor authentication has been enabled": "Autenticazione a due fattori abilitata",
|
||||
"Two-factor authentication has been disabled": "Autenticazione a due fattori disabilitata",
|
||||
"2-step verification": "Verifica in 2 passaggi",
|
||||
"Protect your account with an additional verification layer when signing in.": "Proteggi il tuo account con un ulteriore livello di verifica durante l'accesso.",
|
||||
"Two-factor authentication is active on your account.": "L'autenticazione a due fattori è attiva sul tuo account.",
|
||||
"Add 2FA method": "Aggiungi metodo 2FA",
|
||||
"Backup codes": "Codici di backup",
|
||||
"Disable": "Disattiva",
|
||||
"Disable": "Disabilita",
|
||||
"Invalid verification code": "Codice di verifica non valido",
|
||||
"New backup codes have been generated": "Sono stati generati nuovi codici di backup",
|
||||
"Failed to regenerate backup codes": "Rigenerazione dei codici di backup non riuscita",
|
||||
"New backup codes have been generated": "Nuovi codici di backup generati",
|
||||
"Failed to regenerate backup codes": "Rigenerazione codici di backup non riuscita",
|
||||
"About backup codes": "Informazioni sui codici di backup",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "I codici di backup possono essere utilizzati per accedere al tuo account se perdi l'accesso alla tua app di autenticazione. Ogni codice può essere usato solo una volta.",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Puoi rigenerare nuovi codici di backup in qualsiasi momento. Questo invaliderà tutti i codici esistenti.",
|
||||
@@ -561,9 +490,9 @@
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Assicurati di salvare questi codici in un luogo sicuro. I tuoi vecchi codici di backup non sono più validi.",
|
||||
"Your new backup codes": "I tuoi nuovi codici di backup",
|
||||
"I've saved my backup codes": "Ho salvato i miei codici di backup",
|
||||
"Failed to setup MFA": "Configurazione MFA non riuscita",
|
||||
"Setup & Verify": "Configura e verifica",
|
||||
"Add to authenticator": "Aggiungi all'autenticatore",
|
||||
"Failed to setup MFA": "Impostazione MFA non riuscita",
|
||||
"Setup & Verify": "Imposta e Verifica",
|
||||
"Add to authenticator": "Aggiungi ad authenticator",
|
||||
"1. Scan this QR code with your authenticator app": "1. Scansiona questo codice QR con la tua app di autenticazione",
|
||||
"Can't scan the code?": "Non riesci a scansionare il codice?",
|
||||
"Enter this code manually in your authenticator app:": "Inserisci questo codice manualmente nella tua app di autenticazione:",
|
||||
@@ -577,17 +506,17 @@
|
||||
"Print": "Stampa",
|
||||
"Two-factor authentication has been set up. Please log in again.": "L'autenticazione a due fattori è stata impostata. Effettua nuovamente l'accesso, per favore.",
|
||||
"Two-Factor authentication required": "Autenticazione a due fattori richiesta",
|
||||
"Your workspace requires two-factor authentication for all users": "Il tuo workspace richiede l'autenticazione a due fattori per tutti gli utenti",
|
||||
"Your workspace requires two-factor authentication for all users": "Il tuo spazio di lavoro richiede l'autenticazione a due fattori per tutti gli utenti",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Per continuare ad accedere al tuo spazio di lavoro, devi impostare l'autenticazione a due fattori. Questo aggiunge un ulteriore livello di sicurezza al tuo account.",
|
||||
"Set up two-factor authentication": "Configura l'autenticazione a due fattori",
|
||||
"Cancel and logout": "Annulla e disconnettiti",
|
||||
"Set up two-factor authentication": "Imposta l'autenticazione a due fattori",
|
||||
"Cancel and logout": "Annulla e disconnetti",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "Il tuo spazio di lavoro richiede l'autenticazione a due fattori. Impostala per continuare.",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Questo aggiunge un ulteriore livello di sicurezza al tuo account richiedendo un codice di verifica dalla tua app di autenticazione.",
|
||||
"Password is required": "La password è obbligatoria",
|
||||
"Password must be at least 8 characters": "La password deve contenere almeno 8 caratteri",
|
||||
"Please enter a 6-digit code": "Inserisci un codice di 6 cifre",
|
||||
"Password is required": "La password è richiesta",
|
||||
"Password must be at least 8 characters": "La password deve essere di almeno 8 caratteri",
|
||||
"Please enter a 6-digit code": "Inserisci un codice a 6 cifre",
|
||||
"Code must be exactly 6 digits": "Il codice deve essere esattamente di 6 cifre",
|
||||
"Enter the 6-digit code found in your authenticator app": "Inserisci il codice di 6 cifre presente nella tua app di autenticazione",
|
||||
"Enter the 6-digit code found in your authenticator app": "Inserisci il codice a 6 cifre trovato nella tua app di autenticazione",
|
||||
"Need help authenticating?": "Hai bisogno di aiuto per autenticarti?",
|
||||
"MFA QR Code": "Codice QR MFA",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "Account creato con successo. Effettua l'accesso per impostare l'autenticazione a due fattori.",
|
||||
@@ -595,7 +524,7 @@
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Reimpostazione della password riuscita. Accedi con la tua nuova password per impostare l'autenticazione a due fattori.",
|
||||
"Password reset was successful. Please log in with your new password.": "Reimpostazione della password riuscita. Accedi con la tua nuova password.",
|
||||
"Two-factor authentication": "Autenticazione a due fattori",
|
||||
"Use authenticator app instead": "Usa invece l'app di autenticazione",
|
||||
"Use authenticator app instead": "Usa l'app di autenticazione invece",
|
||||
"Verify backup code": "Verifica codice di backup",
|
||||
"Use backup code": "Usa codice di backup",
|
||||
"Enter one of your backup codes": "Inserisci uno dei tuoi codici di backup",
|
||||
@@ -612,15 +541,11 @@
|
||||
"Move to trash": "Sposta nel cestino",
|
||||
"Move this page to trash?": "Spostare questa pagina nel cestino?",
|
||||
"Restore page": "Ripristina pagina",
|
||||
"Permanently delete": "Elimina definitivamente",
|
||||
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> 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",
|
||||
"Deleted at": "Eliminato il",
|
||||
"Preview": "Anteprima",
|
||||
"Base preview unavailable": "Anteprima della base non disponibile",
|
||||
"Restore this base to view its contents.": "Ripristina questa base per visualizzarne i contenuti.",
|
||||
"Subpages": "Sottopagine",
|
||||
"Failed to load subpages": "Caricamento delle sottopagine non riuscito",
|
||||
"No subpages": "Nessuna sottopagina",
|
||||
@@ -632,20 +557,20 @@
|
||||
"Find a space": "Trova uno spazio",
|
||||
"Search in all your spaces": "Cerca in tutti i tuoi spazi",
|
||||
"Type": "Tipo",
|
||||
"Enterprise": "Enterprise",
|
||||
"Enterprise": "Impresa",
|
||||
"Download attachment": "Scarica allegato",
|
||||
"Allowed email domains": "Domini email consentiti",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Solo gli utenti con indirizzi email di questi domini possono registrarsi tramite SSO.",
|
||||
"Enter valid domain names separated by comma or space": "Inserisci nomi di dominio validi separati da virgola o spazio",
|
||||
"Enforce two-factor authentication": "Rendi obbligatoria l'autenticazione a due fattori",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Solo gli utenti con indirizzi email provenienti da questi domini possono registrarsi tramite SSO.",
|
||||
"Enter valid domain names separated by comma or space": "Inserisci nomi di dominio validi separati da virgole o spazi",
|
||||
"Enforce two-factor authentication": "Imponi l'autenticazione a due fattori",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Una volta impostata, tutti i membri devono abilitare l'autenticazione a due fattori per accedere all'area di lavoro.",
|
||||
"Toggle MFA enforcement": "Attiva/disattiva obbligatorietà MFA",
|
||||
"Toggle MFA enforcement": "Attiva disattiva l'applicazione MFA",
|
||||
"Display name": "Nome visualizzato",
|
||||
"Allow signup": "Consenti registrazione",
|
||||
"Allow signup": "Consenti iscrizione",
|
||||
"Enabled": "Abilitato",
|
||||
"Advanced Settings": "Impostazioni avanzate",
|
||||
"Enable TLS/SSL": "Abilita TLS/SSL",
|
||||
"Use secure connection to LDAP server": "Usa una connessione sicura al server LDAP",
|
||||
"Use secure connection to LDAP server": "Usa connessione sicura al server LDAP",
|
||||
"Group sync": "Sincronizzazione gruppi",
|
||||
"No SSO providers found.": "Nessun provider SSO trovato.",
|
||||
"Delete SSO provider": "Elimina provider SSO",
|
||||
@@ -659,21 +584,25 @@
|
||||
"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": "Aggiorna",
|
||||
"Update {{credential}}": "Aggiorna {{credential}}",
|
||||
"Update API key": "Aggiorna chiave API",
|
||||
"Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro",
|
||||
"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.",
|
||||
@@ -684,7 +613,6 @@
|
||||
"AI Answer": "Risposta AI",
|
||||
"Ask AI": "Chiedi all'AI",
|
||||
"AI is thinking...": "L'AI sta pensando...",
|
||||
"Thinking": "Sto pensando",
|
||||
"Ask a question...": "Fai una domanda...",
|
||||
"AI Answers": "Risposte AI",
|
||||
"AI-powered search (AI Answers)": "Ricerca con AI (Risposte AI)",
|
||||
@@ -693,9 +621,7 @@
|
||||
"Generative AI (Ask AI)": "AI generativa (Chiedi AI)",
|
||||
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Abilita la generazione di contenuti con AI nell'editor. Consente agli utenti di generare, migliorare, tradurre e trasformare il testo.",
|
||||
"Toggle generative AI": "Attiva/Disattiva AI generativa",
|
||||
"Upgrade your plan": "Aggiorna il tuo piano",
|
||||
"Available with a paid license": "Disponibile con una licenza a pagamento",
|
||||
"Upgrade your license tier.": "Aggiorna il livello della tua licenza.",
|
||||
"Enterprise feature": "Funzionalità Enterprise",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
|
||||
"AI & MCP": "IA e MCP",
|
||||
"AI": "IA",
|
||||
@@ -703,15 +629,17 @@
|
||||
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
|
||||
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Abilita il server MCP per consentire ad assistenti e strumenti IA di interagire con i contenuti del tuo spazio di lavoro.",
|
||||
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
|
||||
"MCP documentation": "Documentazione MCP",
|
||||
"MCP Server URL": "URL del server MCP",
|
||||
"Use your API key for authentication. You can manage API keys in your account settings.": "Usa la tua chiave API per l'autenticazione. Puoi gestire le chiavi API nelle impostazioni del tuo account.",
|
||||
"Supported tools": "Strumenti supportati",
|
||||
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Il tuo spazio di lavoro ha MCP abilitato. Usa la tua chiave API per collegare gli assistenti IA.",
|
||||
"MCP server URL:": "URL del server MCP:",
|
||||
"Learn more": "Scopri di più",
|
||||
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestisci le API key per tutti gli utenti nello spazio di lavoro. Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
|
||||
"View the <anchor>API documentation</anchor> for usage details.": "Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
|
||||
"View the <anchor>MCP documentation</anchor>.": "Consulta la <anchor>documentazione MCP</anchor>.",
|
||||
"View the": "Visualizza la",
|
||||
"for usage details.": "per i dettagli sull'utilizzo.",
|
||||
"for setup instructions.": "per le istruzioni di configurazione.",
|
||||
"API documentation": "Documentazione API",
|
||||
"Sources": "Fonti",
|
||||
"AI Answers not available for attachments": "Risposte AI non disponibili per gli allegati",
|
||||
"No answer available": "Nessuna risposta disponibile",
|
||||
@@ -726,34 +654,12 @@
|
||||
"Mark all as read": "Segna tutto come letto",
|
||||
"Mark as read": "Segna come letto",
|
||||
"More options": "Altre opzioni",
|
||||
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> ti ha menzionato in un commento",
|
||||
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> ha commentato una pagina",
|
||||
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> ha risolto un commento",
|
||||
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> ti ha menzionato in una pagina",
|
||||
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> ti ha dato accesso in modifica a una pagina",
|
||||
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> ti ha dato accesso in visualizzazione a una pagina",
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> ha aggiornato una pagina",
|
||||
"Watch page": "Segui pagina",
|
||||
"Stop watching": "Smetti di seguire",
|
||||
"Watch space": "Segui spazio",
|
||||
"Stop watching space": "Smetti di seguire lo spazio",
|
||||
"Email notifications": "Notifiche email",
|
||||
"Page updates": "Aggiornamenti pagina",
|
||||
"Get notified when pages you watch are updated.": "Ricevi una notifica quando le pagine che segui vengono aggiornate.",
|
||||
"Page mentions": "Menzioni nella pagina",
|
||||
"Get notified when someone mentions you on a page.": "Ricevi una notifica quando qualcuno ti menziona su una pagina.",
|
||||
"Comment mentions": "Menzioni nei commenti",
|
||||
"Get notified when someone mentions you in a comment.": "Ricevi una notifica quando qualcuno ti menziona in un commento.",
|
||||
"New comments": "Nuovi commenti",
|
||||
"Get notified about new comments on threads you participate in.": "Ricevi una notifica sui nuovi commenti nelle discussioni a cui partecipi.",
|
||||
"Resolved comments": "Commenti risolti",
|
||||
"Get notified when your comment is resolved.": "Ricevi una notifica quando il tuo commento viene risolto.",
|
||||
"You are now watching this page": "Ora stai seguendo questa pagina",
|
||||
"You are no longer watching this page": "Non stai più seguendo questa pagina",
|
||||
"You are now watching this space": "Ora stai seguendo questo spazio",
|
||||
"You are no longer watching this space": "Non stai più seguendo questo spazio",
|
||||
"Direct": "Diretto",
|
||||
"Updates": "Aggiornamenti",
|
||||
"mentioned you in a comment": "ti ha menzionato in un commento",
|
||||
"commented on a page": "ha commentato una pagina",
|
||||
"resolved a comment": "ha risolto un commento",
|
||||
"mentioned you on a page": "ti ha menzionato in una pagina",
|
||||
"gave you edit access to a page": "ti ha concesso l'accesso per modificare una pagina",
|
||||
"gave you view access to a page": "ti ha concesso l'accesso per visualizzare una pagina",
|
||||
"Today": "Oggi",
|
||||
"Yesterday": "Ieri",
|
||||
"This week": "Questa settimana",
|
||||
@@ -787,369 +693,5 @@
|
||||
"Failed to update trash retention": "Impossibile aggiornare la conservazione del cestino",
|
||||
"Removed page restriction": "Restrizione della pagina rimossa",
|
||||
"Added page permission": "Permesso sulla pagina aggiunto",
|
||||
"Removed page permission": "Permesso sulla pagina rimosso",
|
||||
"day": "giorno",
|
||||
"days": "giorni",
|
||||
"week": "settimana",
|
||||
"weeks": "settimane",
|
||||
"month": "mese",
|
||||
"months": "mesi",
|
||||
"year": "anno",
|
||||
"years": "anni",
|
||||
"Period": "Periodo",
|
||||
"Fixed date": "Data fissa",
|
||||
"Indefinitely": "A tempo indeterminato",
|
||||
"Days": "Giorni",
|
||||
"Weeks": "Settimane",
|
||||
"Months": "Mesi",
|
||||
"Years": "Anni",
|
||||
"Pick a date": "Scegli una data",
|
||||
"Maximum is {{max}} {{unit}} for this unit": "Il massimo consentito è {{max}} {{unit}} per questa unità",
|
||||
"Never expires. Verifiers can re-verify at any time.": "Non scade mai. I verificatori possono verificare nuovamente in qualsiasi momento.",
|
||||
"Verified": "Verificato",
|
||||
"Review needed": "Revisione necessaria",
|
||||
"Verification expired": "Verifica scaduta",
|
||||
"Draft": "Bozza",
|
||||
"In Approval": "In approvazione",
|
||||
"In approval": "In approvazione",
|
||||
"Approved": "Approvato",
|
||||
"Obsolete": "Obsoleto",
|
||||
"Expiring": "In scadenza",
|
||||
"Set up verification": "Configura la verifica",
|
||||
"Verify page": "Verifica la pagina",
|
||||
"Page verification": "Verifica della pagina",
|
||||
"Add verification": "Aggiungi verifica",
|
||||
"Edit verification": "Modifica verifica",
|
||||
"Search by title": "Cerca per titolo",
|
||||
"Choose how this page should stay accurate.": "Scegli come mantenere accurata questa pagina.",
|
||||
"Recurring verification": "Verifica ricorrente",
|
||||
"Verifiers re-confirm this page on a schedule.": "I verificatori riconfermano questa pagina secondo una pianificazione.",
|
||||
"Re-verify on a schedule (e.g every 30 days )": "Verifica nuovamente secondo una pianificazione (ad es. ogni 30 giorni)",
|
||||
"Page stays editable at all times": "La pagina resta sempre modificabile",
|
||||
"Best for runbooks, FAQs, living documentation": "Ideale per runbook, FAQ e documentazione dinamica",
|
||||
"Approval workflow": "Flusso di approvazione",
|
||||
"Formal document lifecycle with named approvers.": "Ciclo di vita formale del documento con approvatori nominati.",
|
||||
"Draft → In approval → Approved → Obsolete": "Bozza → In approvazione → Approvato → Obsoleto",
|
||||
"Locked once approved, with full history": "Bloccato una volta approvato, con cronologia completa",
|
||||
"Designed for ISO 9001, ISO 13485, and FDA": "Progettato per ISO 9001, ISO 13485 e FDA",
|
||||
"Best for SOPs and controlled documents": "Ideale per SOP e documenti controllati",
|
||||
"Back": "Indietro",
|
||||
"Quality management": "Gestione della qualità",
|
||||
"Recurring": "Ricorrente",
|
||||
"Pages move through draft, approval, and approved stages.": "Le pagine passano attraverso le fasi di bozza, approvazione e approvato.",
|
||||
"Verifiers": "Verificatori",
|
||||
"Add verifier": "Aggiungi verificatore",
|
||||
"I've reviewed this page for accuracy": "Ho controllato l'accuratezza di questa pagina",
|
||||
"Set up": "Configura",
|
||||
"Remove verification": "Rimuovi verifica",
|
||||
"Are you sure you want to remove verification from this page?": "Sei sicuro di voler rimuovere la verifica da questa pagina?",
|
||||
"Assigned verifiers must periodically re-verify this page.": "I verificatori assegnati devono verificare nuovamente questa pagina periodicamente.",
|
||||
"Last verified by {{name}} {{time}} (expired)": "Ultima verifica effettuata da {{name}} {{time}} (scaduta)",
|
||||
"The fixed expiration date has passed.": "La data di scadenza fissa è trascorsa.",
|
||||
"Verified by {{name}} {{time}}": "Verificato da {{name}} {{time}}",
|
||||
"Expires {{date}}": "Scade il {{date}}",
|
||||
"Expired {{date}}": "Scaduto il {{date}}",
|
||||
"Mark as obsolete": "Contrassegna come obsoleto",
|
||||
"Mark obsolete": "Contrassegna come obsoleto",
|
||||
"Returned by {{name}} {{time}}": "Restituito da {{name}} {{time}}",
|
||||
"No approval has been requested yet.": "Non è stata ancora richiesta alcuna approvazione.",
|
||||
"Submitted by {{name}} {{time}}": "Inviato da {{name}} {{time}}",
|
||||
"Someone": "Qualcuno",
|
||||
"Approved by {{name}} {{time}}": "Approvato da {{name}} {{time}}",
|
||||
"This document has been marked as obsolete.": "Questo documento è stato contrassegnato come obsoleto.",
|
||||
"Rejection comment": "Commento di rifiuto",
|
||||
"Reason for returning this document...": "Motivo della restituzione di questo documento...",
|
||||
"Confirm rejection": "Conferma rifiuto",
|
||||
"Submit for approval": "Invia per approvazione",
|
||||
"Reject": "Rifiuta",
|
||||
"Approve": "Approva",
|
||||
"Re-submit for approval": "Invia nuovamente per approvazione",
|
||||
"Verified until": "Verificato fino al",
|
||||
"QMS": "QMS",
|
||||
"Verified pages": "Pagine verificate",
|
||||
"Search pages...": "Cerca pagine...",
|
||||
"Filter by space": "Filtra per spazio",
|
||||
"Filter by type": "Filtra per tipo",
|
||||
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> ha verificato una pagina",
|
||||
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> ha inviato una pagina per la tua approvazione",
|
||||
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> ha restituito una pagina per la revisione",
|
||||
"Page verification expires soon": "La verifica della pagina scadrà presto",
|
||||
"Page verification has expired": "La verifica della pagina è scaduta",
|
||||
"Verifying your email": "Verifica della tua email in corso",
|
||||
"Please wait...": "Attendere...",
|
||||
"Verification failed. The link may have expired.": "Verifica non riuscita. Il link potrebbe essere scaduto.",
|
||||
"Check your email": "Controlla la tua email",
|
||||
"We sent a verification link to {{email}}.": "Abbiamo inviato un link di verifica a {{email}}.",
|
||||
"We sent a verification link to your email.": "Abbiamo inviato un link di verifica alla tua email.",
|
||||
"Click the link to verify your email and access your workspace.": "Clicca sul link per verificare la tua email e accedere al tuo workspace.",
|
||||
"Resend verification email": "Invia nuovamente email di verifica",
|
||||
"Verification email sent. Please check your inbox.": "Email di verifica inviata. Controlla la tua casella di posta.",
|
||||
"Failed to resend verification email. Please try again.": "Invio dell'email di verifica non riuscito. Si prega di riprovare.",
|
||||
"We've sent you an email with your associated workspaces.": "Ti abbiamo inviato un'email con i workspace associati.",
|
||||
"Load more": "Carica altro",
|
||||
"Log out of all devices": "Disconnetti da tutti i dispositivi",
|
||||
"Log out of all sessions except this device": "Disconnetti da tutte le sessioni tranne questo dispositivo",
|
||||
"This Device": "Questo dispositivo",
|
||||
"Unknown device": "Dispositivo sconosciuto",
|
||||
"No active sessions": "Nessuna sessione attiva",
|
||||
"Session revoked": "Sessione revocata",
|
||||
"All other sessions revoked": "Tutte le altre sessioni sono state revocate",
|
||||
"Last used": "Ultimo utilizzo",
|
||||
"Created": "Creato",
|
||||
"Rename": "Rinomina",
|
||||
"Publish": "Pubblica",
|
||||
"Security": "Sicurezza",
|
||||
"Enforce SSO": "Rendi obbligatorio SSO",
|
||||
"Once enforced, members will not be able to login with email and password.": "Una volta reso obbligatorio, i membri non potranno accedere con email e password.",
|
||||
"AI-generated content may not be accurate.": "I contenuti generati dall'IA potrebbero non essere accurati.",
|
||||
"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.",
|
||||
"How can I help you today?": "Come posso aiutarti oggi?",
|
||||
"New chat": "Nuova chat",
|
||||
"No chat history": "Nessuna cronologia chat",
|
||||
"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",
|
||||
"Translate this page": "Traduci questa pagina",
|
||||
"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?",
|
||||
"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 for {{title}}": "Menu chat per {{title}}",
|
||||
"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 for {{name}}": "Azioni membro per {{name}}",
|
||||
"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}}",
|
||||
"Allow personal spaces": "Consenti spazi personali",
|
||||
"Members can create their own personal space.": "I membri possono creare il proprio spazio personale.",
|
||||
"Toggle allow personal spaces": "Attiva/disattiva spazi personali",
|
||||
"Create personal space": "Crea spazio personale",
|
||||
"Personal space": "Spazio personale",
|
||||
"{{name}}'s space": "Spazio di {{name}}",
|
||||
"Apply": "Applica",
|
||||
"Cells that aren't already a page reference will be cleared.": "Le celle che non sono già un riferimento a una pagina verranno svuotate.",
|
||||
"Cells that aren't a valid URL will be cleared.": "Le celle che non contengono un URL valido verranno svuotate.",
|
||||
"Cells that aren't a valid email address will be cleared.": "Le celle che non contengono un indirizzo email valido verranno svuotate.",
|
||||
"Cells that can't be parsed as a date will be cleared.": "Le celle che non possono essere interpretate come una data verranno svuotate.",
|
||||
"Cells that can't be parsed as a number will be cleared.": "Le celle che non possono essere interpretate come un numero verranno svuotate.",
|
||||
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Le celle verranno convertite forzatamente (yes/true/1 diventeranno selezionate; tutto il resto diventerà non selezionato o verrà svuotato).",
|
||||
"Cells will be reinterpreted under the new type.": "Le celle verranno reinterpretate con il nuovo tipo.",
|
||||
"Cells will be replaced with a comma-separated list of file names.": "Le celle verranno sostituite con un elenco di nomi file separati da virgole.",
|
||||
"Cells will be replaced with a comma-separated list of option names.": "Le celle verranno sostituite con un elenco di nomi di opzioni separati da virgole.",
|
||||
"Cells will be replaced with the option name.": "Le celle verranno sostituite con il nome dell'opzione.",
|
||||
"Cells will be replaced with the page title.": "Le celle verranno sostituite con il titolo della pagina.",
|
||||
"Cells will be replaced with the person's name.": "Le celle verranno sostituite con il nome della persona.",
|
||||
"Change type": "Cambia tipo",
|
||||
"Change type to {{label}}?": "Cambiare tipo in {{label}}?",
|
||||
"Converting…": "Conversione…",
|
||||
"Existing values become single-item lists. No data is lost.": "I valori esistenti diventano elenchi con un solo elemento. Nessun dato andrà perso.",
|
||||
"Only the first selected item per row will be kept; the rest will be discarded.": "Verrà mantenuto solo il primo elemento selezionato per riga; gli altri verranno scartati.",
|
||||
"Previous record": "Record precedente",
|
||||
"Next record": "Record successivo",
|
||||
"Record actions": "Azioni record",
|
||||
"Delete record": "Elimina record",
|
||||
"Delete record?": "Eliminare il record?",
|
||||
"This action cannot be undone.": "Questa azione non può essere annullata.",
|
||||
"to navigate": "per navigare",
|
||||
"to close": "per chiudere",
|
||||
"Expand row {{number}}": "Espandi riga {{number}}",
|
||||
"Saving…": "Salvataggio…",
|
||||
"Read-only": "Sola lettura",
|
||||
"Loading…": "Caricamento…",
|
||||
"Updated {{when}}": "Aggiornato {{when}}",
|
||||
"Add property": "Aggiungi proprietà",
|
||||
"Create property": "Crea proprietà",
|
||||
"Hide properties": "Nascondi proprietà",
|
||||
"Find a property type": "Trova un tipo di proprietà",
|
||||
"Properties": "Proprietà"
|
||||
"Removed page permission": "Permesso sulla pagina rimosso"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@
|
||||
"Add members": "Adicionar membros",
|
||||
"Add to groups": "Adicionar aos grupos",
|
||||
"Add space members": "Adicionar membros do espaço",
|
||||
"Add to favorites": "Adicionar aos favoritos",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
|
||||
@@ -41,8 +40,6 @@
|
||||
"Dark": "Escuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Excluir",
|
||||
"Remove from page": "Remover da página",
|
||||
"Base options": "Opções da base",
|
||||
"Delete group": "Excluir grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Você tem certeza que quer deletar essa página? Isso irá deletar todas as páginas filhas e to o histórico. Esta ação é irreversível.",
|
||||
"Description": "Descrição",
|
||||
@@ -62,7 +59,7 @@
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Insira uma senha forte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
|
||||
"enter valid emails addresses": "insira endereços de e-mail válidos",
|
||||
"enter valid emails addresses": "insira endereços de email válidos",
|
||||
"Enter your current password": "Insira sua senha atual",
|
||||
"enter your full name": "insira seu nome completo",
|
||||
"Enter your new password": "Insira sua nova senha",
|
||||
@@ -73,32 +70,10 @@
|
||||
"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.",
|
||||
"Failed to update data": "Falha ao atualizar dados",
|
||||
"Failed to create base": "Falha ao criar base",
|
||||
"Failed to update base": "Falha ao atualizar base",
|
||||
"Failed to delete base": "Falha ao excluir base",
|
||||
"Failed to create property": "Falha ao criar propriedade",
|
||||
"Failed to update property": "Falha ao atualizar propriedade",
|
||||
"Failed to delete property": "Falha ao excluir propriedade",
|
||||
"Failed to reorder property": "Falha ao reordenar propriedade",
|
||||
"Failed to create view": "Falha ao criar visualização",
|
||||
"Failed to update view": "Falha ao atualizar visualização",
|
||||
"Failed to delete view": "Falha ao excluir visualização",
|
||||
"Failed to create row": "Falha ao criar linha",
|
||||
"Failed to update row": "Falha ao atualizar linha",
|
||||
"Failed to delete row": "Falha ao excluir linha",
|
||||
"Failed to delete rows": "Falha ao excluir linhas",
|
||||
"Failed to reorder row": "Falha ao reordenar linha",
|
||||
"Failed to move card": "Falha ao mover cartão",
|
||||
"Failed to add card": "Falha ao adicionar cartão",
|
||||
"Failed to export CSV": "Falha ao exportar CSV",
|
||||
"Favorite spaces": "Espaços favoritos",
|
||||
"Favorite spaces appear here": "Os espaços favoritos aparecem aqui",
|
||||
"Favorites": "Favoritos",
|
||||
"Full access": "Acesso total",
|
||||
"Full page width": "Usar largura total da página",
|
||||
"Full width": "Largura total",
|
||||
@@ -117,7 +92,6 @@
|
||||
"Invite by email": "Convidar por email",
|
||||
"Invite members": "Convidar membros",
|
||||
"Invite new members": "Convidar novos membros",
|
||||
"Invite People": "Convidar pessoas",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
|
||||
"Join the workspace": "Entrar no workspace",
|
||||
@@ -165,7 +139,6 @@
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Atualizado recentemente",
|
||||
"Remove": "Remover",
|
||||
"Remove from favorites": "Remover dos favoritos",
|
||||
"Remove group member": "Remover membro do grupo",
|
||||
"Remove space member": "Remover membro do espaço",
|
||||
"Restore": "Restaurar",
|
||||
@@ -176,16 +149,16 @@
|
||||
"Search for users": "Buscar usuários",
|
||||
"Search for users and groups": "Buscar usuários e grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Selecione o idioma",
|
||||
"Select role": "Selecione a função",
|
||||
"Select role to assign to all invited members": "Selecione a função a ser atribuída a todos os membros convidados",
|
||||
"Select theme": "Selecione o tema",
|
||||
"Select language": "Selecionar idioma",
|
||||
"Select role": "Selecionar função",
|
||||
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
|
||||
"Select theme": "Selecionar tema",
|
||||
"Send invitation": "Enviar convite",
|
||||
"Invitation sent": "Convite enviado",
|
||||
"Settings": "Configurações",
|
||||
"Setup workspace": "Configurar workspace",
|
||||
"Sign In": "Entrar",
|
||||
"Sign Up": "Cadastrar-se",
|
||||
"Sign Up": "Registrar-se",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espaço",
|
||||
"Space description": "Descrição do espaço",
|
||||
@@ -198,35 +171,34 @@
|
||||
"No space found": "Nenhum espaço encontrado",
|
||||
"Search for spaces": "Pesquisar espaços",
|
||||
"Start typing to search...": "Comece a digitar para buscar...",
|
||||
"Status": "Status",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado com sucesso",
|
||||
"Successfully restored": "Restaurado com sucesso",
|
||||
"System settings": "Configurações do sistema",
|
||||
"Templates": "Modelos",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
|
||||
"Toggle full page width": "Alternar largura total da página",
|
||||
"Toggle full page width": "Alternar para largura total da página",
|
||||
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
|
||||
"untitled": "sem título",
|
||||
"Untitled": "Sem título",
|
||||
"Updated successfully": "Atualizado com sucesso",
|
||||
"User": "Usuário",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Nome do workspace",
|
||||
"Workspace": "Espaço de Trabalho",
|
||||
"Workspace Name": "Nome do Workspace",
|
||||
"Workspace settings": "Configurações do workspace",
|
||||
"You can change your password here.": "Você pode alterar sua senha aqui.",
|
||||
"Your Email": "Seu e-mail",
|
||||
"Your Email": "Seu email",
|
||||
"Your import is complete.": "Sua importação está concluída.",
|
||||
"Your name": "Seu nome",
|
||||
"Your Name": "Seu Nome",
|
||||
"Your password": "Sua senha",
|
||||
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
|
||||
"Sidebar toggle": "Alternar barra lateral",
|
||||
"Sidebar toggle": "Interruptor do painel lateral",
|
||||
"Comments": "Comentários",
|
||||
"404 page not found": "404 página não encontrada",
|
||||
"404 page not found": "Erro 404: Página não encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
|
||||
"Take me back to homepage": "Voltar para a página inicial",
|
||||
"Forgot password": "Esqueceu a senha",
|
||||
"Take me back to homepage": "Leve-me de volta para a página inicial",
|
||||
"Forgot password": "Esqueci a senha",
|
||||
"Forgot your password?": "Esqueceu sua senha?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
|
||||
"Send reset link": "Enviar link de recuperação",
|
||||
@@ -243,8 +215,6 @@
|
||||
"Edit comment": "Editar comentário",
|
||||
"Delete comment": "Excluir comentário",
|
||||
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
|
||||
"Delete chat": "Excluir chat",
|
||||
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Tem certeza de que deseja excluir '{{title}}'? Esta ação não pode ser desfeita.",
|
||||
"Comment created successfully": "Comentário criado com sucesso",
|
||||
"Error creating comment": "Erro ao criar comentário",
|
||||
"Comment updated successfully": "Comentário atualizado com sucesso",
|
||||
@@ -253,12 +223,12 @@
|
||||
"Failed to delete comment": "Falha ao excluir comentário",
|
||||
"Comment resolved successfully": "Comentário resolvido com sucesso",
|
||||
"Comment re-opened successfully": "Comentário reaberto com sucesso",
|
||||
"Comment unresolved successfully": "Comentário marcado como não resolvido com sucesso",
|
||||
"Comment unresolved successfully": "Comentário não resolvido com sucesso",
|
||||
"Failed to resolve comment": "Falha ao resolver comentário",
|
||||
"Resolve comment": "Resolver comentário",
|
||||
"Unresolve comment": "Marcar comentário como não resolvido",
|
||||
"Resolve Comment Thread": "Resolver tópico de comentários",
|
||||
"Unresolve Comment Thread": "Marcar tópico de comentários como não resolvido",
|
||||
"Unresolve comment": "Não resolver comentário",
|
||||
"Resolve Comment Thread": "Resolver Fio de Comentários",
|
||||
"Unresolve Comment Thread": "Não resolver Fio de Comentários",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Tem certeza de que deseja resolver este fio de comentários? Isso o marcará como concluído.",
|
||||
"Are you sure you want to unresolve this comment thread?": "Tem certeza de que deseja não resolver este fio de comentários?",
|
||||
"Resolved": "Resolvido",
|
||||
@@ -281,7 +251,7 @@
|
||||
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
|
||||
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
|
||||
"Confirm space name": "Confirmar nome do espaço",
|
||||
"Confirm space name": "Confirme o nome do espaço",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
@@ -297,9 +267,6 @@
|
||||
"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",
|
||||
@@ -310,19 +277,6 @@
|
||||
"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",
|
||||
@@ -335,11 +289,6 @@
|
||||
"Save & Exit": "Salvar e Sair",
|
||||
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
|
||||
"Paste link": "Colar link",
|
||||
"Paste link or search pages": "Cole o link ou pesquise páginas",
|
||||
"Link to web page": "Link para página da web",
|
||||
"Recents": "Recentes",
|
||||
"Page or URL": "Página ou URL",
|
||||
"Link title": "Título do link",
|
||||
"Edit link": "Editar link",
|
||||
"Remove link": "Remover link",
|
||||
"Add link": "Adicionar link",
|
||||
@@ -358,7 +307,7 @@
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Cinza",
|
||||
"Embed link": "Link embutido",
|
||||
"Invalid {{provider}} embed link": "Link de incorporação do {{provider}} inválido",
|
||||
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido",
|
||||
"Embed {{provider}}": "Incorporar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
|
||||
"Bold": "Negrito",
|
||||
@@ -385,11 +334,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.",
|
||||
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
|
||||
"Uploading {{name}}": "Enviando {{name}}",
|
||||
"Uploading file": "Enviando arquivo",
|
||||
@@ -400,27 +346,19 @@
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Citação",
|
||||
"Image": "Imagem",
|
||||
"Audio": "Áudio",
|
||||
"Embed PDF": "Incorporar PDF",
|
||||
"Upload and embed a PDF file.": "Envie e incorpore um arquivo PDF.",
|
||||
"Embed as PDF": "Incorporar como PDF",
|
||||
"Failed to load PDF": "Falha ao carregar PDF",
|
||||
"Convert to attachment": "Converter em anexo",
|
||||
"File attachment": "Anexo de arquivo",
|
||||
"Toggle block": "Bloco recolhível",
|
||||
"Callout": "Chamada",
|
||||
"Toggle block": "Bloco colapsável",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insira um aviso.",
|
||||
"Math inline": "Matemática em linha",
|
||||
"Math inline": "Matemática inline",
|
||||
"Insert inline math equation.": "Insira uma equação matemática inline.",
|
||||
"Math block": "Bloco matemático",
|
||||
"Insert math equation": "Inserir equação matemática",
|
||||
"Math block": "Bloco de matemática",
|
||||
"Insert math equation": "Insira uma equação matemática",
|
||||
"Mermaid diagram": "Diagrama Mermaid",
|
||||
"Insert mermaid diagram": "Inserir diagrama Mermaid",
|
||||
"Insert and design Drawio diagrams": "Inserir e criar diagramas Drawio",
|
||||
"Insert current date": "Inserir data atual",
|
||||
"Time": "Horário",
|
||||
"Insert current time": "Inserir horário atual",
|
||||
"Draw and sketch excalidraw diagrams": "Desenhar e esboçar diagramas Excalidraw",
|
||||
"Insert mermaid diagram": "Insira um diagrama Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio",
|
||||
"Insert current date": "Insira a data atual",
|
||||
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw",
|
||||
"Multiple": "Múltiplo",
|
||||
"Turn into": "Transformar em",
|
||||
"Text align": "Alinhar texto",
|
||||
@@ -428,25 +366,20 @@
|
||||
"Go to homepage": "Ir para a página inicial",
|
||||
"Pages you create will show up here.": "As páginas que você criar aparecerão aqui.",
|
||||
"Heading {{level}}": "Título {{level}}",
|
||||
"Toggle title": "Título do bloco recolhível",
|
||||
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para ver os comandos",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos",
|
||||
"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",
|
||||
"Wide center": "Centro largo",
|
||||
"Left wide": "Largo à esquerda",
|
||||
"Right wide": "Largo à direita",
|
||||
"Names do not match": "Os nomes não correspondem",
|
||||
"Names do not match": "Os nomes não coincidem",
|
||||
"Today, {{time}}": "Hoje, {{time}}",
|
||||
"Yesterday, {{time}}": "Ontem, {{time}}",
|
||||
"now": "agora",
|
||||
"Space created successfully": "Espaço criado com sucesso",
|
||||
"Space updated successfully": "Espaço atualizado com sucesso",
|
||||
"Space deleted successfully": "Espaço excluído com sucesso",
|
||||
@@ -460,12 +393,11 @@
|
||||
"Character count: {{characterCount}}": "Contagem de caracteres: {{characterCount}}",
|
||||
"New update": "Nova atualização",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} está disponível",
|
||||
"Default page edit mode": "Modo padrão de edição da página",
|
||||
"Default page edit mode": "Modo de edição de página padrão",
|
||||
"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",
|
||||
"Member deleted successfully": "Membro removido com sucesso",
|
||||
"Are you sure you want to delete this workspace member? This action is irreversible.": "Você tem certeza que deseja deletar este membro do workspace? Esta ação é irreversível.",
|
||||
"Deactivate member": "Desativar membro",
|
||||
"Activate member": "Ativar membro",
|
||||
@@ -478,38 +410,36 @@
|
||||
"Move page": "Mover página",
|
||||
"Move page to a different space.": "Mover página para um espaço diferente.",
|
||||
"Real-time editor connection lost. Retrying...": "Conexão do editor em tempo real perdida. Tentando novamente...",
|
||||
"Table of contents": "Sumário",
|
||||
"Table of contents": "Tabela de conteúdos",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "Adicionar títulos (H1, H2, H3) para gerar uma tabela de conteúdo.",
|
||||
"Share": "Compartilhar",
|
||||
"Public sharing": "Compartilhamento público",
|
||||
"Shared by": "Compartilhado por",
|
||||
"Shared at": "Compartilhado em",
|
||||
"Inherits public sharing from": "Herda o compartilhamento público de",
|
||||
"Inherits public sharing from": "Herdado do compartilhamento público de",
|
||||
"Share to web": "Compartilhar na web",
|
||||
"Shared to web": "Compartilhado na web",
|
||||
"Anyone with the link can view this page": "Qualquer pessoa com o link pode visualizar esta página",
|
||||
"Make this page publicly accessible": "Tornar esta página acessível publicamente",
|
||||
"Include sub-pages": "Incluir subpáginas",
|
||||
"Make sub-pages public too": "Tornar as subpáginas públicas também",
|
||||
"Anyone with the link can view this page": "Qualquer um com o link pode ver esta página",
|
||||
"Make this page publicly accessible": "Tornar esta página publicamente acessível",
|
||||
"Include sub-pages": "Incluir sub-páginas",
|
||||
"Make sub-pages public too": "Tornar as sub-páginas públicas também",
|
||||
"Allow search engines to index page": "Permitir que mecanismos de busca indexem a página",
|
||||
"Open page": "Abrir página",
|
||||
"Page": "Página",
|
||||
"Delete public share link": "Excluir link de compartilhamento público",
|
||||
"Delete public share link": "Excluir o link público compartilhado",
|
||||
"Delete share": "Excluir compartilhamento",
|
||||
"Are you sure you want to delete this shared link?": "Tem certeza de que deseja excluir este link compartilhado?",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Páginas compartilhadas publicamente dos espaços dos quais você é membro aparecerão aqui",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Páginas compartilhadas publicamente de espaços que você é membro aparecerão aqui",
|
||||
"Share deleted successfully": "Compartilhamento excluído com sucesso",
|
||||
"Share not found": "Compartilhamento não encontrado",
|
||||
"Failed to share page": "Falha ao compartilhar a página",
|
||||
"Failed to share page": "Falha ao compartilhar página",
|
||||
"Disable public sharing": "Desativar compartilhamento público",
|
||||
"Prevent members from sharing pages publicly.": "Impedir que os membros compartilhem páginas publicamente.",
|
||||
"Toggle public sharing": "Alternar compartilhamento público",
|
||||
"Toggle space public sharing": "Alternar compartilhamento público do espaço",
|
||||
"Allow viewers to comment": "Permitir que os visualizadores comentem",
|
||||
"Allow viewers to add comments on pages in this space.": "Permitir que os visualizadores adicionem comentários em páginas deste espaço.",
|
||||
"Toggle viewer comments": "Ativar/desativar comentários de visualizadores",
|
||||
"Public sharing is disabled at the workspace level": "O compartilhamento público está desativado no nível do espaço de trabalho",
|
||||
"Prevent pages in this space from being shared publicly.": "Impedir que as páginas neste espaço sejam compartilhadas publicamente.",
|
||||
"Requires an enterprise license": "Requer uma licença empresarial",
|
||||
"Page permissions": "Permissões da página},{",
|
||||
"Control who can view and edit individual pages. Available with an enterprise license.": "Controle quem pode visualizar e editar páginas individuais. Disponível com licença empresarial.",
|
||||
"Enable public sharing": "Ativar compartilhamento público",
|
||||
@@ -524,7 +454,7 @@
|
||||
"Copy page to a different space.": "Copiar página para um espaço diferente.",
|
||||
"Page copied successfully": "Página copiada com sucesso",
|
||||
"Page duplicated successfully": "Página duplicada com sucesso",
|
||||
"Find": "Localizar",
|
||||
"Find": "Encontrar",
|
||||
"Not found": "Não encontrado",
|
||||
"Previous Match (Shift+Enter)": "Correspondência anterior (Shift+Enter)",
|
||||
"Next match (Enter)": "Próxima correspondência (Enter)",
|
||||
@@ -534,16 +464,15 @@
|
||||
"Replace (Enter)": "Substituir (Enter)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Substituir tudo (Ctrl+Alt+Enter)",
|
||||
"Replace all": "Substituir tudo",
|
||||
"View all": "Ver tudo",
|
||||
"View all spaces": "Ver todos os espaços",
|
||||
"Error": "Erro",
|
||||
"Failed to disable MFA": "Falha ao desativar a MFA",
|
||||
"Disable two-factor authentication": "Desativar autenticação de dois fatores",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Desativar a autenticação de dois fatores tornará sua conta menos segura. Você só precisará de sua senha para entrar.",
|
||||
"Please enter your password to disable two-factor authentication:": "Por favor, insira sua senha para desativar a autenticação de dois fatores:",
|
||||
"Two-factor authentication has been enabled": "A autenticação de dois fatores foi ativada",
|
||||
"Two-factor authentication has been disabled": "A autenticação de dois fatores foi desativada",
|
||||
"2-step verification": "Verificação em 2 etapas",
|
||||
"Two-factor authentication has been enabled": "Autenticação de dois fatores foi ativada",
|
||||
"Two-factor authentication has been disabled": "Autenticação de dois fatores foi desativada",
|
||||
"2-step verification": "Verificação em duas etapas",
|
||||
"Protect your account with an additional verification layer when signing in.": "Proteja sua conta com uma camada adicional de verificação ao entrar.",
|
||||
"Two-factor authentication is active on your account.": "Autenticação de dois fatores está ativa na sua conta.",
|
||||
"Add 2FA method": "Adicionar método de 2FA",
|
||||
@@ -551,43 +480,43 @@
|
||||
"Disable": "Desativar",
|
||||
"Invalid verification code": "Código de verificação inválido",
|
||||
"New backup codes have been generated": "Novos códigos de backup foram gerados",
|
||||
"Failed to regenerate backup codes": "Falha ao regenerar os códigos de backup",
|
||||
"About backup codes": "Sobre os códigos de backup",
|
||||
"Failed to regenerate backup codes": "Falha ao regenerar códigos de backup",
|
||||
"About backup codes": "Sobre códigos de backup",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Códigos de backup podem ser usados para acessar sua conta se perder acesso ao aplicativo autenticador. Cada código só pode ser usado uma vez.",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Você pode regenerar novos códigos de backup a qualquer momento. Isso invalidará todos os códigos existentes.",
|
||||
"Confirm password": "Confirmar senha",
|
||||
"Generate new backup codes": "Gerar novos códigos de backup",
|
||||
"Save your new backup codes": "Salve seus novos códigos de backup",
|
||||
"Save your new backup codes": "Salvar seus novos códigos de backup",
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Certifique-se de salvar esses códigos em um local seguro. Seus códigos de backup antigos não são mais válidos.",
|
||||
"Your new backup codes": "Seus novos códigos de backup",
|
||||
"I've saved my backup codes": "Salvei meus códigos de backup",
|
||||
"I've saved my backup codes": "Eu salvei meus códigos de backup",
|
||||
"Failed to setup MFA": "Falha ao configurar a MFA",
|
||||
"Setup & Verify": "Configurar e verificar",
|
||||
"Setup & Verify": "Configurar & Verificar",
|
||||
"Add to authenticator": "Adicionar ao autenticador",
|
||||
"1. Scan this QR code with your authenticator app": "1. Escaneie este código QR com seu aplicativo autenticador",
|
||||
"Can't scan the code?": "Não consegue escanear o código?",
|
||||
"Enter this code manually in your authenticator app:": "Digite este código manualmente em seu aplicativo autenticador:",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. Insira o código de 6 dígitos do seu autenticador",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. Digite o código de 6 dígitos do seu autenticador",
|
||||
"Verify and enable": "Verificar e ativar",
|
||||
"Failed to generate QR code. Please try again.": "Falha ao gerar código QR. Por favor, tente novamente.",
|
||||
"Backup": "Backup",
|
||||
"Save codes": "Salvar códigos",
|
||||
"Save your backup codes": "Salve seus códigos de backup",
|
||||
"Save your backup codes": "Salvar seus códigos de backup",
|
||||
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Esses códigos podem ser usados para acessar sua conta se você perder o acesso ao aplicativo autenticador. Cada código só pode ser usado uma vez.",
|
||||
"Print": "Imprimir",
|
||||
"Two-factor authentication has been set up. Please log in again.": "A autenticação de dois fatores foi configurada. Por favor, faça login novamente.",
|
||||
"Two-Factor authentication required": "Autenticação de dois fatores obrigatória",
|
||||
"Your workspace requires two-factor authentication for all users": "Seu workspace exige autenticação de dois fatores para todos os usuários",
|
||||
"Two-Factor authentication required": "Autenticação de dois fatores necessária",
|
||||
"Your workspace requires two-factor authentication for all users": "Seu espaço de trabalho requer autenticação de dois fatores para todos os usuários",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Para continuar acessando seu espaço de trabalho, você deve configurar a autenticação de dois fatores. Isso adiciona uma camada extra de segurança à sua conta.",
|
||||
"Set up two-factor authentication": "Configurar autenticação de dois fatores",
|
||||
"Cancel and logout": "Cancelar e sair",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "Seu espaço de trabalho requer autenticação de dois fatores. Por favor, configure para continuar.",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Isso adiciona uma camada extra de segurança à sua conta, exigindo um código de verificação de seu aplicativo autenticador.",
|
||||
"Password is required": "A senha é obrigatória",
|
||||
"Password is required": "Senha é necessária",
|
||||
"Password must be at least 8 characters": "A senha deve ter pelo menos 8 caracteres",
|
||||
"Please enter a 6-digit code": "Insira um código de 6 dígitos",
|
||||
"Please enter a 6-digit code": "Por favor, insira um código de 6 dígitos",
|
||||
"Code must be exactly 6 digits": "O código deve ter exatamente 6 dígitos",
|
||||
"Enter the 6-digit code found in your authenticator app": "Insira o código de 6 dígitos encontrado no seu aplicativo autenticador",
|
||||
"Enter the 6-digit code found in your authenticator app": "Insira o código de 6 dígitos encontrado em seu aplicativo autenticador",
|
||||
"Need help authenticating?": "Precisa de ajuda para autenticar?",
|
||||
"MFA QR Code": "Código QR de MFA",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "Conta criada com sucesso. Por favor, faça login para configurar a autenticação de dois fatores.",
|
||||
@@ -595,36 +524,32 @@
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Redefinição de senha bem-sucedida. Por favor, faça login com sua nova senha para configurar a autenticação de dois fatores.",
|
||||
"Password reset was successful. Please log in with your new password.": "Redefinição de senha foi bem-sucedida. Por favor, faça login com sua nova senha.",
|
||||
"Two-factor authentication": "Autenticação de dois fatores",
|
||||
"Use authenticator app instead": "Usar aplicativo autenticador em vez disso",
|
||||
"Use authenticator app instead": "Use o aplicativo autenticador em vez disso",
|
||||
"Verify backup code": "Verificar código de backup",
|
||||
"Use backup code": "Usar código de backup",
|
||||
"Enter one of your backup codes": "Insira um dos seus códigos de backup",
|
||||
"Enter one of your backup codes": "Digite um de seus códigos de backup",
|
||||
"Backup code": "Código de backup",
|
||||
"Enter one of your backup codes. Each backup code can only be used once.": "Digite um de seus códigos de backup. Cada código de backup só pode ser usado uma vez.",
|
||||
"Verify": "Verificar",
|
||||
"Trash": "Lixeira",
|
||||
"Pages in trash will be permanently deleted after {{count}} days.": "{count, plural, one {A página na lixeira será excluída permanentemente após # dia.} other {As páginas na lixeira serão excluídas permanentemente após # dias.}}",
|
||||
"Deleted": "Excluído",
|
||||
"No pages in trash": "Nenhuma página na lixeira",
|
||||
"No pages in trash": "Sem páginas na lixeira",
|
||||
"Permanently delete page?": "Excluir página permanentemente?",
|
||||
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Tem certeza de que deseja excluir permanentemente '{{title}}'? Esta ação não pode ser desfeita.",
|
||||
"Restore '{{title}}' and its sub-pages?": "Restaurar '{{title}}' e suas subpáginas?",
|
||||
"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",
|
||||
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> 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",
|
||||
"Deleted at": "Excluído em",
|
||||
"Preview": "Visualização",
|
||||
"Base preview unavailable": "Pré-visualização da base indisponível",
|
||||
"Restore this base to view its contents.": "Restaure esta base para visualizar seu conteúdo.",
|
||||
"Subpages": "Subpáginas",
|
||||
"Failed to load subpages": "Falha ao carregar as subpáginas",
|
||||
"No subpages": "Nenhuma subpágina",
|
||||
"Subpages (Child pages)": "Subpáginas (páginas filhas)",
|
||||
"Failed to load subpages": "Falha ao carregar subpáginas",
|
||||
"No subpages": "Sem subpáginas",
|
||||
"Subpages (Child pages)": "Subpáginas (Páginas filhas)",
|
||||
"List all subpages of the current page": "Listar todas as subpáginas da página atual",
|
||||
"Attachments": "Anexos",
|
||||
"All spaces": "Todos os espaços",
|
||||
@@ -632,48 +557,52 @@
|
||||
"Find a space": "Encontrar um espaço",
|
||||
"Search in all your spaces": "Pesquisar em todos os seus espaços",
|
||||
"Type": "Tipo",
|
||||
"Enterprise": "Enterprise",
|
||||
"Enterprise": "Empresa",
|
||||
"Download attachment": "Baixar anexo",
|
||||
"Allowed email domains": "Domínios de e-mail permitidos",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Somente usuários com endereços de e-mail desses domínios podem se cadastrar via SSO.",
|
||||
"Allowed email domains": "Domínios de email permitidos",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Apenas usuários com endereços de email desses domínios podem se inscrever via SSO.",
|
||||
"Enter valid domain names separated by comma or space": "Insira nomes de domínio válidos separados por vírgula ou espaço",
|
||||
"Enforce two-factor authentication": "Exigir autenticação de dois fatores",
|
||||
"Enforce two-factor authentication": "Impor autenticação de dois fatores",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Uma vez imposto, todos os membros devem habilitar a autenticação de dois fatores para acessar o espaço de trabalho.",
|
||||
"Toggle MFA enforcement": "Alternar exigência de MFA",
|
||||
"Toggle MFA enforcement": "Alternar imposição de MFA",
|
||||
"Display name": "Nome de exibição",
|
||||
"Allow signup": "Permitir cadastro",
|
||||
"Enabled": "Ativado",
|
||||
"Advanced Settings": "Configurações avançadas",
|
||||
"Enable TLS/SSL": "Ativar TLS/SSL",
|
||||
"Allow signup": "Permitir inscrição",
|
||||
"Enabled": "Habilitado",
|
||||
"Advanced Settings": "Configurações Avançadas",
|
||||
"Enable TLS/SSL": "Habilitar TLS/SSL",
|
||||
"Use secure connection to LDAP server": "Usar conexão segura com o servidor LDAP",
|
||||
"Group sync": "Sincronização de grupos",
|
||||
"Group sync": "Sincronização de grupo",
|
||||
"No SSO providers found.": "Nenhum provedor de SSO encontrado.",
|
||||
"Delete SSO provider": "Excluir provedor de SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Tem certeza de que deseja excluir este provedor de SSO?",
|
||||
"Action": "Ação",
|
||||
"{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}",
|
||||
"Icon": "Ícone",
|
||||
"Upload image": "Enviar imagem",
|
||||
"Upload image": "Fazer upload da imagem",
|
||||
"Remove image": "Remover imagem",
|
||||
"Failed to remove image": "Falha ao remover imagem",
|
||||
"Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.",
|
||||
"Image removed successfully": "Imagem removida com sucesso",
|
||||
"API key": "Chave API",
|
||||
"API key created successfully": "Chave API criada com sucesso",
|
||||
"API keys": "Chaves API",
|
||||
"API management": "Gestão de API",
|
||||
"Are you sure you want to revoke this API key": "Tem certeza de que deseja revogar esta chave API",
|
||||
"Create API Key": "Criar Chave API",
|
||||
"Custom expiration date": "Data de expiração personalizada",
|
||||
"Enter a descriptive token name": "Insira um nome descritivo para o token",
|
||||
"Expiration": "Expiração",
|
||||
"Expired": "Expirado",
|
||||
"Expires": "Expira",
|
||||
"I've saved my API key": "Salvei minha chave API",
|
||||
"Last use": "Último uso",
|
||||
"No API keys found": "Nenhuma chave API encontrada",
|
||||
"No expiration": "Sem expiração",
|
||||
"Revoke API key": "Revogar chave API",
|
||||
"Revoked successfully": "Revogada com sucesso",
|
||||
"Select expiration date": "Selecionar data de expiração",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.",
|
||||
"Update": "Atualizar",
|
||||
"Update {{credential}}": "Atualizar {{credential}}",
|
||||
"Update API key": "Atualizar chave API",
|
||||
"Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho",
|
||||
"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.",
|
||||
@@ -684,7 +613,6 @@
|
||||
"AI Answer": "Resposta de IA",
|
||||
"Ask AI": "Pergunte à IA",
|
||||
"AI is thinking...": "IA está pensando...",
|
||||
"Thinking": "Pensando",
|
||||
"Ask a question...": "Faça uma pergunta...",
|
||||
"AI Answers": "Respostas de IA",
|
||||
"AI-powered search (AI Answers)": "Pesquisa com IA (Respostas de IA)",
|
||||
@@ -693,9 +621,7 @@
|
||||
"Generative AI (Ask AI)": "IA generativa (Perguntar à IA)",
|
||||
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Habilitar geração de conteúdo com IA no editor. Permite aos usuários gerar, melhorar, traduzir e transformar texto.",
|
||||
"Toggle generative AI": "Alternar IA generativa",
|
||||
"Upgrade your plan": "Faça upgrade do seu plano",
|
||||
"Available with a paid license": "Disponível com uma licença paga",
|
||||
"Upgrade your license tier.": "Faça upgrade do seu nível de licença.",
|
||||
"Enterprise feature": "Recurso empresarial",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "A IA está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
|
||||
"AI & MCP": "IA e MCP",
|
||||
"AI": "IA",
|
||||
@@ -703,15 +629,17 @@
|
||||
"Model Context Protocol (MCP)": "Protocolo de Contexto de Modelo (MCP)",
|
||||
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Ative o servidor MCP para permitir que assistentes de IA e ferramentas interajam com o conteúdo do seu espaço de trabalho.",
|
||||
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "O MCP está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
|
||||
"MCP documentation": "Documentação do MCP",
|
||||
"MCP Server URL": "URL do servidor MCP",
|
||||
"Use your API key for authentication. You can manage API keys in your account settings.": "Use sua chave de API para autenticação. Você pode gerenciar chaves de API nas configurações da sua conta.",
|
||||
"Supported tools": "Ferramentas compatíveis",
|
||||
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Seu espaço de trabalho tem MCP habilitado. Use sua chave de API para conectar assistentes de IA.",
|
||||
"MCP server URL:": "URL do servidor MCP:",
|
||||
"Learn more": "Saiba mais",
|
||||
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gerencie as chaves de API de todos os usuários do workspace. Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
|
||||
"View the <anchor>API documentation</anchor> for usage details.": "Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
|
||||
"View the <anchor>MCP documentation</anchor>.": "Veja a <anchor>documentação MCP</anchor>.",
|
||||
"View the": "Veja o",
|
||||
"for usage details.": "para detalhes de uso.",
|
||||
"for setup instructions.": "para instruções de configuração.",
|
||||
"API documentation": "Documentação da API",
|
||||
"Sources": "Fontes",
|
||||
"AI Answers not available for attachments": "Respostas de IA não disponíveis para anexos",
|
||||
"No answer available": "Nenhuma resposta disponível",
|
||||
@@ -726,34 +654,12 @@
|
||||
"Mark all as read": "Marcar todas como lidas",
|
||||
"Mark as read": "Marcar como lida",
|
||||
"More options": "Mais opções",
|
||||
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> mencionou você em um comentário",
|
||||
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> comentou em uma página",
|
||||
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> resolveu um comentário",
|
||||
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> mencionou você em uma página",
|
||||
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> concedeu a você acesso de edição a uma página",
|
||||
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> concedeu a você acesso de visualização a uma página",
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> atualizou uma página",
|
||||
"Watch page": "Acompanhar página",
|
||||
"Stop watching": "Parar de acompanhar",
|
||||
"Watch space": "Acompanhar espaço",
|
||||
"Stop watching space": "Parar de acompanhar espaço",
|
||||
"Email notifications": "Notificações por e-mail",
|
||||
"Page updates": "Atualizações da página",
|
||||
"Get notified when pages you watch are updated.": "Receba notificações quando as páginas que você observa forem atualizadas.",
|
||||
"Page mentions": "Menções na página",
|
||||
"Get notified when someone mentions you on a page.": "Receba notificações quando alguém mencionar você em uma página.",
|
||||
"Comment mentions": "Menções em comentários",
|
||||
"Get notified when someone mentions you in a comment.": "Receba notificações quando alguém mencionar você em um comentário.",
|
||||
"New comments": "Novos comentários",
|
||||
"Get notified about new comments on threads you participate in.": "Receba notificações sobre novos comentários nas discussões em que você participa.",
|
||||
"Resolved comments": "Comentários resolvidos",
|
||||
"Get notified when your comment is resolved.": "Receba notificações quando seu comentário for resolvido.",
|
||||
"You are now watching this page": "Agora você está observando esta página",
|
||||
"You are no longer watching this page": "Você não está mais observando esta página",
|
||||
"You are now watching this space": "Agora você está acompanhando este espaço",
|
||||
"You are no longer watching this space": "Você não está mais acompanhando este espaço",
|
||||
"Direct": "Direto",
|
||||
"Updates": "Atualizações",
|
||||
"mentioned you in a comment": "mencionou você em um comentário",
|
||||
"commented on a page": "comentou em uma página",
|
||||
"resolved a comment": "resolveu um comentário",
|
||||
"mentioned you on a page": "mencionou você em uma página",
|
||||
"gave you edit access to a page": "concedeu a você acesso para editar a página",
|
||||
"gave you view access to a page": "concedeu a você acesso para visualizar a página",
|
||||
"Today": "Hoje",
|
||||
"Yesterday": "Ontem",
|
||||
"This week": "Esta semana",
|
||||
@@ -787,369 +693,5 @@
|
||||
"Failed to update trash retention": "Falha ao atualizar a retenção da lixeira",
|
||||
"Removed page restriction": "Restrição de página removida",
|
||||
"Added page permission": "Permissão de página adicionada",
|
||||
"Removed page permission": "Permissão de página removida",
|
||||
"day": "dia",
|
||||
"days": "dias",
|
||||
"week": "semana",
|
||||
"weeks": "semanas",
|
||||
"month": "mês",
|
||||
"months": "meses",
|
||||
"year": "ano",
|
||||
"years": "anos",
|
||||
"Period": "Período",
|
||||
"Fixed date": "Data fixa",
|
||||
"Indefinitely": "Indefinidamente",
|
||||
"Days": "Dias",
|
||||
"Weeks": "Semanas",
|
||||
"Months": "Meses",
|
||||
"Years": "Anos",
|
||||
"Pick a date": "Escolha uma data",
|
||||
"Maximum is {{max}} {{unit}} for this unit": "O máximo é {{max}} {{unit}} para esta unidade",
|
||||
"Never expires. Verifiers can re-verify at any time.": "Nunca expira. Os verificadores podem verificar novamente a qualquer momento.",
|
||||
"Verified": "Verificado",
|
||||
"Review needed": "Revisão necessária",
|
||||
"Verification expired": "A verificação expirou",
|
||||
"Draft": "Rascunho",
|
||||
"In Approval": "Em aprovação",
|
||||
"In approval": "Em aprovação",
|
||||
"Approved": "Aprovado",
|
||||
"Obsolete": "Obsoleto",
|
||||
"Expiring": "Expirando",
|
||||
"Set up verification": "Configurar verificação",
|
||||
"Verify page": "Verificar página",
|
||||
"Page verification": "Verificação da página",
|
||||
"Add verification": "Adicionar verificação",
|
||||
"Edit verification": "Editar verificação",
|
||||
"Search by title": "Pesquisar por título",
|
||||
"Choose how this page should stay accurate.": "Escolha como esta página deve permanecer precisa.",
|
||||
"Recurring verification": "Verificação recorrente",
|
||||
"Verifiers re-confirm this page on a schedule.": "Os verificadores confirmam novamente esta página em uma programação definida.",
|
||||
"Re-verify on a schedule (e.g every 30 days )": "Verificar novamente em uma programação definida (ex.: a cada 30 dias)",
|
||||
"Page stays editable at all times": "A página permanece editável o tempo todo",
|
||||
"Best for runbooks, FAQs, living documentation": "Ideal para runbooks, FAQs e documentação viva",
|
||||
"Approval workflow": "Fluxo de aprovação",
|
||||
"Formal document lifecycle with named approvers.": "Ciclo de vida formal do documento com aprovadores nomeados.",
|
||||
"Draft → In approval → Approved → Obsolete": "Rascunho → Em aprovação → Aprovado → Obsoleto",
|
||||
"Locked once approved, with full history": "Bloqueado após a aprovação, com histórico completo",
|
||||
"Designed for ISO 9001, ISO 13485, and FDA": "Desenvolvido para ISO 9001, ISO 13485 e FDA",
|
||||
"Best for SOPs and controlled documents": "Ideal para POPs e documentos controlados",
|
||||
"Back": "Voltar",
|
||||
"Quality management": "Gestão da qualidade",
|
||||
"Recurring": "Recorrente",
|
||||
"Pages move through draft, approval, and approved stages.": "As páginas passam pelos estágios de rascunho, aprovação e aprovado.",
|
||||
"Verifiers": "Verificadores",
|
||||
"Add verifier": "Adicionar verificador",
|
||||
"I've reviewed this page for accuracy": "Revisei esta página quanto à precisão",
|
||||
"Set up": "Configurar",
|
||||
"Remove verification": "Remover verificação",
|
||||
"Are you sure you want to remove verification from this page?": "Tem certeza de que deseja remover a verificação desta página?",
|
||||
"Assigned verifiers must periodically re-verify this page.": "Os verificadores atribuídos devem verificar novamente esta página periodicamente.",
|
||||
"Last verified by {{name}} {{time}} (expired)": "Verificado pela última vez por {{name}} {{time}} (expirado)",
|
||||
"The fixed expiration date has passed.": "A data fixa de expiração já passou.",
|
||||
"Verified by {{name}} {{time}}": "Verificado por {{name}} {{time}}",
|
||||
"Expires {{date}}": "Expira em {{date}}",
|
||||
"Expired {{date}}": "Expirou em {{date}}",
|
||||
"Mark as obsolete": "Marcar como obsoleto",
|
||||
"Mark obsolete": "Marcar como obsoleto",
|
||||
"Returned by {{name}} {{time}}": "Devolvido por {{name}} {{time}}",
|
||||
"No approval has been requested yet.": "Nenhuma aprovação foi solicitada ainda.",
|
||||
"Submitted by {{name}} {{time}}": "Enviado por {{name}} {{time}}",
|
||||
"Someone": "Alguém",
|
||||
"Approved by {{name}} {{time}}": "Aprovado por {{name}} {{time}}",
|
||||
"This document has been marked as obsolete.": "Este documento foi marcado como obsoleto.",
|
||||
"Rejection comment": "Comentário de rejeição",
|
||||
"Reason for returning this document...": "Motivo para devolver este documento...",
|
||||
"Confirm rejection": "Confirmar rejeição",
|
||||
"Submit for approval": "Enviar para aprovação",
|
||||
"Reject": "Rejeitar",
|
||||
"Approve": "Aprovar",
|
||||
"Re-submit for approval": "Reenviar para aprovação",
|
||||
"Verified until": "Verificado até",
|
||||
"QMS": "SGQ",
|
||||
"Verified pages": "Páginas verificadas",
|
||||
"Search pages...": "Pesquisar páginas...",
|
||||
"Filter by space": "Filtrar por espaço",
|
||||
"Filter by type": "Filtrar por tipo",
|
||||
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> verificou uma página",
|
||||
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> enviou uma página para sua aprovação",
|
||||
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> devolveu uma página para revisão",
|
||||
"Page verification expires soon": "A verificação da página expirará em breve",
|
||||
"Page verification has expired": "A verificação da página expirou",
|
||||
"Verifying your email": "Verificando seu e-mail",
|
||||
"Please wait...": "Por favor, aguarde...",
|
||||
"Verification failed. The link may have expired.": "Falha na verificação. O link pode ter expirado.",
|
||||
"Check your email": "Verifique seu e-mail",
|
||||
"We sent a verification link to {{email}}.": "Enviamos um link de verificação para {{email}}.",
|
||||
"We sent a verification link to your email.": "Enviamos um link de verificação para seu e-mail.",
|
||||
"Click the link to verify your email and access your workspace.": "Clique no link para verificar seu e-mail e acessar seu workspace.",
|
||||
"Resend verification email": "Reenviar e-mail de verificação",
|
||||
"Verification email sent. Please check your inbox.": "E-mail de verificação enviado. Por favor, verifique sua caixa de entrada.",
|
||||
"Failed to resend verification email. Please try again.": "Falha ao reenviar o e-mail de verificação. Por favor, tente novamente.",
|
||||
"We've sent you an email with your associated workspaces.": "Enviamos um e-mail para você com seus workspaces associados.",
|
||||
"Load more": "Carregar mais",
|
||||
"Log out of all devices": "Sair de todos os dispositivos",
|
||||
"Log out of all sessions except this device": "Sair de todas as sessões, exceto deste dispositivo",
|
||||
"This Device": "Este dispositivo",
|
||||
"Unknown device": "Dispositivo desconhecido",
|
||||
"No active sessions": "Nenhuma sessão ativa",
|
||||
"Session revoked": "Sessão revogada",
|
||||
"All other sessions revoked": "Todas as outras sessões foram revogadas",
|
||||
"Last used": "Último uso",
|
||||
"Created": "Criado",
|
||||
"Rename": "Renomear",
|
||||
"Publish": "Publicar",
|
||||
"Security": "Segurança",
|
||||
"Enforce SSO": "Exigir SSO",
|
||||
"Once enforced, members will not be able to login with email and password.": "Depois de exigido, os membros não poderão fazer login com e-mail e senha.",
|
||||
"AI-generated content may not be accurate.": "O conteúdo gerado por IA pode não ser preciso.",
|
||||
"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.",
|
||||
"How can I help you today?": "Como posso ajudar você hoje?",
|
||||
"New chat": "Novo chat",
|
||||
"No chat history": "Nenhum histórico de chat",
|
||||
"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",
|
||||
"Translate this page": "Traduzir esta página",
|
||||
"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ê?",
|
||||
"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 for {{title}}": "Menu do chat para {{title}}",
|
||||
"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 for {{name}}": "Ações do membro para {{name}}",
|
||||
"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}}",
|
||||
"Allow personal spaces": "Permitir espaços pessoais",
|
||||
"Members can create their own personal space.": "Os membros podem criar seu próprio espaço pessoal.",
|
||||
"Toggle allow personal spaces": "Ativar/desativar permissão para espaços pessoais",
|
||||
"Create personal space": "Criar espaço pessoal",
|
||||
"Personal space": "Espaço pessoal",
|
||||
"{{name}}'s space": "Espaço de {{name}}",
|
||||
"Apply": "Aplicar",
|
||||
"Cells that aren't already a page reference will be cleared.": "As células que ainda não forem uma referência de página serão limpas.",
|
||||
"Cells that aren't a valid URL will be cleared.": "As células que não forem uma URL válida serão limpas.",
|
||||
"Cells that aren't a valid email address will be cleared.": "As células que não forem um endereço de e-mail válido serão limpas.",
|
||||
"Cells that can't be parsed as a date will be cleared.": "As células que não puderem ser interpretadas como uma data serão limpas.",
|
||||
"Cells that can't be parsed as a number will be cleared.": "As células que não puderem ser interpretadas como um número serão limpas.",
|
||||
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "As células serão convertidas (yes/true/1 se tornam marcadas; todo o resto se torna desmarcado ou limpo).",
|
||||
"Cells will be reinterpreted under the new type.": "As células serão reinterpretadas com o novo tipo.",
|
||||
"Cells will be replaced with a comma-separated list of file names.": "As células serão substituídas por uma lista de nomes de arquivos separados por vírgula.",
|
||||
"Cells will be replaced with a comma-separated list of option names.": "As células serão substituídas por uma lista de nomes de opções separados por vírgula.",
|
||||
"Cells will be replaced with the option name.": "As células serão substituídas pelo nome da opção.",
|
||||
"Cells will be replaced with the page title.": "As células serão substituídas pelo título da página.",
|
||||
"Cells will be replaced with the person's name.": "As células serão substituídas pelo nome da pessoa.",
|
||||
"Change type": "Alterar tipo",
|
||||
"Change type to {{label}}?": "Alterar tipo para {{label}}?",
|
||||
"Converting…": "Convertendo…",
|
||||
"Existing values become single-item lists. No data is lost.": "Os valores existentes se tornam listas de item único. Nenhum dado é perdido.",
|
||||
"Only the first selected item per row will be kept; the rest will be discarded.": "Apenas o primeiro item selecionado por linha será mantido; o restante será descartado.",
|
||||
"Previous record": "Registro anterior",
|
||||
"Next record": "Próximo registro",
|
||||
"Record actions": "Ações do registro",
|
||||
"Delete record": "Excluir registro",
|
||||
"Delete record?": "Excluir registro?",
|
||||
"This action cannot be undone.": "Esta ação não pode ser desfeita.",
|
||||
"to navigate": "para navegar",
|
||||
"to close": "para fechar",
|
||||
"Expand row {{number}}": "Expandir linha {{number}}",
|
||||
"Saving…": "Salvando…",
|
||||
"Read-only": "Somente leitura",
|
||||
"Loading…": "Carregando…",
|
||||
"Updated {{when}}": "Atualizado {{when}}",
|
||||
"Add property": "Adicionar propriedade",
|
||||
"Create property": "Criar propriedade",
|
||||
"Hide properties": "Ocultar propriedades",
|
||||
"Find a property type": "Encontrar um tipo de propriedade",
|
||||
"Properties": "Propriedades"
|
||||
"Removed page permission": "Permissão de página removida"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"Add members": "Добавить участников",
|
||||
"Add to groups": "Добавить в группы",
|
||||
"Add space members": "Добавить участников пространства",
|
||||
"Add to favorites": "Добавить в избранное",
|
||||
"Admin": "Администратор",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Вы уверены, что хотите удалить эту группу? Участники потеряют доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to delete this page?": "Вы уверены, что хотите удалить эту страницу?",
|
||||
@@ -41,28 +40,26 @@
|
||||
"Dark": "Темная",
|
||||
"Date": "Дата",
|
||||
"Delete": "Удалить",
|
||||
"Remove from page": "Удалить со страницы",
|
||||
"Base options": "Параметры базы",
|
||||
"Delete group": "Удалить группу",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Вы уверены, что хотите удалить эту страницу? Это удалит её дочерние страницы, а также историю страницы. Это действие необратимо.",
|
||||
"Description": "Описание",
|
||||
"Details": "Подробности",
|
||||
"e.g ACME": "например, ACME",
|
||||
"e.g ACME Inc": "например, ACME Inc",
|
||||
"e.g Developers": "например, Developers",
|
||||
"e.g Developers": "например, Разработчики",
|
||||
"e.g Group for developers": "например, Группа для разработчиков",
|
||||
"e.g product": "например, product",
|
||||
"e.g Product Team": "например, Команда продукта",
|
||||
"e.g Sales": "например, Sales",
|
||||
"e.g Space for product team": "например, Пространство для команды продукта",
|
||||
"e.g product": "например, продукт",
|
||||
"e.g Product Team": "например, Продуктовая команда",
|
||||
"e.g Sales": "например, Продажи",
|
||||
"e.g Space for product team": "например, Пространство для продуктовой команды",
|
||||
"e.g Space for sales team to collaborate": "например, Пространство для совместной работы команды продаж",
|
||||
"Edit": "Редактировать",
|
||||
"Read": "Чтение",
|
||||
"Read": "Читать",
|
||||
"Edit group": "Редактировать группу",
|
||||
"Email": "Электронная почта",
|
||||
"Enter a strong password": "Введите надёжный пароль",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Введите действительные адреса электронной почты, разделенные запятой или пробелом [макс: 50]",
|
||||
"enter valid emails addresses": "введите корректные адреса электронной почты",
|
||||
"enter valid emails addresses": "введите действительные адреса электронной почты",
|
||||
"Enter your current password": "Введите ваш текущий пароль",
|
||||
"enter your full name": "введите ваше полное имя",
|
||||
"Enter your new password": "Введите ваш новый пароль",
|
||||
@@ -73,32 +70,10 @@
|
||||
"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.": "Не удалось загрузить страницу. Произошла ошибка.",
|
||||
"Failed to update data": "Не удалось обновить данные",
|
||||
"Failed to create base": "Не удалось создать базу",
|
||||
"Failed to update base": "Не удалось обновить базу",
|
||||
"Failed to delete base": "Не удалось удалить базу",
|
||||
"Failed to create property": "Не удалось создать свойство",
|
||||
"Failed to update property": "Не удалось обновить свойство",
|
||||
"Failed to delete property": "Не удалось удалить свойство",
|
||||
"Failed to reorder property": "Не удалось изменить порядок свойств",
|
||||
"Failed to create view": "Не удалось создать представление",
|
||||
"Failed to update view": "Не удалось обновить представление",
|
||||
"Failed to delete view": "Не удалось удалить представление",
|
||||
"Failed to create row": "Не удалось создать строку",
|
||||
"Failed to update row": "Не удалось обновить строку",
|
||||
"Failed to delete row": "Не удалось удалить строку",
|
||||
"Failed to delete rows": "Не удалось удалить строки",
|
||||
"Failed to reorder row": "Не удалось изменить порядок строки",
|
||||
"Failed to move card": "Не удалось переместить карточку",
|
||||
"Failed to add card": "Не удалось добавить карточку",
|
||||
"Failed to export CSV": "Не удалось экспортировать CSV",
|
||||
"Favorite spaces": "Избранные пространства",
|
||||
"Favorite spaces appear here": "Здесь отображаются избранные пространства",
|
||||
"Favorites": "Избранное",
|
||||
"Full access": "Полный доступ",
|
||||
"Full page width": "Ширина на всю страницу",
|
||||
"Full width": "Во всю ширину",
|
||||
@@ -112,12 +87,11 @@
|
||||
"Import pages": "Импорт страниц",
|
||||
"Import pages & space settings": "Импорт страниц и настройки пространства",
|
||||
"Importing pages": "Импортирование страниц",
|
||||
"invalid invitation link": "недействительная ссылка-приглашение",
|
||||
"invalid invitation link": "ссылка на приглашение недействительна",
|
||||
"Invitation signup": "Регистрация по приглашению",
|
||||
"Invite by email": "Пригласить по электронной почте",
|
||||
"Invite members": "Пригласить участников",
|
||||
"Invite new members": "Пригласить новых участников",
|
||||
"Invite People": "Пригласить людей",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Приглашённые участники, которые ещё не приняли приглашение, появятся здесь.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Приглашённые участники получат доступ к пространствам, доступ к которым есть у группы",
|
||||
"Join the workspace": "Присоединиться к рабочей области",
|
||||
@@ -165,7 +139,6 @@
|
||||
"Profile": "Профиль",
|
||||
"Recently updated": "Обновлено недавно",
|
||||
"Remove": "Удалить",
|
||||
"Remove from favorites": "Удалить из избранного",
|
||||
"Remove group member": "Удалить участника группы",
|
||||
"Remove space member": "Удалить участника пространства",
|
||||
"Restore": "Восстановить",
|
||||
@@ -178,50 +151,49 @@
|
||||
"Search...": "Поиск...",
|
||||
"Select language": "Выберите язык",
|
||||
"Select role": "Выберите роль",
|
||||
"Select role to assign to all invited members": "Выберите роль, которую нужно назначить всем приглашённым участникам",
|
||||
"Select role to assign to all invited members": "Выберите роль для всех приглашённых участников",
|
||||
"Select theme": "Выберите тему",
|
||||
"Send invitation": "Отправить приглашение",
|
||||
"Invitation sent": "Приглашение отправлено",
|
||||
"Settings": "Настройки",
|
||||
"Setup workspace": "Настроить рабочее пространство",
|
||||
"Sign In": "Войти",
|
||||
"Sign Up": "Зарегистрироваться",
|
||||
"Slug": "Слаг",
|
||||
"Setup workspace": "Настроить рабочую область",
|
||||
"Sign In": "Вход",
|
||||
"Sign Up": "Регистрация",
|
||||
"Slug": "Slug",
|
||||
"Space": "Пространство",
|
||||
"Space description": "Описание пространства",
|
||||
"Space menu": "Меню пространства",
|
||||
"Space name": "Название пространства",
|
||||
"Space settings": "Настройки пространства",
|
||||
"Space slug": "Слаг пространства",
|
||||
"Space slug": "Slug пространства",
|
||||
"Spaces": "Пространства",
|
||||
"Spaces you belong to": "Пространства, в которых вы состоите",
|
||||
"No space found": "Пространство не найдено",
|
||||
"Spaces you belong to": "Пространства, к которым вы принадлежите",
|
||||
"No space found": "Пространства не найдены",
|
||||
"Search for spaces": "Поиск пространств",
|
||||
"Start typing to search...": "Начните вводить для поиска...",
|
||||
"Status": "Статус",
|
||||
"Successfully imported": "Успешно импортировано",
|
||||
"Successfully restored": "Успешно восстановлено",
|
||||
"System settings": "Системные настройки",
|
||||
"Templates": "Шаблоны",
|
||||
"Theme": "Тема",
|
||||
"To change your email, you have to enter your password and new email.": "Чтобы изменить электронную почту, вам нужно ввести пароль и новый адрес.",
|
||||
"Toggle full page width": "Переключить полную ширину страницы",
|
||||
"Toggle full page width": "Переключить ширину на всю страницу",
|
||||
"Unable to import pages. Please try again.": "Не удалось импортировать страницы. Пожалуйста, попробуйте ещё раз.",
|
||||
"untitled": "без названия",
|
||||
"Untitled": "Без названия",
|
||||
"Updated successfully": "Успешно обновлено",
|
||||
"Updated successfully": "Обновлено успешно",
|
||||
"User": "Пользователь",
|
||||
"Workspace": "Рабочее пространство",
|
||||
"Workspace Name": "Название рабочего пространства",
|
||||
"Workspace settings": "Настройки рабочего пространства",
|
||||
"Workspace": "Рабочая область",
|
||||
"Workspace Name": "Имя рабочей области",
|
||||
"Workspace settings": "Настройки рабочей области",
|
||||
"You can change your password here.": "Вы можете изменить свой пароль здесь.",
|
||||
"Your Email": "Ваш адрес электронной почты",
|
||||
"Your import is complete.": "Ваш импорт завершен.",
|
||||
"Your name": "Ваше имя",
|
||||
"Your Name": "Ваше имя",
|
||||
"Your Name": "Ваше Имя",
|
||||
"Your password": "Ваш пароль",
|
||||
"Your password must be a minimum of 8 characters.": "Ваш пароль должен содержать минимум 8 символов.",
|
||||
"Sidebar toggle": "Переключатель боковой панели",
|
||||
"Sidebar toggle": "Переключить боковую панель",
|
||||
"Comments": "Комментарии",
|
||||
"404 page not found": "404 страница не найдена",
|
||||
"Sorry, we can't find the page you are looking for.": "К сожалению, мы не можем найти страницу, которую вы ищете.",
|
||||
@@ -243,8 +215,6 @@
|
||||
"Edit comment": "Редактировать комментарий",
|
||||
"Delete comment": "Удалить комментарий",
|
||||
"Are you sure you want to delete this comment?": "Вы уверены, что хотите удалить этот комментарий?",
|
||||
"Delete chat": "Удалить чат",
|
||||
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Вы уверены, что хотите удалить '{{title}}'? Это действие нельзя отменить.",
|
||||
"Comment created successfully": "Комментарий успешно создан",
|
||||
"Error creating comment": "Ошибка при создании комментария",
|
||||
"Comment updated successfully": "Комментарий успешно обновлён",
|
||||
@@ -252,13 +222,13 @@
|
||||
"Comment deleted successfully": "Комментарий успешно удалён",
|
||||
"Failed to delete comment": "Не удалось удалить комментарий",
|
||||
"Comment resolved successfully": "Комментарий успешно разрешён",
|
||||
"Comment re-opened successfully": "Комментарий успешно открыт повторно",
|
||||
"Comment unresolved successfully": "Комментарий успешно переведён в нерешённые",
|
||||
"Comment re-opened successfully": "Комментарий успешно открыт заново",
|
||||
"Comment unresolved successfully": "Комментарий успешно размечен как нерешённый",
|
||||
"Failed to resolve comment": "Не удалось разрешить комментарий",
|
||||
"Resolve comment": "Решить комментарий",
|
||||
"Unresolve comment": "Снять статус решённого с комментария",
|
||||
"Resolve Comment Thread": "Решить ветку комментариев",
|
||||
"Unresolve Comment Thread": "Снять статус решённой с ветки комментариев",
|
||||
"Resolve comment": "Разрешить комментарий",
|
||||
"Unresolve comment": "Отметить комментарий как нерешённый",
|
||||
"Resolve Comment Thread": "Закрыть цепочку комментариев",
|
||||
"Unresolve Comment Thread": "Отметить цепочку комментариев как нерешённую",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Вы уверены, что хотите закрыть эту цепочку комментариев? Это пометит её как завершённую.",
|
||||
"Are you sure you want to unresolve this comment thread?": "Вы уверены, что хотите отметить эту цепочку комментариев как нерешённую?",
|
||||
"Resolved": "Решено",
|
||||
@@ -297,9 +267,6 @@
|
||||
"Align left": "По левому краю",
|
||||
"Align right": "По правому краю",
|
||||
"Align center": "По центру",
|
||||
"Alt text": "Альтернативный текст",
|
||||
"Describe this for accessibility.": "Опишите это для специальных возможностей.",
|
||||
"Add a description": "Добавить описание",
|
||||
"Justify": "По ширине",
|
||||
"Merge cells": "Объединить ячейки",
|
||||
"Split cell": "Разделить ячейку",
|
||||
@@ -310,19 +277,6 @@
|
||||
"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": "Успешно",
|
||||
@@ -335,11 +289,6 @@
|
||||
"Save & Exit": "Сохранить и выйти",
|
||||
"Double-click to edit Excalidraw diagram": "Кликните дважды для редактирования диаграммы Excalidraw",
|
||||
"Paste link": "Вставить ссылку",
|
||||
"Paste link or search pages": "Вставьте ссылку или найдите страницы",
|
||||
"Link to web page": "Ссылка на веб-страницу",
|
||||
"Recents": "Недавние",
|
||||
"Page or URL": "Страница или URL",
|
||||
"Link title": "Заголовок ссылки",
|
||||
"Edit link": "Редактировать ссылку",
|
||||
"Remove link": "Удалить ссылку",
|
||||
"Add link": "Добавить ссылку",
|
||||
@@ -358,7 +307,7 @@
|
||||
"Pink": "Розовый",
|
||||
"Gray": "Серый",
|
||||
"Embed link": "Встроенная ссылка",
|
||||
"Invalid {{provider}} embed link": "Недействительная ссылка для встраивания {{provider}}",
|
||||
"Invalid {{provider}} embed link": "Неверная ссылка для встраивания {{provider}}",
|
||||
"Embed {{provider}}": "Встроить {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Введите ссылку для встраивания {{provider}}",
|
||||
"Bold": "Жирный",
|
||||
@@ -385,11 +334,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.": "Загрузите любой аудиофайл с вашего устройства.",
|
||||
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
|
||||
"Uploading {{name}}": "Загрузка {{name}}",
|
||||
"Uploading file": "Загрузка файла",
|
||||
@@ -400,27 +346,19 @@
|
||||
"Divider": "Разделитель",
|
||||
"Quote": "Цитата",
|
||||
"Image": "Изображение",
|
||||
"Audio": "Аудио",
|
||||
"Embed PDF": "Встроить PDF",
|
||||
"Upload and embed a PDF file.": "Загрузите и встроите PDF-файл.",
|
||||
"Embed as PDF": "Встроить как PDF",
|
||||
"Failed to load PDF": "Не удалось загрузить PDF",
|
||||
"Convert to attachment": "Преобразовать в вложение",
|
||||
"File attachment": "Вложение файла",
|
||||
"File attachment": "Прикрепленный файл",
|
||||
"Toggle block": "Сворачиваемый блок",
|
||||
"Callout": "Выноска",
|
||||
"Insert callout notice.": "Вставить выноску с сообщением.",
|
||||
"Math inline": "Строчная формула",
|
||||
"Math inline": "Формула",
|
||||
"Insert inline math equation.": "Вставить математическое выражение в строку.",
|
||||
"Math block": "Блок формулы",
|
||||
"Insert math equation": "Вставить математическую формулу",
|
||||
"Math block": "Блок формул",
|
||||
"Insert math equation": "Вставить математическое выражение",
|
||||
"Mermaid diagram": "Диаграмма Mermaid",
|
||||
"Insert mermaid diagram": "Вставить диаграмму Mermaid",
|
||||
"Insert and design Drawio diagrams": "Вставляйте и редактируйте диаграммы Drawio",
|
||||
"Insert and design Drawio diagrams": "Вставить и рисовать диаграммы Draw.io",
|
||||
"Insert current date": "Вставить текущую дату",
|
||||
"Time": "Время",
|
||||
"Insert current time": "Вставить текущее время",
|
||||
"Draw and sketch excalidraw diagrams": "Рисуйте и создавайте диаграммы Excalidraw",
|
||||
"Draw and sketch excalidraw diagrams": "Вставить и рисовать диаграммы Excalidraw",
|
||||
"Multiple": "Несколько",
|
||||
"Turn into": "Преобразовать в",
|
||||
"Text align": "Выравнивание текста",
|
||||
@@ -428,15 +366,11 @@
|
||||
"Go to homepage": "Вернуться на главную",
|
||||
"Pages you create will show up here.": "Созданные вами страницы появятся здесь.",
|
||||
"Heading {{level}}": "Заголовок {{level}}",
|
||||
"Toggle title": "Заголовок сворачиваемого блока",
|
||||
"Write anything. Enter \"/\" for commands": "Пишите что угодно. Введите \"/\" для команд",
|
||||
"Toggle title": "Переключить заголовок",
|
||||
"Write anything. Enter \"/\" for commands": "Начните писать. Введите \"/\" для списка команд",
|
||||
"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": "Правая боковая панель",
|
||||
@@ -446,26 +380,24 @@
|
||||
"Names do not match": "Названия не совпадают",
|
||||
"Today, {{time}}": "Сегодня, {{time}}",
|
||||
"Yesterday, {{time}}": "Вчера, {{time}}",
|
||||
"now": "сейчас",
|
||||
"Space created successfully": "Пространство успешно создано",
|
||||
"Space updated successfully": "Пространство успешно обновлено",
|
||||
"Space deleted successfully": "Пространство успешно удалено",
|
||||
"Members added successfully": "Участники успешно добавлены",
|
||||
"Member removed successfully": "Участник успешно удалён",
|
||||
"Member removed successfully": "Участник успешно удален",
|
||||
"Member role updated successfully": "Роль участника успешно обновлена",
|
||||
"Created by: <b>{{creatorName}}</b>": "Создано: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Создано: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Изменено: {{name}} {{time}}",
|
||||
"Created by: <b>{{creatorName}}</b>": "Автор: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Дата создания: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Изменено {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Количество слов: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Количество символов: {{characterCount}}",
|
||||
"New update": "Новое обновление",
|
||||
"{{latestVersion}} is available": "Доступна версия {{latestVersion}}",
|
||||
"{{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": "Участник успешно удалён",
|
||||
"Member deleted successfully": "Участник успешно удален",
|
||||
"Are you sure you want to delete this workspace member? This action is irreversible.": "Вы уверены, что хотите удалить этого участника рабочей области? Это действие необратимо.",
|
||||
"Deactivate member": "Деактивировать участника",
|
||||
"Activate member": "Активировать участника",
|
||||
@@ -478,38 +410,36 @@
|
||||
"Move page": "Переместить страницу",
|
||||
"Move page to a different space.": "Переместите страницу в другое пространство.",
|
||||
"Real-time editor connection lost. Retrying...": "Соединение с редактором в реальном времени потеряно. Повторная попытка...",
|
||||
"Table of contents": "Оглавление",
|
||||
"Table of contents": "Содержание",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "Добавьте заголовки (H1, H2, H3), чтобы создать оглавление.",
|
||||
"Share": "Поделиться",
|
||||
"Public sharing": "Публичный доступ",
|
||||
"Public sharing": "Общий доступ",
|
||||
"Shared by": "Поделился",
|
||||
"Shared at": "Дата публикации",
|
||||
"Inherits public sharing from": "Наследует публичный доступ от",
|
||||
"Share to web": "Опубликовать в интернете",
|
||||
"Shared to web": "Опубликовано в интернете",
|
||||
"Anyone with the link can view this page": "Любой, у кого есть ссылка, может просматривать эту страницу",
|
||||
"Shared at": "Поделился в",
|
||||
"Inherits public sharing from": "Наследует общий доступ от",
|
||||
"Share to web": "Поделиться в интернете",
|
||||
"Shared to web": "Размещено в интернете",
|
||||
"Anyone with the link can view this page": "Любой, у кого есть ссылка, может просмотреть эту страницу",
|
||||
"Make this page publicly accessible": "Сделать эту страницу общедоступной",
|
||||
"Include sub-pages": "Включить подстраницы",
|
||||
"Make sub-pages public too": "Сделать подстраницы тоже общедоступными",
|
||||
"Make sub-pages public too": "Сделать подстраницы также общедоступными",
|
||||
"Allow search engines to index page": "Разрешить поисковым системам индексировать страницу",
|
||||
"Open page": "Открыть страницу",
|
||||
"Page": "Страница",
|
||||
"Delete public share link": "Удалить публичную ссылку",
|
||||
"Delete public share link": "Удалить ссылку на общий доступ",
|
||||
"Delete share": "Удалить общий доступ",
|
||||
"Are you sure you want to delete this shared link?": "Вы уверены, что хотите удалить эту ссылку общего доступа?",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Здесь будут отображаться публично опубликованные страницы из пространств, участником которых вы являетесь",
|
||||
"Share deleted successfully": "Общий доступ успешно удалён",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "Общие страницы из пространств, участником которых вы являетесь, появятся здесь",
|
||||
"Share deleted successfully": "Общий доступ успешно удален",
|
||||
"Share not found": "Общий доступ не найден",
|
||||
"Failed to share page": "Не удалось предоставить доступ к странице",
|
||||
"Failed to share page": "Не удалось поделиться страницей",
|
||||
"Disable public sharing": "Отключить общий доступ",
|
||||
"Prevent members from sharing pages publicly.": "Запретить участникам делиться страницами публично.",
|
||||
"Toggle public sharing": "Переключить общий доступ",
|
||||
"Toggle space public sharing": "Переключить общий доступ для пространства",
|
||||
"Allow viewers to comment": "Разрешить зрителям комментировать",
|
||||
"Allow viewers to add comments on pages in this space.": "Разрешить зрителям добавлять комментарии на страницах в этом пространстве.",
|
||||
"Toggle viewer comments": "Переключить комментарии зрителей",
|
||||
"Public sharing is disabled at the workspace level": "Общий доступ отключен на уровне рабочего пространства",
|
||||
"Prevent pages in this space from being shared publicly.": "Запретить делиться страницами в этом пространстве публично.",
|
||||
"Requires an enterprise license": "Требуется корпоративная лицензия",
|
||||
"Page permissions": "Права доступа к странице},{",
|
||||
"Control who can view and edit individual pages. Available with an enterprise license.": "Контролируйте, кто может просматривать и редактировать отдельные страницы. Доступно при наличии лицензии Enterprise.",
|
||||
"Enable public sharing": "Включить общий доступ",
|
||||
@@ -534,10 +464,9 @@
|
||||
"Replace (Enter)": "Заменить (Enter)",
|
||||
"Replace all (Ctrl+Alt+Enter)": "Заменить все (Ctrl+Alt+Enter)",
|
||||
"Replace all": "Заменить все",
|
||||
"View all": "Просмотреть все",
|
||||
"View all spaces": "Просмотреть все пространства",
|
||||
"Error": "Ошибка",
|
||||
"Failed to disable MFA": "Не удалось отключить MFA",
|
||||
"Failed to disable MFA": "Не удалось отключить двухфакторную аутентификацию",
|
||||
"Disable two-factor authentication": "Отключить двухфакторную аутентификацию",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Отключение двухфакторной аутентификации сделает вашу учетную запись менее безопасной. Для входа потребуется только пароль.",
|
||||
"Please enter your password to disable two-factor authentication:": "Пожалуйста, введите ваш пароль, чтобы отключить двухфакторную аутентификацию:",
|
||||
@@ -549,59 +478,59 @@
|
||||
"Add 2FA method": "Добавить метод 2FA",
|
||||
"Backup codes": "Резервные коды",
|
||||
"Disable": "Отключить",
|
||||
"Invalid verification code": "Недействительный код подтверждения",
|
||||
"New backup codes have been generated": "Новые резервные коды сгенерированы",
|
||||
"Failed to regenerate backup codes": "Не удалось заново сгенерировать резервные коды",
|
||||
"Invalid verification code": "Неверный код проверки",
|
||||
"New backup codes have been generated": "Созданы новые резервные коды",
|
||||
"Failed to regenerate backup codes": "Не удалось создать новые резервные коды",
|
||||
"About backup codes": "О резервных кодах",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Резервные коды можно использовать для доступа к вашей учетной записи, если вы потеряли доступ к приложению-аутентификатору. Каждый код можно использовать только один раз.",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Вы можете создать новые резервные коды в любое время. Это аннулирует все существующие коды.",
|
||||
"Confirm password": "Подтвердите пароль",
|
||||
"Generate new backup codes": "Сгенерировать новые резервные коды",
|
||||
"Generate new backup codes": "Создать новые резервные коды",
|
||||
"Save your new backup codes": "Сохраните ваши новые резервные коды",
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Убедитесь, что сохранили эти коды в безопасном месте. Ваши старые резервные коды больше недействительны.",
|
||||
"Your new backup codes": "Ваши новые резервные коды",
|
||||
"I've saved my backup codes": "Я сохранил резервные коды",
|
||||
"Failed to setup MFA": "Не удалось настроить MFA",
|
||||
"Setup & Verify": "Настроить и подтвердить",
|
||||
"Add to authenticator": "Добавить в приложение-аутентификатор",
|
||||
"1. Scan this QR code with your authenticator app": "1. Отсканируйте этот QR-код с помощью приложения-аутентификатора",
|
||||
"I've saved my backup codes": "Я сохранил(а) свои резервные коды",
|
||||
"Failed to setup MFA": "Не удалось настроить многофакторную аутентификацию",
|
||||
"Setup & Verify": "Настроить и проверить",
|
||||
"Add to authenticator": "Добавить в аутентификатор",
|
||||
"1. Scan this QR code with your authenticator app": "1. Отсканируйте этот QR-код с помощью вашего приложения-аутентификатора",
|
||||
"Can't scan the code?": "Не удается сканировать код?",
|
||||
"Enter this code manually in your authenticator app:": "Введите этот код вручную в приложении-аутентификаторе:",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. Введите 6-значный код из приложения-аутентификатора",
|
||||
"Verify and enable": "Подтвердить и включить",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. Введите 6-значный код из вашего аутентификатора",
|
||||
"Verify and enable": "Проверить и включить",
|
||||
"Failed to generate QR code. Please try again.": "Не удалось создать QR-код. Пожалуйста, попробуйте снова.",
|
||||
"Backup": "Резервный",
|
||||
"Backup": "Резервное копирование",
|
||||
"Save codes": "Сохранить коды",
|
||||
"Save your backup codes": "Сохраните резервные коды",
|
||||
"Save your backup codes": "Сохраните ваши резервные коды",
|
||||
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Эти коды можно использовать для доступа к вашей учетной записи, если вы потеряли доступ к приложению-аутентификатору. Каждый код можно использовать только один раз.",
|
||||
"Print": "Печать",
|
||||
"Two-factor authentication has been set up. Please log in again.": "Двухфакторная аутентификация настроена. Пожалуйста, войдите снова.",
|
||||
"Two-Factor authentication required": "Требуется двухфакторная аутентификация",
|
||||
"Your workspace requires two-factor authentication for all users": "Ваше рабочее пространство требует двухфакторную аутентификацию для всех пользователей",
|
||||
"Your workspace requires two-factor authentication for all users": "Ваше рабочее пространство требует двухфакторной аутентификации для всех пользователей",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Чтобы продолжать доступ к вашему рабочему пространству, вы должны настроить двухфакторную аутентификацию. Это добавляет дополнительный уровень безопасности к вашей учетной записи.",
|
||||
"Set up two-factor authentication": "Настроить двухфакторную аутентификацию",
|
||||
"Set up two-factor authentication": "Настройте двухфакторную аутентификацию",
|
||||
"Cancel and logout": "Отменить и выйти",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "Ваше рабочее пространство требует двухфакторной аутентификации. Пожалуйста, настройте её, чтобы продолжить.",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Это добавляет дополнительный уровень безопасности к вашей учетной записи, требуя код проверки из вашего приложения-аутентификатора.",
|
||||
"Password is required": "Требуется пароль",
|
||||
"Password must be at least 8 characters": "Пароль должен содержать не менее 8 символов",
|
||||
"Password must be at least 8 characters": "Пароль должен содержать как минимум 8 символов",
|
||||
"Please enter a 6-digit code": "Пожалуйста, введите 6-значный код",
|
||||
"Code must be exactly 6 digits": "Код должен состоять ровно из 6 цифр",
|
||||
"Code must be exactly 6 digits": "Код должен содержать ровно 6 цифр",
|
||||
"Enter the 6-digit code found in your authenticator app": "Введите 6-значный код из вашего приложения-аутентификатора",
|
||||
"Need help authenticating?": "Нужна помощь с аутентификацией?",
|
||||
"MFA QR Code": "QR-код MFA",
|
||||
"MFA QR Code": "QR-код двухфакторной аутентификации",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "Учетная запись успешно создана. Пожалуйста, войдите, чтобы настроить двухфакторную аутентификацию.",
|
||||
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Сброс пароля выполнен успешно. Пожалуйста, войдите с вашим новым паролем и завершите настройку двухфакторной аутентификации.",
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Сброс пароля выполнен успешно. Пожалуйста, войдите с вашим новым паролем, чтобы настроить двухфакторную аутентификацию.",
|
||||
"Password reset was successful. Please log in with your new password.": "Сброс пароля выполнен успешно. Пожалуйста, войдите с вашим новым паролем.",
|
||||
"Two-factor authentication": "Двухфакторная аутентификация",
|
||||
"Use authenticator app instead": "Использовать приложение-аутентификатор вместо этого",
|
||||
"Verify backup code": "Подтвердить резервный код",
|
||||
"Use authenticator app instead": "Используйте приложение-аутентификатор вместо этого",
|
||||
"Verify backup code": "Проверка резервного кода",
|
||||
"Use backup code": "Использовать резервный код",
|
||||
"Enter one of your backup codes": "Введите один из ваших резервных кодов",
|
||||
"Backup code": "Резервный код",
|
||||
"Enter one of your backup codes. Each backup code can only be used once.": "Введите один из ваших резервных кодов. Каждый резервный код можно использовать только один раз.",
|
||||
"Verify": "Подтвердить",
|
||||
"Verify": "Проверить",
|
||||
"Trash": "Корзина",
|
||||
"Pages in trash will be permanently deleted after {{count}} days.": "{count, plural, one {Страница в корзине будет окончательно удалена через # день.} few {Страницы в корзине будут окончательно удалены через # дня.} many {Страницы в корзине будут окончательно удалены через # дней.} other {Страницы в корзине будут окончательно удалены через # дней.}}",
|
||||
"Deleted": "Удалено",
|
||||
@@ -612,31 +541,27 @@
|
||||
"Move to trash": "Переместить в корзину",
|
||||
"Move this page to trash?": "Переместить эту страницу в корзину?",
|
||||
"Restore page": "Восстановить страницу",
|
||||
"Permanently delete": "Удалить навсегда",
|
||||
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> переместил(а) эту страницу в корзину {{time}}.",
|
||||
"Page moved to trash": "Страница перемещена в корзину",
|
||||
"Page restored successfully": "Страница успешно восстановлена",
|
||||
"Deleted by": "Удалено пользователем",
|
||||
"Deleted at": "Удалено",
|
||||
"Deleted at": "Удалено в",
|
||||
"Preview": "Предпросмотр",
|
||||
"Base preview unavailable": "Предпросмотр базы недоступен",
|
||||
"Restore this base to view its contents.": "Восстановите эту базу, чтобы просмотреть её содержимое.",
|
||||
"Subpages": "Подстраницы",
|
||||
"Failed to load subpages": "Не удалось загрузить подстраницы",
|
||||
"Failed to load subpages": "Не удалось загрузить под страницы",
|
||||
"No subpages": "Нет подстраниц",
|
||||
"Subpages (Child pages)": "Подстраницы (дочерние страницы)",
|
||||
"List all subpages of the current page": "Показать все подстраницы текущей страницы",
|
||||
"Subpages (Child pages)": "Подстраницы (вложенные страницы)",
|
||||
"List all subpages of the current page": "Показать все под страницы",
|
||||
"Attachments": "Вложения",
|
||||
"All spaces": "Все пространства",
|
||||
"Unknown": "Неизвестно",
|
||||
"Find a space": "Найти пространство",
|
||||
"Search in all your spaces": "Искать во всех ваших пространствах",
|
||||
"Search in all your spaces": "Поиск во всех ваших пространствах",
|
||||
"Type": "Тип",
|
||||
"Enterprise": "Корпоративный",
|
||||
"Enterprise": "Предприятие",
|
||||
"Download attachment": "Скачать вложение",
|
||||
"Allowed email domains": "Разрешённые домены электронной почты",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Только пользователи с адресами электронной почты из этих доменов могут зарегистрироваться через SSO.",
|
||||
"Enter valid domain names separated by comma or space": "Введите корректные доменные имена, разделённые запятой или пробелом",
|
||||
"Allowed email domains": "Разрешенные домены электронной почты",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "Только пользователи с электронными адресами из этих доменов могут зарегистрироваться через SSO.",
|
||||
"Enter valid domain names separated by comma or space": "Введите допустимые доменные имена, разделённые запятыми или пробелами",
|
||||
"Enforce two-factor authentication": "Обязательная двухфакторная аутентификация",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "После введения обязательности все участники должны будут включить двухфакторную аутентификацию для доступа к рабочему пространству.",
|
||||
"Toggle MFA enforcement": "Переключить обязательность MFA",
|
||||
@@ -645,13 +570,13 @@
|
||||
"Enabled": "Включено",
|
||||
"Advanced Settings": "Расширенные настройки",
|
||||
"Enable TLS/SSL": "Включить TLS/SSL",
|
||||
"Use secure connection to LDAP server": "Использовать защищённое подключение к серверу LDAP",
|
||||
"Group sync": "Синхронизация групп",
|
||||
"Use secure connection to LDAP server": "Использовать защищённое соединение с сервером LDAP",
|
||||
"Group sync": "Синхронизация группы",
|
||||
"No SSO providers found.": "Поставщики SSO не найдены.",
|
||||
"Delete SSO provider": "Удалить провайдера SSO",
|
||||
"Delete SSO provider": "Удалить поставщика SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Вы уверены, что хотите удалить этого поставщика SSO?",
|
||||
"Action": "Действие",
|
||||
"{{ssoProviderType}} configuration": "Конфигурация {{ssoProviderType}}",
|
||||
"{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}",
|
||||
"Icon": "Иконка",
|
||||
"Upload image": "Загрузить изображение",
|
||||
"Remove image": "Удалить изображение",
|
||||
@@ -659,21 +584,25 @@
|
||||
"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": "Обновить",
|
||||
"Update {{credential}}": "Обновить {{credential}}",
|
||||
"Update API key": "Обновить API ключ",
|
||||
"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-ключи. Существующие ключи участников продолжат работать.",
|
||||
@@ -684,7 +613,6 @@
|
||||
"AI Answer": "Ответ ИИ",
|
||||
"Ask AI": "Спросить ИИ",
|
||||
"AI is thinking...": "ИИ обрабатывает запрос...",
|
||||
"Thinking": "Думаю",
|
||||
"Ask a question...": "Задайте вопрос...",
|
||||
"AI Answers": "Ответы ИИ",
|
||||
"AI-powered search (AI Answers)": "Поиск на базе ИИ (Ответы ИИ)",
|
||||
@@ -693,9 +621,7 @@
|
||||
"Generative AI (Ask AI)": "Генеративный ИИ (Спросить ИИ)",
|
||||
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Включите создание контента на базе ИИ в редакторе. Позволяет пользователям генерировать, улучшать, переводить и преобразовывать текст.",
|
||||
"Toggle generative AI": "Переключить генеративный ИИ",
|
||||
"Upgrade your plan": "Обновите свой тарифный план",
|
||||
"Available with a paid license": "Доступно с платной лицензией",
|
||||
"Upgrade your license tier.": "Обновите уровень вашей лицензии.",
|
||||
"Enterprise feature": "Корпоративная функция",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ИИ доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
|
||||
"AI & MCP": "ИИ и MCP",
|
||||
"AI": "ИИ",
|
||||
@@ -703,15 +629,17 @@
|
||||
"Model Context Protocol (MCP)": "Протокол контекста модели (MCP)",
|
||||
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Включите сервер MCP, чтобы ИИ-ассистенты и инструменты могли взаимодействовать с содержимым вашего рабочего пространства.",
|
||||
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
|
||||
"MCP documentation": "Документация MCP",
|
||||
"MCP Server URL": "URL сервера MCP",
|
||||
"Use your API key for authentication. You can manage API keys in your account settings.": "Используйте ваш API-ключ для аутентификации. Управлять API-ключами можно в настройках аккаунта.",
|
||||
"Supported tools": "Поддерживаемые инструменты",
|
||||
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "В вашем рабочем пространстве включён MCP. Используйте свой API-ключ для подключения ИИ-ассистентов.",
|
||||
"MCP server URL:": "URL сервера MCP:",
|
||||
"Learn more": "Подробнее",
|
||||
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Управляйте API-ключами для всех пользователей в рабочем пространстве. Смотрите <anchor>документацию по API</anchor> для получения информации об использовании.",
|
||||
"View the <anchor>API documentation</anchor> for usage details.": "Смотрите <anchor>документацию по API</anchor> для получения информации об использовании.",
|
||||
"View the <anchor>MCP documentation</anchor>.": "Смотрите <anchor>документацию по MCP</anchor>.",
|
||||
"View the": "Просмотреть",
|
||||
"for usage details.": "для подробностей использования.",
|
||||
"for setup instructions.": "для инструкций по настройке.",
|
||||
"API documentation": "Документация API",
|
||||
"Sources": "Источники",
|
||||
"AI Answers not available for attachments": "Ответы ИИ недоступны для вложений",
|
||||
"No answer available": "Ответ недоступен",
|
||||
@@ -726,34 +654,12 @@
|
||||
"Mark all as read": "Отметить все как прочитанные",
|
||||
"Mark as read": "Отметить как прочитанное",
|
||||
"More options": "Больше возможностей",
|
||||
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> упомянул вас в комментарии",
|
||||
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> оставил комментарий на странице",
|
||||
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> отметил(а) комментарий как решённый",
|
||||
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> упомянул(а) вас на странице",
|
||||
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> предоставил(а) вам доступ на редактирование страницы",
|
||||
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> предоставил(а) вам доступ на просмотр страницы",
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> обновил(а) страницу",
|
||||
"Watch page": "Следить за страницей",
|
||||
"Stop watching": "Перестать следить",
|
||||
"Watch space": "Следить за пространством",
|
||||
"Stop watching space": "Перестать следить за пространством",
|
||||
"Email notifications": "Уведомления на email",
|
||||
"Page updates": "Обновления страницы",
|
||||
"Get notified when pages you watch are updated.": "Получайте уведомления, когда отслеживаемые вами страницы обновляются.",
|
||||
"Page mentions": "Упоминания на странице",
|
||||
"Get notified when someone mentions you on a page.": "Получайте уведомления, когда кто-то упоминает вас на странице.",
|
||||
"Comment mentions": "Упоминания в комментариях",
|
||||
"Get notified when someone mentions you in a comment.": "Получайте уведомления, когда кто-то упоминает вас в комментарии.",
|
||||
"New comments": "Новые комментарии",
|
||||
"Get notified about new comments on threads you participate in.": "Получайте уведомления о новых комментариях в цепочках, в которых вы участвуете.",
|
||||
"Resolved comments": "Разрешённые комментарии",
|
||||
"Get notified when your comment is resolved.": "Получайте уведомление, когда ваш комментарий разрешён.",
|
||||
"You are now watching this page": "Вы теперь следите за этой страницей",
|
||||
"You are no longer watching this page": "Вы больше не следите за этой страницей",
|
||||
"You are now watching this space": "Теперь вы следите за этим пространством",
|
||||
"You are no longer watching this space": "Вы больше не следите за этим пространством",
|
||||
"Direct": "Прямые",
|
||||
"Updates": "Обновления",
|
||||
"mentioned you in a comment": "упомянул вас в комментарии",
|
||||
"commented on a page": "прокомментировал на странице",
|
||||
"resolved a comment": "разрешил комментарий",
|
||||
"mentioned you on a page": "упомянул вас на странице",
|
||||
"gave you edit access to a page": "предоставил вам доступ на редактирование страницы",
|
||||
"gave you view access to a page": "предоставил вам доступ для просмотра страницы",
|
||||
"Today": "Сегодня",
|
||||
"Yesterday": "Вчера",
|
||||
"This week": "На этой неделе",
|
||||
@@ -787,369 +693,5 @@
|
||||
"Failed to update trash retention": "Не удалось обновить срок хранения корзины",
|
||||
"Removed page restriction": "Ограничение доступа к странице удалено",
|
||||
"Added page permission": "Добавлено разрешение доступа к странице",
|
||||
"Removed page permission": "Удалено разрешение доступа к странице",
|
||||
"day": "день",
|
||||
"days": "дни",
|
||||
"week": "неделя",
|
||||
"weeks": "недели",
|
||||
"month": "месяц",
|
||||
"months": "месяцы",
|
||||
"year": "год",
|
||||
"years": "годы",
|
||||
"Period": "Период",
|
||||
"Fixed date": "Фиксированная дата",
|
||||
"Indefinitely": "Бессрочно",
|
||||
"Days": "Дни",
|
||||
"Weeks": "Недели",
|
||||
"Months": "Месяцы",
|
||||
"Years": "Годы",
|
||||
"Pick a date": "Выберите дату",
|
||||
"Maximum is {{max}} {{unit}} for this unit": "Максимум для этой единицы измерения — {{max}} {{unit}}",
|
||||
"Never expires. Verifiers can re-verify at any time.": "Срок действия не истекает. Проверяющие могут повторно подтверждать в любое время.",
|
||||
"Verified": "Проверено",
|
||||
"Review needed": "Требуется проверка",
|
||||
"Verification expired": "Срок проверки истёк",
|
||||
"Draft": "Черновик",
|
||||
"In Approval": "На утверждении",
|
||||
"In approval": "На утверждении",
|
||||
"Approved": "Утверждено",
|
||||
"Obsolete": "Устарело",
|
||||
"Expiring": "Истекает",
|
||||
"Set up verification": "Настроить проверку",
|
||||
"Verify page": "Проверить страницу",
|
||||
"Page verification": "Проверка страницы",
|
||||
"Add verification": "Добавить проверку",
|
||||
"Edit verification": "Изменить проверку",
|
||||
"Search by title": "Поиск по заголовку",
|
||||
"Choose how this page should stay accurate.": "Выберите, как поддерживать актуальность этой страницы.",
|
||||
"Recurring verification": "Регулярная проверка",
|
||||
"Verifiers re-confirm this page on a schedule.": "Проверяющие повторно подтверждают эту страницу по расписанию.",
|
||||
"Re-verify on a schedule (e.g every 30 days )": "Повторно проверять по расписанию (например, каждые 30 дней)",
|
||||
"Page stays editable at all times": "Страница остаётся редактируемой в любое время",
|
||||
"Best for runbooks, FAQs, living documentation": "Лучше всего подходит для инструкций, FAQ и живой документации",
|
||||
"Approval workflow": "Процесс утверждения",
|
||||
"Formal document lifecycle with named approvers.": "Формальный жизненный цикл документа с назначенными утверждающими.",
|
||||
"Draft → In approval → Approved → Obsolete": "Черновик → На утверждении → Утверждено → Устарело",
|
||||
"Locked once approved, with full history": "После утверждения блокируется, с полной историей",
|
||||
"Designed for ISO 9001, ISO 13485, and FDA": "Разработано для ISO 9001, ISO 13485 и FDA",
|
||||
"Best for SOPs and controlled documents": "Лучше всего подходит для СОП и контролируемых документов",
|
||||
"Back": "Назад",
|
||||
"Quality management": "Управление качеством",
|
||||
"Recurring": "Регулярно",
|
||||
"Pages move through draft, approval, and approved stages.": "Страницы проходят стадии черновика, утверждения и утверждённого состояния.",
|
||||
"Verifiers": "Проверяющие",
|
||||
"Add verifier": "Добавить проверяющего",
|
||||
"I've reviewed this page for accuracy": "Я проверил(а) эту страницу на точность",
|
||||
"Set up": "Настроить",
|
||||
"Remove verification": "Удалить проверку",
|
||||
"Are you sure you want to remove verification from this page?": "Вы уверены, что хотите удалить проверку с этой страницы?",
|
||||
"Assigned verifiers must periodically re-verify this page.": "Назначенные проверяющие должны периодически повторно проверять эту страницу.",
|
||||
"Last verified by {{name}} {{time}} (expired)": "Последняя проверка: {{name}}, {{time}} (срок истёк)",
|
||||
"The fixed expiration date has passed.": "Фиксированная дата истечения срока уже прошла.",
|
||||
"Verified by {{name}} {{time}}": "Проверено: {{name}}, {{time}}",
|
||||
"Expires {{date}}": "Истекает {{date}}",
|
||||
"Expired {{date}}": "Срок истёк {{date}}",
|
||||
"Mark as obsolete": "Отметить как устаревшее",
|
||||
"Mark obsolete": "Отметить как устаревшее",
|
||||
"Returned by {{name}} {{time}}": "Возвращено: {{name}}, {{time}}",
|
||||
"No approval has been requested yet.": "Запрос на утверждение ещё не отправлен.",
|
||||
"Submitted by {{name}} {{time}}": "Отправлено: {{name}}, {{time}}",
|
||||
"Someone": "Кто-то",
|
||||
"Approved by {{name}} {{time}}": "Утверждено: {{name}}, {{time}}",
|
||||
"This document has been marked as obsolete.": "Этот документ был отмечен как устаревший.",
|
||||
"Rejection comment": "Комментарий к отклонению",
|
||||
"Reason for returning this document...": "Причина возврата этого документа...",
|
||||
"Confirm rejection": "Подтвердить отклонение",
|
||||
"Submit for approval": "Отправить на утверждение",
|
||||
"Reject": "Отклонить",
|
||||
"Approve": "Утвердить",
|
||||
"Re-submit for approval": "Повторно отправить на утверждение",
|
||||
"Verified until": "Проверено до",
|
||||
"QMS": "QMS",
|
||||
"Verified pages": "Проверенные страницы",
|
||||
"Search pages...": "Поиск страниц...",
|
||||
"Filter by space": "Фильтр по пространству",
|
||||
"Filter by type": "Фильтр по типу",
|
||||
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> проверил(а) страницу",
|
||||
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> отправил(а) страницу вам на утверждение",
|
||||
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> вернул(а) страницу на доработку",
|
||||
"Page verification expires soon": "Срок проверки страницы скоро истекает",
|
||||
"Page verification has expired": "Срок проверки страницы истёк",
|
||||
"Verifying your email": "Подтверждение вашего адреса электронной почты",
|
||||
"Please wait...": "Пожалуйста, подождите...",
|
||||
"Verification failed. The link may have expired.": "Ошибка проверки. Ссылка могла устареть.",
|
||||
"Check your email": "Проверьте вашу электронную почту",
|
||||
"We sent a verification link to {{email}}.": "Мы отправили ссылку для подтверждения на {{email}}.",
|
||||
"We sent a verification link to your email.": "Мы отправили ссылку для подтверждения на вашу электронную почту.",
|
||||
"Click the link to verify your email and access your workspace.": "Перейдите по ссылке, чтобы подтвердить электронную почту и получить доступ к рабочему пространству.",
|
||||
"Resend verification email": "Отправить письмо для подтверждения повторно",
|
||||
"Verification email sent. Please check your inbox.": "Письмо для подтверждения отправлено. Пожалуйста, проверьте ваш почтовый ящик.",
|
||||
"Failed to resend verification email. Please try again.": "Не удалось отправить письмо для подтверждения. Пожалуйста, попробуйте снова.",
|
||||
"We've sent you an email with your associated workspaces.": "Мы отправили вам электронное письмо с привязанными рабочими пространствами.",
|
||||
"Load more": "Загрузить ещё",
|
||||
"Log out of all devices": "Выйти на всех устройствах",
|
||||
"Log out of all sessions except this device": "Выйти из всех сеансов, кроме этого устройства",
|
||||
"This Device": "Это устройство",
|
||||
"Unknown device": "Неизвестное устройство",
|
||||
"No active sessions": "Нет активных сеансов",
|
||||
"Session revoked": "Сеанс отозван",
|
||||
"All other sessions revoked": "Все остальные сеансы отозваны",
|
||||
"Last used": "Последнее использование",
|
||||
"Created": "Создано",
|
||||
"Rename": "Переименовать",
|
||||
"Publish": "Опубликовать",
|
||||
"Security": "Безопасность",
|
||||
"Enforce SSO": "Сделать SSO обязательным",
|
||||
"Once enforced, members will not be able to login with email and password.": "После включения участники не смогут входить с помощью электронной почты и пароля.",
|
||||
"AI-generated content may not be accurate.": "Контент, созданный ИИ, может быть неточным.",
|
||||
"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.": "Не удалось отобразить это сообщение.",
|
||||
"How can I help you today?": "Чем я могу помочь вам сегодня?",
|
||||
"New chat": "Новый чат",
|
||||
"No chat history": "Нет истории чатов",
|
||||
"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": "Переключить чат с ИИ",
|
||||
"Translate this page": "Перевести эту страницу",
|
||||
"Try a different search term.": "Попробуйте другой поисковый запрос.",
|
||||
"Try again": "Попробовать снова",
|
||||
"Untitled chat": "Чат без названия",
|
||||
"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 for {{title}}": "Меню чата для {{title}}",
|
||||
"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 for {{name}}": "Действия с участниками для {{name}}",
|
||||
"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}}",
|
||||
"Allow personal spaces": "Разрешить личные пространства",
|
||||
"Members can create their own personal space.": "Участники могут создавать свои собственные личные пространства.",
|
||||
"Toggle allow personal spaces": "Переключить разрешение личных пространств",
|
||||
"Create personal space": "Создать личное пространство",
|
||||
"Personal space": "Личное пространство",
|
||||
"{{name}}'s space": "Пространство {{name}}",
|
||||
"Apply": "Применить",
|
||||
"Cells that aren't already a page reference will be cleared.": "Ячейки, которые ещё не являются ссылкой на страницу, будут очищены.",
|
||||
"Cells that aren't a valid URL will be cleared.": "Ячейки, которые не содержат допустимый URL, будут очищены.",
|
||||
"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 number will be cleared.": "Ячейки, которые нельзя распознать как число, будут очищены.",
|
||||
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Значения в ячейках будут приведены к типу (yes/true/1 станут отмеченными; всё остальное станет неотмеченным или будет очищено).",
|
||||
"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 option names.": "Ячейки будут заменены списком названий параметров, разделённых запятыми.",
|
||||
"Cells will be replaced with the option name.": "Ячейки будут заменены названием параметра.",
|
||||
"Cells will be replaced with the page title.": "Ячейки будут заменены заголовком страницы.",
|
||||
"Cells will be replaced with the person's name.": "Ячейки будут заменены именем человека.",
|
||||
"Change type": "Изменить тип",
|
||||
"Change type to {{label}}?": "Изменить тип на {{label}}?",
|
||||
"Converting…": "Преобразование…",
|
||||
"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.": "Для каждой строки будет сохранён только первый выбранный элемент; остальные будут отброшены.",
|
||||
"Previous record": "Предыдущая запись",
|
||||
"Next record": "Следующая запись",
|
||||
"Record actions": "Действия с записью",
|
||||
"Delete record": "Удалить запись",
|
||||
"Delete record?": "Удалить запись?",
|
||||
"This action cannot be undone.": "Это действие нельзя отменить.",
|
||||
"to navigate": "для навигации",
|
||||
"to close": "для закрытия",
|
||||
"Expand row {{number}}": "Развернуть строку {{number}}",
|
||||
"Saving…": "Сохранение…",
|
||||
"Read-only": "Только для чтения",
|
||||
"Loading…": "Загрузка…",
|
||||
"Updated {{when}}": "Обновлено {{when}}",
|
||||
"Add property": "Добавить свойство",
|
||||
"Create property": "Создать свойство",
|
||||
"Hide properties": "Скрыть свойства",
|
||||
"Find a property type": "Найти тип свойства",
|
||||
"Properties": "Свойства"
|
||||
"Removed page permission": "Удалено разрешение доступа к странице"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ import Security from "@/ee/security/pages/security.tsx";
|
||||
import License from "@/ee/licence/pages/license.tsx";
|
||||
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
||||
import SharedPage from "@/pages/share/shared-page.tsx";
|
||||
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
|
||||
import Shares from "@/pages/settings/shares/shares.tsx";
|
||||
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
||||
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
||||
@@ -38,15 +37,8 @@ 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";
|
||||
import TemplateEditor from "@/ee/template/pages/template-editor";
|
||||
import FavoritesPage from "@/pages/favorites/favorites-page";
|
||||
import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
|
||||
import VerifyEmail from "@/ee/pages/verify-email.tsx";
|
||||
import LabelPage from "@/pages/label/label-page";
|
||||
|
||||
export default function App() {
|
||||
const { t } = useTranslation();
|
||||
@@ -84,22 +76,12 @@ export default function App() {
|
||||
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path={"/pdf-render/:pageId"} element={<PdfRenderPage />} />
|
||||
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
|
||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||
|
||||
<Route element={<Layout />}>
|
||||
<Route path={"/home"} element={<Home />} />
|
||||
<Route path={"/ai"} element={<AiChat />} />
|
||||
<Route path={"/ai/chat/:chatId"} element={<AiChat />} />
|
||||
<Route path={"/spaces"} element={<SpacesPage />} />
|
||||
<Route path={"/favorites"} element={<FavoritesPage />} />
|
||||
<Route path={"/labels/:labelName"} element={<LabelPage />} />
|
||||
<Route path={"/templates"} element={<TemplateList />} />
|
||||
<Route
|
||||
path={"/templates/:templateId"}
|
||||
element={<TemplateEditor />}
|
||||
/>
|
||||
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
|
||||
<Route path={"/s/:spaceSlug/trash"} element={<SpaceTrash />} />
|
||||
<Route
|
||||
@@ -107,8 +89,6 @@ export default function App() {
|
||||
element={<Page />}
|
||||
/>
|
||||
|
||||
<Route path={"/base/:pageId"} element={<BasePage />} />
|
||||
|
||||
<Route path={"/settings"}>
|
||||
<Route path={"account/profile"} element={<AccountSettings />} />
|
||||
<Route
|
||||
@@ -127,7 +107,6 @@ export default function App() {
|
||||
<Route path={"ai"} element={<AiSettings />} />
|
||||
<Route path={"ai/mcp"} element={<AiSettings />} />
|
||||
<Route path={"audit"} element={<AuditLogs />} />
|
||||
<Route path={"verifications"} element={<VerifiedPages />} />
|
||||
{!isCloud() && <Route path={"license"} element={<License />} />}
|
||||
{isCloud() && <Route path={"billing"} element={<Billing />} />}
|
||||
</Route>
|
||||
|
||||
@@ -80,20 +80,6 @@ export default function AvatarUploader({
|
||||
}
|
||||
};
|
||||
|
||||
const actionLabel = {
|
||||
[AvatarIconType.AVATAR]: t("Change avatar"),
|
||||
[AvatarIconType.SPACE_ICON]: t("Change space icon"),
|
||||
[AvatarIconType.WORKSPACE_ICON]: t("Change workspace icon"),
|
||||
}[type];
|
||||
|
||||
// Per WCAG 2.5.3 (Label in Name), the accessible name must include the
|
||||
// visible text. When no image is set, the avatar renders the name's
|
||||
// initials, so prepend the name to the action label.
|
||||
const ariaLabel =
|
||||
!currentImageUrl && fallbackName
|
||||
? `${fallbackName} – ${actionLabel}`
|
||||
: actionLabel;
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (disabled) return;
|
||||
|
||||
@@ -118,8 +104,6 @@ export default function AvatarUploader({
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
aria-label={ariaLabel}
|
||||
tabIndex={-1}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
@@ -131,8 +115,6 @@ export default function AvatarUploader({
|
||||
size={size}
|
||||
avatarUrl={currentImageUrl}
|
||||
name={fallbackName}
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup="menu"
|
||||
style={{
|
||||
cursor: disabled || isLoading ? "default" : "pointer",
|
||||
opacity: isLoading ? 0.6 : 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionIcon, MantineColor, MantineSize, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
@@ -6,21 +6,15 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
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, label }: CopyProps) {
|
||||
export default function CopyTextButton({ text }: CopyProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const copyLabel = label ?? t("Copy");
|
||||
|
||||
return (
|
||||
<CopyButton value={text} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? t("Copied") : copyLabel}
|
||||
label={copied ? t("Copied") : t("Copy")}
|
||||
withArrow
|
||||
position="right"
|
||||
>
|
||||
@@ -28,8 +22,6 @@ export default function CopyTextButton({ text, size, label }: CopyProps) {
|
||||
color={copied ? "teal" : "gray"}
|
||||
variant="subtle"
|
||||
onClick={copy}
|
||||
size={size}
|
||||
aria-label={copied ? t("Copied") : copyLabel}
|
||||
>
|
||||
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
</ActionIcon>
|
||||
|
||||
@@ -6,21 +6,13 @@ import {
|
||||
Select,
|
||||
Switch,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
exportPage,
|
||||
exportPageToDocx,
|
||||
} from "@/features/page/services/page-service.ts";
|
||||
import { exportPage } 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;
|
||||
@@ -40,25 +32,17 @@ export default function ExportModal({
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
|
||||
const [isExporting, setIsExporting] = useState<boolean>(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") {
|
||||
if (format === ExportFormat.Docx) {
|
||||
await exportPageToDocx({ pageId: id });
|
||||
} else {
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
}
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
}
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
@@ -97,22 +81,17 @@ export default function ExportModal({
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title>
|
||||
<Modal.CloseButton aria-label={t("Close")} />
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection
|
||||
format={format}
|
||||
onChange={handleChange}
|
||||
includeDocx={type === "page"}
|
||||
docxEntitled={docxEntitled}
|
||||
/>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
</Group>
|
||||
|
||||
{type === "page" && !isDocx && (
|
||||
{type === "page" && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
@@ -164,16 +143,7 @@ export default function ExportModal({
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Tooltip label={upgradeLabel} disabled={!blockedByLicense} withArrow>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
loading={isExporting}
|
||||
disabled={blockedByLicense}
|
||||
data-disabled={blockedByLicense || undefined}
|
||||
>
|
||||
{t("Export")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
@@ -184,49 +154,23 @@ export default function ExportModal({
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
includeDocx?: boolean;
|
||||
docxEntitled?: boolean;
|
||||
}
|
||||
function ExportFormatSelection({
|
||||
format,
|
||||
onChange,
|
||||
includeDocx,
|
||||
docxEntitled,
|
||||
}: ExportFormatSelection) {
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = [
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
...(includeDocx
|
||||
? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={data}
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 140 }, option: { opacity: 1 } }}
|
||||
comboboxProps={{ width: 200 }}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label={t("Select export format")}
|
||||
renderOption={({ option }) =>
|
||||
option.value === "docx" && !docxEntitled ? (
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{option.label}
|
||||
</Text>
|
||||
<Badge size="xs" mt={4}>
|
||||
{t("Enterprise")}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="sm">{option.label}</Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { ThemeIcon } from "@mantine/core";
|
||||
import { IconFileDescription, IconTable } from "@tabler/icons-react";
|
||||
|
||||
type Props = {
|
||||
icon?: string | null;
|
||||
isBase?: boolean;
|
||||
};
|
||||
|
||||
export function PageListIcon({ icon, isBase }: Props) {
|
||||
if (icon) {
|
||||
return <>{icon}</>;
|
||||
}
|
||||
return (
|
||||
<ThemeIcon variant="transparent" color="gray" size={18}>
|
||||
{isBase ? <IconTable size={18} /> : <IconFileDescription size={18} />}
|
||||
</ThemeIcon>
|
||||
);
|
||||
}
|
||||
@@ -4,20 +4,18 @@ import {
|
||||
UnstyledButton,
|
||||
Badge,
|
||||
Table,
|
||||
Button,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
|
||||
import { buildPageUrl, getPageTitle } from "@/features/page/page.utils.ts";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { formattedDate } from "@/lib/time.ts";
|
||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { PageListIcon } from "@/components/common/page-list-icon";
|
||||
import { IconFiles } from "@tabler/icons-react";
|
||||
import { IconFileDescription, IconFiles } from "@tabler/icons-react";
|
||||
import { EmptyState } from "@/components/ui/empty-state.tsx";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getInitialsColor } from "@/lib/get-initials-color.ts";
|
||||
import rowClasses from "@/components/ui/clickable-table-row.module.css";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
@@ -25,8 +23,7 @@ interface Props {
|
||||
|
||||
export default function RecentChanges({ spaceId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isError, hasNextPage, fetchNextPage, isFetchingNextPage } = useRecentChangesQuery(spaceId);
|
||||
const pages = data?.pages.flatMap((p) => p.items) ?? [];
|
||||
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
@@ -36,69 +33,58 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
return <Text>{t("Failed to fetch recent pages")}</Text>;
|
||||
}
|
||||
|
||||
return pages.length > 0 ? (
|
||||
<>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{pages.map((page) => (
|
||||
<Table.Tr key={page.id} className={rowClasses.row}>
|
||||
<Table.Td>
|
||||
<UnstyledButton
|
||||
className={rowClasses.link}
|
||||
component={Link}
|
||||
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
<PageListIcon icon={page.icon} isBase={page.isBase} />
|
||||
return pages && pages.items.length > 0 ? (
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{pages.items.map((page) => (
|
||||
<Table.Tr key={page.id}>
|
||||
<Table.Td>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
{page.icon || (
|
||||
<ActionIcon variant="transparent" color="gray" size={18}>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{getPageTitle(page.title, page.isBase, t)}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
{!spaceId && (
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={getInitialsColor(page?.space.name)}
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
{!spaceId && (
|
||||
<Table.Td>
|
||||
<Text
|
||||
c="dimmed"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
size="xs"
|
||||
fw={500}
|
||||
<Badge
|
||||
color={getInitialsColor(page?.space.name)}
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
mb="xl"
|
||||
onClick={() => fetchNextPage()}
|
||||
loading={isFetchingNextPage}
|
||||
>
|
||||
{t("Load more")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Text
|
||||
c="dimmed"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
size="xs"
|
||||
fw={500}
|
||||
>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={IconFiles}
|
||||
|
||||
@@ -6,14 +6,12 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface SearchInputProps {
|
||||
placeholder?: string;
|
||||
ariaLabel?: string;
|
||||
debounceDelay?: number;
|
||||
onSearch: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchInput({
|
||||
placeholder,
|
||||
ariaLabel,
|
||||
debounceDelay = 500,
|
||||
onSearch,
|
||||
}: SearchInputProps) {
|
||||
@@ -30,7 +28,6 @@ export function SearchInput({
|
||||
<TextInput
|
||||
size="sm"
|
||||
placeholder={placeholder || t("Search...")}
|
||||
aria-label={ariaLabel || placeholder || t("Search")}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ThemeIcon } from "@mantine/core";
|
||||
import { ActionIcon, rem } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
export function IconGroupCircle() {
|
||||
return (
|
||||
<ThemeIcon variant="light" size="lg" color="gray" radius="xl">
|
||||
<ActionIcon variant="light" size="lg" color="gray" radius="xl">
|
||||
<IconUsersGroup stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,19 +7,6 @@
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.brandIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
@@ -29,9 +16,6 @@
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Group,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Badge, Group, Text, Tooltip } from "@mantine/core";
|
||||
import classes from "./app-header.module.css";
|
||||
import React from "react";
|
||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { IconSparkles } from "@tabler/icons-react";
|
||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
@@ -33,11 +23,8 @@ import {
|
||||
shareSearchSpotlight,
|
||||
} from "@/features/search/constants.ts";
|
||||
import { NotificationPopover } from "@/features/notification/components/notification-popover.tsx";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
const links = [
|
||||
{ link: APP_ROUTE.HOME, label: "Home" },
|
||||
];
|
||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||
|
||||
export function AppHeader() {
|
||||
const { t } = useTranslation();
|
||||
@@ -47,12 +34,10 @@ export function AppHeader() {
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
|
||||
const { isTrial, trialDaysLeft } = useTrial();
|
||||
const location = useLocation();
|
||||
const toggleAside = useToggleAside();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
|
||||
|
||||
const isPageRoute = location.pathname.includes("/p/");
|
||||
const isHomeRoute = location.pathname.startsWith("/home");
|
||||
const isSpacesRoute = location.pathname === "/spaces";
|
||||
const hideSidebar = isHomeRoute || isSpacesRoute;
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Link key={link.label} to={link.link} className={classes.link}>
|
||||
@@ -64,44 +49,39 @@ export function AppHeader() {
|
||||
<>
|
||||
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
|
||||
<Group wrap="nowrap">
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
{!hideSidebar && (
|
||||
<>
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Link to="/home" className={classes.brand} aria-label="Docmost">
|
||||
<Box hiddenFrom="sm" className={classes.brandIcon}>
|
||||
<img
|
||||
src="/icons/favicon-32x32.png"
|
||||
alt="Docmost"
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
size="lg"
|
||||
fw={600}
|
||||
style={{ userSelect: "none" }}
|
||||
visibleFrom="sm"
|
||||
>
|
||||
Docmost
|
||||
</Text>
|
||||
</Link>
|
||||
<Text
|
||||
size="lg"
|
||||
fw={600}
|
||||
style={{ cursor: "pointer", userSelect: "none" }}
|
||||
component={Link}
|
||||
to="/home"
|
||||
>
|
||||
Docmost
|
||||
</Text>
|
||||
|
||||
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm">
|
||||
{items}
|
||||
@@ -118,49 +98,6 @@ export function AppHeader() {
|
||||
</div>
|
||||
|
||||
<Group px={"xl"} wrap="nowrap">
|
||||
{aiChatEnabled && (
|
||||
<>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to="/ai"
|
||||
className={classes.link}
|
||||
visibleFrom="sm"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
|
||||
return;
|
||||
}
|
||||
if (isPageRoute) {
|
||||
e.preventDefault();
|
||||
toggleAside("chat");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("AI Chat")}
|
||||
</UnstyledButton>
|
||||
<Tooltip label={t("AI Chat")} openDelay={250} withArrow>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to="/ai"
|
||||
variant="subtle"
|
||||
color="dark"
|
||||
size="sm"
|
||||
hiddenFrom="sm"
|
||||
aria-label={t("AI Chat")}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
|
||||
return;
|
||||
}
|
||||
if (isPageRoute) {
|
||||
e.preventDefault();
|
||||
toggleAside("chat");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconSparkles size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<NotificationPopover />
|
||||
{isCloud() && isTrial && trialDaysLeft !== 0 && (
|
||||
<Badge
|
||||
|
||||
@@ -27,3 +27,5 @@
|
||||
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { Box, ScrollArea, Text } from "@mantine/core";
|
||||
import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode, useEffect } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
|
||||
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
|
||||
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
const pageEditor = useAtomValue(pageEditorAtom);
|
||||
const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false }));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAsideOpen) return;
|
||||
document.getElementById(ASIDE_PANEL_ID)?.focus();
|
||||
}, [isAsideOpen, tab]);
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
@@ -35,14 +25,6 @@ export default function Aside() {
|
||||
component = <TableOfContents editor={pageEditor} />;
|
||||
title = "Table of contents";
|
||||
break;
|
||||
case "chat":
|
||||
component = <AsideChatPanel />;
|
||||
title = "AI Chat";
|
||||
break;
|
||||
case "details":
|
||||
component = <PageDetailsAside />;
|
||||
title = "Details";
|
||||
break;
|
||||
default:
|
||||
component = null;
|
||||
title = null;
|
||||
@@ -52,24 +34,12 @@ export default function Aside() {
|
||||
<Box p="md" style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{component && (
|
||||
<>
|
||||
{tab !== "chat" && (
|
||||
<Group justify="space-between" wrap="nowrap" mb="md">
|
||||
<Title order={2} size="h6" fw={500}>{t(title)}</Title>
|
||||
<Tooltip label={t("Close")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={closeAside}
|
||||
aria-label={t("Close")}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
<Text mb="md" fw={500}>
|
||||
{t(title)}
|
||||
</Text>
|
||||
|
||||
{tab === "comments" || tab === "chat" ? (
|
||||
component
|
||||
{tab === "comments" ? (
|
||||
<CommentListWithTabs />
|
||||
) : (
|
||||
<ScrollArea
|
||||
style={{ height: "85vh" }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
@@ -11,27 +10,22 @@ import {
|
||||
sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx";
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
import Aside from "@/components/layouts/global/aside.tsx";
|
||||
import classes from "./app-shell.module.css";
|
||||
import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import GlobalSidebar from "@/components/layouts/global/global-sidebar.tsx";
|
||||
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
|
||||
import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx";
|
||||
|
||||
export default function GlobalAppShell({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
useTrialEndAction();
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const [{ isAsideOpen, tab: asideTab }] = useAtom(asideStateAtom);
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef(null);
|
||||
@@ -78,23 +72,24 @@ export default function GlobalAppShell({
|
||||
const location = useLocation();
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
const isSpaceRoute = location.pathname.startsWith("/s/");
|
||||
const isAiRoute = location.pathname.startsWith("/ai");
|
||||
const isHomeRoute = location.pathname.startsWith("/home");
|
||||
const isSpacesRoute = location.pathname === "/spaces";
|
||||
const isPageRoute = location.pathname.includes("/p/");
|
||||
const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute;
|
||||
const hideSidebar = isHomeRoute || isSpacesRoute;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkipToMain />
|
||||
<AppShell
|
||||
<AppShell
|
||||
header={{ height: 45 }}
|
||||
navbar={{
|
||||
width: isSpaceRoute ? sidebarWidth : 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
desktop: !desktopOpened,
|
||||
},
|
||||
}}
|
||||
navbar={
|
||||
!hideSidebar && {
|
||||
width: isSpaceRoute ? sidebarWidth : 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
desktop: !desktopOpened,
|
||||
},
|
||||
}
|
||||
}
|
||||
aside={
|
||||
isPageRoute && {
|
||||
width: 350,
|
||||
@@ -107,61 +102,30 @@ export default function GlobalAppShell({
|
||||
<AppShell.Header px="md" className={classes.header}>
|
||||
<AppHeader />
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar
|
||||
className={classes.navbar}
|
||||
withBorder={false}
|
||||
ref={sidebarRef}
|
||||
aria-label={
|
||||
isSpaceRoute
|
||||
? t("Space navigation")
|
||||
: isSettingsRoute
|
||||
? t("Settings navigation")
|
||||
: isAiRoute
|
||||
? t("AI navigation")
|
||||
: t("Main navigation")
|
||||
}
|
||||
>
|
||||
{isSpaceRoute && (
|
||||
{!hideSidebar && (
|
||||
<AppShell.Navbar
|
||||
className={classes.navbar}
|
||||
withBorder={false}
|
||||
ref={sidebarRef}
|
||||
>
|
||||
<div className={classes.resizeHandle} onMouseDown={startResizing} />
|
||||
)}
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
{isAiRoute && <AiChatSidebar />}
|
||||
{showGlobalSidebar && <GlobalSidebar />}
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}>
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
</AppShell.Navbar>
|
||||
)}
|
||||
<AppShell.Main>
|
||||
{isSettingsRoute ? (
|
||||
<Container size={900} pb={80}>
|
||||
{children}
|
||||
</Container>
|
||||
<Container size={850}>{children}</Container>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</AppShell.Main>
|
||||
|
||||
{isPageRoute && (
|
||||
<AppShell.Aside
|
||||
id={ASIDE_PANEL_ID}
|
||||
tabIndex={-1}
|
||||
className={classes.aside}
|
||||
p="md"
|
||||
withBorder={false}
|
||||
aria-label={
|
||||
asideTab === "comments"
|
||||
? t("Comments")
|
||||
: asideTab === "toc"
|
||||
? t("Table of contents")
|
||||
: asideTab === "chat"
|
||||
? t("AI Chat")
|
||||
: asideTab === "details"
|
||||
? t("Details")
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<AppShell.Aside className={classes.aside} p="md" withBorder={false}>
|
||||
<Aside />
|
||||
</AppShell.Aside>
|
||||
)}
|
||||
</AppShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
.navbar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
padding-left: var(--mantine-spacing-xs);
|
||||
min-height: 30px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
& :hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
@mixin hover {
|
||||
background-color: transparent;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
margin-right: var(--mantine-spacing-sm);
|
||||
width: rem(16px);
|
||||
height: rem(16px);
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: var(--mantine-color-dimmed);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bottomSection {
|
||||
padding-top: var(--mantine-spacing-xs);
|
||||
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.spaceItem {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--mantine-spacing-sm);
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
padding-left: var(--mantine-spacing-xs);
|
||||
min-height: 30px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ScrollArea, Text, Divider, Modal, UnstyledButton, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconHome,
|
||||
IconClock,
|
||||
IconStar,
|
||||
IconLayoutGrid,
|
||||
IconSettings,
|
||||
IconUserPlus,
|
||||
IconTemplate,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import classes from "./global-sidebar.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAtom } from "jotai";
|
||||
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar";
|
||||
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
|
||||
import { getSpaceUrl } from "@/lib/config";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function GlobalSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState(location.pathname);
|
||||
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
|
||||
const hasTemplates = useHasFeature(Feature.TEMPLATES);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const mainNavItems = [
|
||||
{ label: "Home", icon: IconHome, path: "/home" },
|
||||
{ label: "Favorites", icon: IconStar, path: "/favorites" },
|
||||
{ label: "Spaces", icon: IconLayoutGrid, path: "/spaces" },
|
||||
{
|
||||
label: "Templates",
|
||||
icon: IconTemplate,
|
||||
path: "/templates",
|
||||
disabled: !hasTemplates,
|
||||
},
|
||||
];
|
||||
const { data: favoriteSpacesData, isPending: isFavoritesPending } = useFavoritesQuery("space");
|
||||
const favoriteSpaces = favoriteSpacesData?.pages.flatMap((p) => p.items) ?? [];
|
||||
const sortedFavoriteSpaces = [...favoriteSpaces]
|
||||
.filter((fav) => fav.space)
|
||||
.sort((a, b) => {
|
||||
const cmp = (a.space!.name ?? "").localeCompare(b.space!.name ?? "", undefined, { sensitivity: "base" });
|
||||
return cmp !== 0 ? cmp : a.id.localeCompare(b.id);
|
||||
});
|
||||
const [inviteOpened, { open: openInvite, close: closeInvite }] =
|
||||
useDisclosure(false);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleNavClick = () => {
|
||||
if (mobileSidebarOpened) {
|
||||
toggleMobileSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.navbar}>
|
||||
<ScrollArea w="100%" style={{ flex: 1 }}>
|
||||
<div className={classes.section}>
|
||||
{mainNavItems.map((item) =>
|
||||
item.disabled ? (
|
||||
<Tooltip
|
||||
key={item.label}
|
||||
label={upgradeLabel}
|
||||
position="right"
|
||||
withArrow
|
||||
>
|
||||
<UnstyledButton
|
||||
className={classes.link}
|
||||
data-disabled
|
||||
aria-disabled="true"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Link
|
||||
key={item.label}
|
||||
className={classes.link}
|
||||
data-active={active === item.path || undefined}
|
||||
aria-current={active === item.path ? "page" : undefined}
|
||||
to={item.path}
|
||||
onClick={handleNavClick}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider my="xs" />
|
||||
<div className={classes.section}>
|
||||
<Text component="h2" className={classes.sectionHeader}>{t("Favorite spaces")}</Text>
|
||||
{!isFavoritesPending && sortedFavoriteSpaces.length === 0 ? (
|
||||
<Text size="xs" c="dimmed" pl="xs" py={4}>
|
||||
{t("Favorite spaces appear here")}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{sortedFavoriteSpaces.slice(0, 10).map((fav) => (
|
||||
<Link
|
||||
key={fav.id}
|
||||
className={classes.spaceItem}
|
||||
to={getSpaceUrl(fav.space!.slug)}
|
||||
onClick={handleNavClick}
|
||||
>
|
||||
<CustomAvatar
|
||||
name={fav.space!.name}
|
||||
avatarUrl={fav.space!.logo}
|
||||
type={AvatarIconType.SPACE_ICON}
|
||||
color="initials"
|
||||
variant="filled"
|
||||
size={20}
|
||||
/>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{fav.space!.name}
|
||||
</Text>
|
||||
</Link>
|
||||
))}
|
||||
{sortedFavoriteSpaces.length > 10 && (
|
||||
<Link
|
||||
className={classes.spaceItem}
|
||||
to="/spaces"
|
||||
onClick={handleNavClick}
|
||||
>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("View all")}
|
||||
</Text>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</ScrollArea>
|
||||
|
||||
<div className={classes.bottomSection}>
|
||||
<UnstyledButton
|
||||
className={classes.link}
|
||||
onClick={openInvite}
|
||||
>
|
||||
<IconUserPlus className={classes.linkIcon} stroke={2} />
|
||||
<span>{t("Invite People")}</span>
|
||||
</UnstyledButton>
|
||||
<Link
|
||||
className={classes.link}
|
||||
data-active={active.startsWith("/settings") || undefined}
|
||||
aria-current={active.startsWith("/settings") ? "page" : undefined}
|
||||
to="/settings/account/profile"
|
||||
onClick={handleNavClick}
|
||||
>
|
||||
<IconSettings className={classes.linkIcon} stroke={2} />
|
||||
<span>{t("Settings")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
size="550"
|
||||
opened={inviteOpened}
|
||||
onClose={closeInvite}
|
||||
title={t("Invite new members")}
|
||||
centered
|
||||
>
|
||||
<Divider size="xs" mb="xs" />
|
||||
<ScrollArea h="80%">
|
||||
<WorkspaceInviteForm onClose={closeInvite} />
|
||||
</ScrollArea>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export const desktopSidebarAtom = atomWithWebStorage<boolean>(
|
||||
|
||||
export const desktopAsideAtom = atom<boolean>(false);
|
||||
|
||||
// Valid `tab` values: "" | "comments" | "toc" | "chat" | "details"
|
||||
type AsideStateType = {
|
||||
tab: string;
|
||||
isAsideOpen: boolean;
|
||||
|
||||
@@ -15,16 +15,9 @@ import {
|
||||
IconMoon,
|
||||
IconSettings,
|
||||
IconSun,
|
||||
IconUser,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { usePersonalSpaceQuery } from "@/ee/personal-space/queries/personal-space-query";
|
||||
import CreatePersonalSpaceModal from "@/ee/personal-space/components/create-personal-space-modal";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -43,20 +36,11 @@ export default function TopMenu() {
|
||||
const user = currentUser?.user;
|
||||
const workspace = currentUser?.workspace;
|
||||
|
||||
const hasPersonalSpaces = useHasFeature(Feature.PERSONAL_SPACES);
|
||||
const settingEnabled = workspace?.settings?.spaces?.allowPersonal === true;
|
||||
const { data: personalSpace } = usePersonalSpaceQuery(hasPersonalSpaces);
|
||||
const [
|
||||
createOpened,
|
||||
{ open: openCreate, close: closeCreate },
|
||||
] = useDisclosure(false);
|
||||
|
||||
if (!user || !workspace) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu width={250} position="bottom-end" withArrow shadow={"lg"}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
@@ -131,26 +115,6 @@ export default function TopMenu() {
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
{personalSpace ? (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={getSpaceUrl(personalSpace.slug)}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Personal space")}
|
||||
</Menu.Item>
|
||||
) : (
|
||||
hasPersonalSpaces &&
|
||||
settingEnabled && (
|
||||
<Menu.Item
|
||||
onClick={openCreate}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Create personal space")}
|
||||
</Menu.Item>
|
||||
)
|
||||
)}
|
||||
|
||||
<Menu.Sub>
|
||||
<Menu.Sub.Target>
|
||||
<Menu.Sub.Item leftSection={<IconBrightnessFilled size={16} />}>
|
||||
@@ -196,8 +160,5 @@ export default function TopMenu() {
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<CreatePersonalSpaceModal opened={createOpened} onClose={closeCreate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import { getSsoProviders } from "@/ee/security/services/security-service.ts";
|
||||
import { getShares } from "@/features/share/services/share-service.ts";
|
||||
import { getApiKeys } from "@/ee/api-key";
|
||||
import { getAuditLogs } from "@/ee/audit/services/audit-service";
|
||||
import { getVerificationList } from "@/ee/page-verification/services/page-verification-service";
|
||||
import { getScimTokens } from "@/ee/scim/services/scim-token-service";
|
||||
|
||||
export const prefetchWorkspaceMembers = () => {
|
||||
const params: QueryParams = { limit: 100, query: "" };
|
||||
@@ -91,18 +89,3 @@ export const prefetchAuditLogs = () => {
|
||||
queryFn: () => getAuditLogs(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchVerifiedPages = () => {
|
||||
const params = { limit: 50 };
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["verification-list", params],
|
||||
queryFn: () => getVerificationList(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchScimTokens = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["scim-token-list", { cursor: undefined }],
|
||||
queryFn: () => getScimTokens({}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
IconWorld,
|
||||
IconSparkles,
|
||||
IconHistory,
|
||||
IconShieldCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
@@ -31,13 +30,11 @@ import {
|
||||
prefetchBilling,
|
||||
prefetchGroups,
|
||||
prefetchLicense,
|
||||
prefetchScimTokens,
|
||||
prefetchShares,
|
||||
prefetchSpaces,
|
||||
prefetchSsoProviders,
|
||||
prefetchWorkspaceMembers,
|
||||
prefetchAuditLogs,
|
||||
prefetchVerifiedPages,
|
||||
} from "@/components/settings/settings-queries.tsx";
|
||||
import AppVersion from "@/components/settings/app-version.tsx";
|
||||
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
@@ -98,12 +95,6 @@ const groupedData: DataGroup[] = [
|
||||
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
|
||||
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
|
||||
{ label: "Public sharing", icon: IconWorld, path: "/settings/sharing" },
|
||||
{
|
||||
label: "Verified pages",
|
||||
icon: IconShieldCheck,
|
||||
path: "/settings/verifications",
|
||||
feature: Feature.PAGE_VERIFICATION,
|
||||
},
|
||||
{
|
||||
label: "API management",
|
||||
icon: IconKey,
|
||||
@@ -205,10 +196,7 @@ export default function SettingsSidebar() {
|
||||
}
|
||||
break;
|
||||
case "Security & SSO":
|
||||
prefetchHandler = () => {
|
||||
prefetchSsoProviders();
|
||||
prefetchScimTokens();
|
||||
};
|
||||
prefetchHandler = prefetchSsoProviders;
|
||||
break;
|
||||
case "Public sharing":
|
||||
prefetchHandler = prefetchShares;
|
||||
@@ -222,14 +210,37 @@ export default function SettingsSidebar() {
|
||||
case "Audit log":
|
||||
prefetchHandler = prefetchAuditLogs;
|
||||
break;
|
||||
case "Verified pages":
|
||||
prefetchHandler = prefetchVerifiedPages;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const isDisabled = isItemDisabled(item);
|
||||
const linkElement = (
|
||||
<Link
|
||||
onMouseEnter={!isDisabled ? prefetchHandler : undefined}
|
||||
className={classes.link}
|
||||
data-active={active.startsWith(item.path) || undefined}
|
||||
data-disabled={isDisabled || undefined}
|
||||
key={item.label}
|
||||
to={isDisabled ? "#" : item.path}
|
||||
onClick={(e) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (mobileSidebarOpened) {
|
||||
toggleMobileSidebar();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
if (isDisabled) {
|
||||
return (
|
||||
@@ -239,41 +250,12 @@ export default function SettingsSidebar() {
|
||||
position="right"
|
||||
withArrow
|
||||
>
|
||||
<span
|
||||
className={classes.link}
|
||||
data-disabled
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
cursor: "not-allowed",
|
||||
}}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</span>
|
||||
{linkElement}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
onMouseEnter={prefetchHandler}
|
||||
className={classes.link}
|
||||
data-active={active.startsWith(item.path) || undefined}
|
||||
key={item.label}
|
||||
to={item.path}
|
||||
onClick={() => {
|
||||
if (mobileSidebarOpened) {
|
||||
toggleMobileSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
);
|
||||
return linkElement;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -291,7 +273,7 @@ export default function SettingsSidebar() {
|
||||
}}
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
aria-label={t("Back")}
|
||||
aria-label="Back"
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Divider, Title } from '@mantine/core';
|
||||
export default function SettingsTitle({ title }: { title: string }) {
|
||||
return (
|
||||
<>
|
||||
<Title order={1} size="h3">
|
||||
<Title order={3}>
|
||||
{title}
|
||||
</Title>
|
||||
<Divider my="md" />
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
gap: var(--mantine-spacing-md);
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
.track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.track > * {
|
||||
scroll-snap-align: start;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease, background-color 120ms ease, transform 120ms ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.root:hover .arrow.visible,
|
||||
.arrow.visible:focus-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.arrow:active {
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.arrowLeft {
|
||||
left: -14px;
|
||||
}
|
||||
|
||||
.arrowRight {
|
||||
right: -14px;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "./card-carousel.module.css";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export default function CardCarousel({ children, ariaLabel }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const el = trackRef.current;
|
||||
if (!el) return;
|
||||
const maxScroll = el.scrollWidth - el.clientWidth;
|
||||
setCanScrollLeft(el.scrollLeft > 1);
|
||||
setCanScrollRight(el.scrollLeft < maxScroll - 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateScrollState();
|
||||
const el = trackRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new ResizeObserver(updateScrollState);
|
||||
observer.observe(el);
|
||||
for (const child of Array.from(el.children)) {
|
||||
observer.observe(child);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [updateScrollState, children]);
|
||||
|
||||
const scrollBy = (direction: 1 | -1) => {
|
||||
const el = trackRef.current;
|
||||
if (!el) return;
|
||||
el.scrollBy({ left: direction * el.clientWidth * 0.85, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div
|
||||
ref={trackRef}
|
||||
className={classes.track}
|
||||
onScroll={updateScrollState}
|
||||
{...(ariaLabel ? { role: "region", "aria-label": ariaLabel } : {})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`${classes.arrow} ${classes.arrowLeft} ${canScrollLeft ? classes.visible : ""}`}
|
||||
onClick={() => scrollBy(-1)}
|
||||
aria-label={t("Scroll left")}
|
||||
tabIndex={canScrollLeft ? 0 : -1}
|
||||
>
|
||||
<IconChevronLeft size={18} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`${classes.arrow} ${classes.arrowRight} ${canScrollRight ? classes.visible : ""}`}
|
||||
onClick={() => scrollBy(1)}
|
||||
aria-label={t("Scroll right")}
|
||||
tabIndex={canScrollRight ? 0 : -1}
|
||||
>
|
||||
<IconChevronRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Focus styling for list-style tables (recent changes, favorites, all
|
||||
* spaces, groups, verified pages, shares).
|
||||
*
|
||||
* Per WAI-ARIA Authoring Practices and Adrian Roselli's guidance on table
|
||||
* accessibility (https://adrianroselli.com/2020/02/block-links-cards-clickable-regions-etc.html),
|
||||
* data tables should not be made fully clickable. Only the title cell is the
|
||||
* link, and that link is what receives Tab focus.
|
||||
*
|
||||
* - `.row` adds a subtle background tint when the row contains the focused
|
||||
* element, so keyboard users can see which row they're inspecting.
|
||||
* - `.link` adds a visible :focus-visible outline on the title link itself.
|
||||
*
|
||||
* No stretched-link pseudo here on purpose: absolutely-positioned pseudos
|
||||
* inside table cells cause column reflow on focus in Chromium.
|
||||
*/
|
||||
|
||||
.row:focus-within {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
|
||||
.link:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Avatar, MantineColor } from "@mantine/core";
|
||||
import { Avatar } from "@mantine/core";
|
||||
import { getAvatarUrl } from "@/lib/config.ts";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||
|
||||
@@ -16,68 +16,19 @@ interface CustomAvatarProps {
|
||||
mt?: string | number;
|
||||
}
|
||||
|
||||
// color.shade picks whose FILLED variant (white text on the shade) meets WCAG AA 4.5:1.
|
||||
// Avoids lime/yellow/green/orange, too light even at dark shades.
|
||||
// For non-filled variants, initials text is forced to the .9 shade at render time:
|
||||
// Mantine otherwise caps light-variant placeholder text at .6, dropping contrast to ~3:1.
|
||||
const SAFE_INITIALS_COLORS: MantineColor[] = [
|
||||
"blue.8",
|
||||
"cyan.9",
|
||||
"grape.9",
|
||||
"indigo.8",
|
||||
"pink.8",
|
||||
"red.8",
|
||||
"violet.7",
|
||||
];
|
||||
|
||||
function hashName(input: string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < input.length; i += 1) {
|
||||
hash = (hash << 5) - hash + input.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
function pickInitialsColor(name: string) {
|
||||
return SAFE_INITIALS_COLORS[hashName(name) % SAFE_INITIALS_COLORS.length];
|
||||
}
|
||||
|
||||
function sanitizeInitialsSource(name: string) {
|
||||
const sanitized = name.replace(/[^\p{L}\p{N}\s]/gu, " ").trim();
|
||||
return sanitized || name;
|
||||
}
|
||||
|
||||
export const CustomAvatar = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
CustomAvatarProps
|
||||
>(({ avatarUrl, name, type, color, variant, ...props }: CustomAvatarProps, ref) => {
|
||||
>(({ avatarUrl, name, type, ...props }: CustomAvatarProps, ref) => {
|
||||
const avatarLink = getAvatarUrl(avatarUrl, type);
|
||||
const isInitials = !color || color === "initials";
|
||||
const pickedColor = isInitials ? pickInitialsColor(name ?? "") : color;
|
||||
const hue = pickedColor.split(".")[0];
|
||||
const initialsSource = sanitizeInitialsSource(name ?? "");
|
||||
|
||||
const resolvedColor = variant === "filled" ? pickedColor : hue;
|
||||
|
||||
const placeholderStyles =
|
||||
isInitials && variant !== "filled"
|
||||
? {
|
||||
placeholder: {
|
||||
color: `var(--mantine-color-${hue}-9)`,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
ref={ref}
|
||||
src={avatarLink}
|
||||
name={initialsSource}
|
||||
name={name}
|
||||
alt={name}
|
||||
color={resolvedColor}
|
||||
variant={variant}
|
||||
styles={placeholderStyles}
|
||||
color="initials"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Button, Group, Divider } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DestinationPicker } from "./destination-picker";
|
||||
import {
|
||||
DestinationPickerModalProps,
|
||||
DestinationSelection,
|
||||
} from "./destination-picker.types";
|
||||
|
||||
export function DestinationPickerModal({
|
||||
opened,
|
||||
onClose,
|
||||
title,
|
||||
actionLabel,
|
||||
onSelect,
|
||||
loading,
|
||||
excludePageId,
|
||||
pageLimit,
|
||||
initialSpaceId,
|
||||
searchSpacesOnly,
|
||||
}: DestinationPickerModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selection, setSelection] = useState<DestinationSelection | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
setSelection(null);
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<Modal.Root
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
size={550}
|
||||
padding="lg"
|
||||
yOffset="10vh"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>{title}</Modal.Title>
|
||||
<Modal.CloseButton aria-label={t("Close")} />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DestinationPicker
|
||||
onSelectionChange={setSelection}
|
||||
excludePageId={excludePageId}
|
||||
pageLimit={pageLimit}
|
||||
initialSpaceId={initialSpaceId}
|
||||
searchSpacesOnly={searchSpacesOnly}
|
||||
/>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
{t("Close")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => selection && onSelect(selection)}
|
||||
disabled={!selection}
|
||||
loading={loading}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
.searchInput {
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.scrollArea {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
transition: background-color 150ms ease;
|
||||
user-select: none;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-blue-0),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
border-left: 2px solid var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
.spaceRow {
|
||||
composes: row;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pageRow {
|
||||
composes: row;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
flex-shrink: 0;
|
||||
transition: transform 150ms ease;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.chevronExpanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
cursor: pointer;
|
||||
|
||||
@mixin hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
}
|
||||
|
||||
.searchResult {
|
||||
composes: row;
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
}
|
||||
|
||||
.spaceName {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { ActionIcon, TextInput, ScrollArea, Loader } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconSearch, IconFileDescription } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
|
||||
import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
import { IPage } from "@/features/page/types/page.types";
|
||||
import { DestinationSelection } from "./destination-picker.types";
|
||||
import { SpaceRow } from "./space-row";
|
||||
import classes from "./destination-picker.module.css";
|
||||
|
||||
type DestinationPickerProps = {
|
||||
onSelectionChange: (selection: DestinationSelection | null) => void;
|
||||
excludePageId?: string;
|
||||
pageLimit?: number;
|
||||
initialSpaceId?: string;
|
||||
searchSpacesOnly?: boolean;
|
||||
};
|
||||
|
||||
export function DestinationPicker({
|
||||
onSelectionChange,
|
||||
excludePageId,
|
||||
pageLimit = 15,
|
||||
initialSpaceId,
|
||||
searchSpacesOnly,
|
||||
}: DestinationPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selection, setSelection] = useState<DestinationSelection | null>(null);
|
||||
const [debouncedQuery] = useDebouncedValue(searchQuery, 300);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: spacesData, isLoading: spacesLoading } = useGetSpacesQuery({
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const searchEnabled =
|
||||
!searchSpacesOnly && debouncedQuery && debouncedQuery.length >= 2;
|
||||
|
||||
const { data: searchData, isLoading: searchLoading } =
|
||||
useSearchSuggestionsQuery({
|
||||
query: searchEnabled ? debouncedQuery : "",
|
||||
includePages: true,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const isSearching = !!searchEnabled;
|
||||
|
||||
const filteredSpaces = useMemo(() => {
|
||||
const items = spacesData?.items ?? [];
|
||||
if (!searchSpacesOnly || !debouncedQuery) return items;
|
||||
const fold = (s: string) =>
|
||||
s
|
||||
.normalize("NFD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.toLocaleLowerCase();
|
||||
const term = fold(debouncedQuery);
|
||||
return items.filter((s) => fold(s.name).includes(term));
|
||||
}, [spacesData, searchSpacesOnly, debouncedQuery]);
|
||||
|
||||
const selectedId =
|
||||
selection?.type === "space" ? selection.spaceId : selection?.pageId ?? null;
|
||||
|
||||
const updateSelection = useCallback(
|
||||
(next: DestinationSelection | null) => {
|
||||
setSelection(next);
|
||||
onSelectionChange(next);
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const handleSearchResultClick = (page: Partial<IPage>) => {
|
||||
if (!page.space || !page.id) return;
|
||||
|
||||
updateSelection({
|
||||
type: "page",
|
||||
spaceId: page.space.id,
|
||||
pageId: page.id,
|
||||
page,
|
||||
space: page.space,
|
||||
});
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const handleSelectSpace = useCallback(
|
||||
(space: ISpace) => {
|
||||
updateSelection({ type: "space", spaceId: space.id, space });
|
||||
},
|
||||
[updateSelection],
|
||||
);
|
||||
|
||||
const handleSelectPage = useCallback(
|
||||
(page: Partial<IPage>, space: ISpace) => {
|
||||
if (!page.id) return;
|
||||
updateSelection({
|
||||
type: "page",
|
||||
spaceId: page.spaceId ?? space.id,
|
||||
pageId: page.id,
|
||||
page,
|
||||
space,
|
||||
});
|
||||
},
|
||||
[updateSelection],
|
||||
);
|
||||
|
||||
// Pre-select space when initialSpaceId is set and spaces have loaded.
|
||||
// Only runs once: skip if user has already made a selection.
|
||||
useEffect(() => {
|
||||
if (!initialSpaceId || selection) return;
|
||||
const match = spacesData?.items?.find((s) => s.id === initialSpaceId);
|
||||
if (match) {
|
||||
updateSelection({ type: "space", spaceId: match.id, space: match });
|
||||
requestAnimationFrame(() => {
|
||||
const el = viewportRef.current?.querySelector<HTMLElement>(
|
||||
`[data-space-id="${match.id}"]`,
|
||||
);
|
||||
el?.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
}, [initialSpaceId, selection, spacesData, updateSelection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
leftSection={<IconSearch size={16} />}
|
||||
placeholder={
|
||||
searchSpacesOnly
|
||||
? t("Search spaces...")
|
||||
: t("Search pages and spaces...")
|
||||
}
|
||||
aria-label={
|
||||
searchSpacesOnly
|
||||
? t("Search spaces...")
|
||||
: t("Search pages and spaces...")
|
||||
}
|
||||
variant="filled"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
className={classes.searchInput}
|
||||
/>
|
||||
|
||||
<ScrollArea
|
||||
h="50vh"
|
||||
offsetScrollbars
|
||||
className={classes.scrollArea}
|
||||
viewportRef={viewportRef}
|
||||
>
|
||||
{isSearching ? (
|
||||
searchLoading ? (
|
||||
<div className={classes.emptyState}>
|
||||
<Loader size="xs" />
|
||||
</div>
|
||||
) : searchData?.pages && searchData.pages.length > 0 ? (
|
||||
searchData.pages.map(
|
||||
(page) =>
|
||||
page && (
|
||||
<div
|
||||
key={page.id}
|
||||
className={classes.searchResult}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSearchResultClick(page)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSearchResultClick(page);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={classes.iconWrapper}>
|
||||
{page.icon ? (
|
||||
page.icon
|
||||
) : (
|
||||
<ActionIcon
|
||||
component="div"
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
size={22}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.pageTitle}>
|
||||
{page.title || t("Untitled")}
|
||||
</div>
|
||||
{page.space && (
|
||||
<div className={classes.spaceName}>
|
||||
{page.space.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<div className={classes.emptyState}>{t("No results found")}</div>
|
||||
)
|
||||
) : spacesLoading ? (
|
||||
<div className={classes.emptyState}>
|
||||
<Loader size="xs" />
|
||||
</div>
|
||||
) : filteredSpaces.length === 0 ? (
|
||||
<div className={classes.emptyState}>
|
||||
{searchSpacesOnly && debouncedQuery
|
||||
? t("No spaces found")
|
||||
: t("No results found")}
|
||||
</div>
|
||||
) : (
|
||||
filteredSpaces.map((space) => (
|
||||
<SpaceRow
|
||||
key={space.id}
|
||||
space={space}
|
||||
limit={pageLimit}
|
||||
selectedId={selectedId}
|
||||
excludePageId={excludePageId}
|
||||
onSelectSpace={handleSelectSpace}
|
||||
onSelectPage={handleSelectPage}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
import { IPage } from "@/features/page/types/page.types";
|
||||
|
||||
export type DestinationSelection =
|
||||
| { type: "space"; spaceId: string; space: ISpace }
|
||||
| {
|
||||
type: "page";
|
||||
spaceId: string;
|
||||
pageId: string;
|
||||
page: Partial<IPage>;
|
||||
space: Partial<ISpace>;
|
||||
};
|
||||
|
||||
export type DestinationPickerModalProps = {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
actionLabel: string;
|
||||
onSelect: (selection: DestinationSelection) => void | Promise<void>;
|
||||
loading?: boolean;
|
||||
excludePageId?: string;
|
||||
pageLimit?: number;
|
||||
initialSpaceId?: string;
|
||||
searchSpacesOnly?: boolean;
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { Loader } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getSidebarPages } from "@/features/page/services/page-service";
|
||||
import { IPage } from "@/features/page/types/page.types";
|
||||
import { IPagination } from "@/lib/types";
|
||||
import { PageRow } from "./page-row";
|
||||
import classes from "./destination-picker.module.css";
|
||||
|
||||
type PageChildrenProps = {
|
||||
spaceId: string;
|
||||
pageId?: string;
|
||||
depth: number;
|
||||
limit: number;
|
||||
selectedId: string | null;
|
||||
excludePageId?: string;
|
||||
onSelectPage: (page: Partial<IPage>) => void;
|
||||
};
|
||||
|
||||
export function PageChildren({
|
||||
spaceId,
|
||||
pageId,
|
||||
depth,
|
||||
limit,
|
||||
selectedId,
|
||||
excludePageId,
|
||||
onSelectPage,
|
||||
}: PageChildrenProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({
|
||||
queryKey: ["destination-pages", spaceId, pageId ?? "root"],
|
||||
queryFn: ({ pageParam }) =>
|
||||
getSidebarPages({
|
||||
spaceId,
|
||||
pageId,
|
||||
limit,
|
||||
cursor: pageParam,
|
||||
}),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage: IPagination<IPage>) =>
|
||||
lastPage.meta?.nextCursor ?? undefined,
|
||||
});
|
||||
|
||||
const pages = data?.pages.flatMap((page) => page.items) ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={classes.emptyState}>
|
||||
<Loader size="xs" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
return (
|
||||
<div className={classes.emptyState}>
|
||||
{pageId ? t("No pages inside") : t("No pages in this space")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pages.map((page) => (
|
||||
<PageRow
|
||||
key={page.id}
|
||||
page={page}
|
||||
depth={depth}
|
||||
limit={limit}
|
||||
selectedId={selectedId}
|
||||
excludePageId={excludePageId}
|
||||
onSelect={onSelectPage}
|
||||
/>
|
||||
))}
|
||||
{hasNextPage && (
|
||||
<div
|
||||
className={classes.loadMore}
|
||||
onClick={() => fetchNextPage()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
fetchNextPage();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{t("Load more")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { KeyboardEvent, useState } from "react";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconChevronRight, IconFileDescription } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IPage } from "@/features/page/types/page.types";
|
||||
import { getPageTitle } from "@/features/page/page.utils";
|
||||
import { PageChildren } from "./page-children";
|
||||
import classes from "./destination-picker.module.css";
|
||||
|
||||
type PageRowProps = {
|
||||
page: Partial<IPage>;
|
||||
depth: number;
|
||||
limit: number;
|
||||
selectedId: string | null;
|
||||
excludePageId?: string;
|
||||
onSelect: (page: Partial<IPage>) => void;
|
||||
};
|
||||
|
||||
export function PageRow({
|
||||
page,
|
||||
depth,
|
||||
limit,
|
||||
selectedId,
|
||||
excludePageId,
|
||||
onSelect,
|
||||
}: PageRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const isExcluded = page.id === excludePageId;
|
||||
const isSelected = page.id === selectedId;
|
||||
|
||||
const rowClasses = [
|
||||
classes.pageRow,
|
||||
isSelected && classes.selected,
|
||||
isExcluded && classes.disabled,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const handleSelect = () => {
|
||||
if (!isExcluded) onSelect(page);
|
||||
};
|
||||
|
||||
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={rowClasses}
|
||||
style={{ paddingLeft: depth * 20 + 12 }}
|
||||
role="button"
|
||||
tabIndex={isExcluded ? -1 : 0}
|
||||
aria-disabled={isExcluded || undefined}
|
||||
onClick={handleSelect}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
>
|
||||
{page.hasChildren ? (
|
||||
<ActionIcon
|
||||
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
aria-label={expanded ? t("Collapse") : t("Expand")}
|
||||
aria-expanded={expanded}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<IconChevronRight size={14} />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<div style={{ width: 20, flexShrink: 0 }} />
|
||||
)}
|
||||
|
||||
<div className={classes.iconWrapper}>
|
||||
{page.icon ? (
|
||||
page.icon
|
||||
) : (
|
||||
<ActionIcon
|
||||
component="div"
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
size={22}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classes.pageTitle}>
|
||||
{getPageTitle(page.title, page.isBase, t)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && page.hasChildren && (
|
||||
<PageChildren
|
||||
spaceId={page.spaceId}
|
||||
pageId={page.id}
|
||||
depth={depth + 1}
|
||||
limit={limit}
|
||||
selectedId={selectedId}
|
||||
excludePageId={excludePageId}
|
||||
onSelectPage={onSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { KeyboardEvent, useState } from "react";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconChevronRight, IconLock } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
import { IPage } from "@/features/page/types/page.types";
|
||||
import { SpaceRole } from "@/lib/types";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
|
||||
import { PageChildren } from "./page-children";
|
||||
import classes from "./destination-picker.module.css";
|
||||
|
||||
type SpaceRowProps = {
|
||||
space: ISpace;
|
||||
limit: number;
|
||||
selectedId: string | null;
|
||||
excludePageId?: string;
|
||||
onSelectSpace: (space: ISpace) => void;
|
||||
onSelectPage: (page: Partial<IPage>, space: ISpace) => void;
|
||||
};
|
||||
|
||||
export function SpaceRow({
|
||||
space,
|
||||
limit,
|
||||
selectedId,
|
||||
excludePageId,
|
||||
onSelectSpace,
|
||||
onSelectPage,
|
||||
}: SpaceRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const writable =
|
||||
!!space.membership?.role && space.membership.role !== SpaceRole.READER;
|
||||
const isSelected = space.id === selectedId;
|
||||
|
||||
const rowClasses = [
|
||||
classes.spaceRow,
|
||||
isSelected && classes.selected,
|
||||
!writable && classes.disabled,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const handleSelect = () => {
|
||||
if (writable) onSelectSpace(space);
|
||||
};
|
||||
|
||||
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
};
|
||||
|
||||
const rowContent = (
|
||||
<div
|
||||
className={rowClasses}
|
||||
data-space-id={space.id}
|
||||
role="button"
|
||||
tabIndex={writable ? 0 : -1}
|
||||
aria-disabled={!writable || undefined}
|
||||
onClick={handleSelect}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
>
|
||||
{writable ? (
|
||||
<ActionIcon
|
||||
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
aria-label={expanded ? t("Collapse") : t("Expand")}
|
||||
aria-expanded={expanded}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<IconChevronRight size={14} />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<div style={{ width: 20, flexShrink: 0 }} />
|
||||
)}
|
||||
|
||||
<CustomAvatar
|
||||
name={space.name}
|
||||
avatarUrl={space.logo}
|
||||
type={AvatarIconType.SPACE_ICON}
|
||||
size={22}
|
||||
/>
|
||||
|
||||
<div className={classes.pageTitle}>{space.name}</div>
|
||||
|
||||
{!writable && (
|
||||
<IconLock
|
||||
size={14}
|
||||
color="var(--mantine-color-gray-5)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{writable ? (
|
||||
rowContent
|
||||
) : (
|
||||
<Tooltip
|
||||
label={t("You don't have permission to create pages here")}
|
||||
position="right"
|
||||
withArrow
|
||||
>
|
||||
<div>{rowContent}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{expanded && writable && (
|
||||
<PageChildren
|
||||
spaceId={space.id}
|
||||
depth={1}
|
||||
limit={limit}
|
||||
selectedId={selectedId}
|
||||
excludePageId={excludePageId}
|
||||
onSelectPage={(page) => onSelectPage(page, space)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Popover,
|
||||
@@ -7,24 +7,9 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
|
||||
import { Suspense } from "react";
|
||||
const Picker = React.lazy(() => import("@emoji-mart/react"));
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Load the picker module AND the emoji data in parallel inside the lazy
|
||||
// resolution, then bind the data into the component. React.lazy only finishes
|
||||
// suspending once both are in memory, so the Suspense boundary hides the
|
||||
// Remove button until the Picker can render with real content.
|
||||
const Picker = React.lazy(async () => {
|
||||
const [pickerModule, dataModule] = await Promise.all([
|
||||
import("@slidoapp/emoji-mart-react"),
|
||||
import("@slidoapp/emoji-mart-data"),
|
||||
]);
|
||||
const PickerComp = pickerModule.default;
|
||||
const data = dataModule.default;
|
||||
return {
|
||||
default: (props: any) => <PickerComp {...props} data={data} />,
|
||||
};
|
||||
});
|
||||
|
||||
export interface EmojiPickerInterface {
|
||||
onEmojiSelect: (emoji: any) => void;
|
||||
icon: ReactNode;
|
||||
@@ -34,7 +19,6 @@ export interface EmojiPickerInterface {
|
||||
size?: string;
|
||||
variant?: string;
|
||||
c?: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,38 +50,6 @@ function EmojiPicker({
|
||||
}
|
||||
});
|
||||
|
||||
// emoji-mart's built-in autoFocus calls .focus() without preventScroll, which
|
||||
// makes the browser scroll every scrollable ancestor of the search input to
|
||||
// bring it on screen — including the page editor's scroll container, so the
|
||||
// page jumps to the top whenever the picker is opened from a scrolled-down
|
||||
// position. The search input lives inside the <em-emoji-picker> custom
|
||||
// element's shadow root, so we poll for it after the dropdown mounts and
|
||||
// focus it ourselves with preventScroll.
|
||||
useEffect(() => {
|
||||
if (!opened || !dropdown) return;
|
||||
let cancelled = false;
|
||||
let rafId = 0;
|
||||
const tryFocus = (attempts: number) => {
|
||||
if (cancelled) return;
|
||||
const pickerEl = dropdown.querySelector("em-emoji-picker");
|
||||
const input = pickerEl?.shadowRoot?.querySelector<HTMLInputElement>(
|
||||
'input[type="search"]',
|
||||
);
|
||||
if (input) {
|
||||
input.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
if (attempts < 60) {
|
||||
rafId = requestAnimationFrame(() => tryFocus(attempts + 1));
|
||||
}
|
||||
};
|
||||
rafId = requestAnimationFrame(() => tryFocus(0));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [opened, dropdown]);
|
||||
|
||||
const handleEmojiSelect = (emoji) => {
|
||||
onEmojiSelect(emoji);
|
||||
handlers.close();
|
||||
@@ -118,15 +70,11 @@ function EmojiPicker({
|
||||
closeOnEscape={true}
|
||||
>
|
||||
<Popover.Target ref={setTarget}>
|
||||
<ActionIcon
|
||||
c={actionIconProps?.c || "gray"}
|
||||
variant={actionIconProps?.variant || "transparent"}
|
||||
<ActionIcon
|
||||
c={actionIconProps?.c || "gray"}
|
||||
variant={actionIconProps?.variant || "transparent"}
|
||||
size={actionIconProps?.size}
|
||||
tabIndex={actionIconProps?.tabIndex}
|
||||
onClick={handlers.toggle}
|
||||
aria-label={t("Pick emoji")}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={opened}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
@@ -134,6 +82,7 @@ function EmojiPicker({
|
||||
<Suspense fallback={null}>
|
||||
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
|
||||
<Picker
|
||||
data={async () => (await import("@emoji-mart/data")).default}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
perLine={8}
|
||||
skinTonePosition="search"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { UnstyledButton } from "@mantine/core";
|
||||
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
|
||||
// Menu.Item hard-codes role="menuitem"; use as its `component` to restore role="menuitemradio" so aria-checked works.
|
||||
export const RadioMenuItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ComponentPropsWithoutRef<"button">
|
||||
>((props, ref) => (
|
||||
<UnstyledButton ref={ref} {...props} role="menuitemradio" />
|
||||
));
|
||||
|
||||
RadioMenuItem.displayName = "RadioMenuItem";
|
||||
@@ -14,14 +14,7 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
|
||||
({ opened, size = "sm", ...others }, ref) => {
|
||||
return (
|
||||
<ActionIcon
|
||||
size={size}
|
||||
aria-expanded={opened}
|
||||
{...others}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
ref={ref}
|
||||
>
|
||||
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
|
||||
{opened ? (
|
||||
<IconLayoutSidebarRightExpand />
|
||||
) : (
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.skipLink {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 9999;
|
||||
padding: 8px 16px;
|
||||
background: var(--mantine-color-body);
|
||||
color: var(--mantine-color-text);
|
||||
border: 2px solid var(--mantine-color-blue-6);
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
transform: translateY(-200%);
|
||||
transition: transform 0.15s ease-out;
|
||||
}
|
||||
|
||||
.skipLink:focus {
|
||||
transform: translateY(0);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.skipLink {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "./skip-to-main.module.css";
|
||||
|
||||
export const MAIN_CONTENT_ID = "main-content";
|
||||
|
||||
export function SkipToMain() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a href={`#${MAIN_CONTENT_ID}`} className={classes.skipLink}>
|
||||
{t("Skip to main content")}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1 @@
|
||||
Files in this directory are subject to the Docmost Enterprise Edition license.
|
||||
|
||||
The Docmost Enterprise License (the “Enterprise License”)
|
||||
Copyright (c) 2023-present Docmost, Inc
|
||||
|
||||
|
||||
With regard to the Docmost Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Docmost Subscription Terms of Service, available
|
||||
at https://docmost.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Docmost, Inc.,
|
||||
and otherwise have a valid Docmost Enterprise Edition subscription for the correct number of user seats.
|
||||
Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Docmost
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Docmost Enterprise Edition subscription for the correct
|
||||
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Docmost and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Docmost Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
Files in this directory are subject to the Docmost Enterprise Edition license.
|
||||
@@ -1,105 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useChatInfoQuery } from "../queries/ai-chat-query";
|
||||
import { useChatStream } from "../hooks/use-chat-stream";
|
||||
import ChatMessageList from "./chat-message-list";
|
||||
import ChatEmptyState from "./chat-empty-state";
|
||||
import ChatInput from "./chat-input";
|
||||
import type { HomeAiPromptInitialState } from "@/features/home/components/home-ai-prompt";
|
||||
import classes from "../styles/ai-chat.module.css";
|
||||
|
||||
export default function AiChatLayout() {
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const chatInfoQuery = useChatInfoQuery(chatId);
|
||||
|
||||
// If the URL points at a chat the user does not own, the info fetch 404s.
|
||||
// Bounce them back to /ai so they cannot interact with any chat UI (including
|
||||
// kicking off orphan uploads) tied to a chat they have no access to.
|
||||
useEffect(() => {
|
||||
if (chatId && chatInfoQuery.isError) {
|
||||
navigate("/ai", { replace: true });
|
||||
}
|
||||
}, [chatId, chatInfoQuery.isError, navigate]);
|
||||
const {
|
||||
messages,
|
||||
streamingContent,
|
||||
streamingToolCalls,
|
||||
isStreaming,
|
||||
error,
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
hydrateFromServer,
|
||||
} = useChatStream(chatId);
|
||||
|
||||
const autoSentRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatInfoQuery.data?.messages) {
|
||||
hydrateFromServer(chatInfoQuery.data.messages);
|
||||
}
|
||||
}, [chatInfoQuery.data, hydrateFromServer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSentRef.current || chatId) return;
|
||||
const state = location.state as HomeAiPromptInitialState | null;
|
||||
if (!state?.initialContent && !state?.initialAttachments?.length) return;
|
||||
|
||||
autoSentRef.current = true;
|
||||
sendMessage(
|
||||
state.initialContent ?? "",
|
||||
state.initialMentions ?? [],
|
||||
state.initialAttachments ?? [],
|
||||
);
|
||||
navigate(location.pathname, { replace: true, state: null });
|
||||
}, [chatId, location, navigate, sendMessage]);
|
||||
|
||||
const hasMessages = messages.length > 0 || isStreaming || !!chatId;
|
||||
|
||||
// While the redirect effect is running (or if the user is still on this
|
||||
// component for any reason) never render the chat UI for a forbidden chat.
|
||||
if (chatId && chatInfoQuery.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.main}>
|
||||
{hasMessages ? (
|
||||
<>
|
||||
<ChatMessageList
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
streamingContent={streamingContent}
|
||||
streamingToolCalls={streamingToolCalls}
|
||||
/>
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-lg)",
|
||||
color: "var(--mantine-color-red-6)",
|
||||
fontSize: "var(--mantine-font-size-sm)",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.inputArea}>
|
||||
<ChatInput
|
||||
isStreaming={isStreaming}
|
||||
onSend={sendMessage}
|
||||
onStop={stopGeneration}
|
||||
chatId={chatId}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ChatEmptyState
|
||||
isStreaming={isStreaming}
|
||||
onSend={sendMessage}
|
||||
onStop={stopGeneration}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
|
||||
import { ActionIcon, Menu, TextInput } from "@mantine/core";
|
||||
import { IconDots, IconTrash, IconEdit } from "@tabler/icons-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AiChat } from "../types/ai-chat.types";
|
||||
import classes from "../styles/chat-sidebar.module.css";
|
||||
|
||||
type Props = {
|
||||
chat: AiChat;
|
||||
isActive: boolean;
|
||||
onDelete: (chatId: string, title: string | null) => void;
|
||||
onRename: (chatId: string, title: string) => void;
|
||||
};
|
||||
|
||||
function formatChatDate(
|
||||
isoString: string | Date,
|
||||
locale: string | undefined,
|
||||
): string {
|
||||
const date = new Date(isoString);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
).getTime();
|
||||
const ts = date.getTime();
|
||||
const sameYear = date.getFullYear() === now.getFullYear();
|
||||
|
||||
if (ts >= startOfToday) {
|
||||
return date.toLocaleTimeString(locale, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
if (sameYear) {
|
||||
return date.toLocaleDateString(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
return date.toLocaleDateString(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export default function AiChatSidebarItem({
|
||||
chat,
|
||||
isActive,
|
||||
onDelete,
|
||||
onRename,
|
||||
}: Props) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const formattedDate = useMemo(
|
||||
() => formatChatDate(chat.updatedAt, i18n.language),
|
||||
[chat.updatedAt, i18n.language],
|
||||
);
|
||||
|
||||
const chatTitle = chat.title || t("Untitled chat");
|
||||
|
||||
useEffect(() => {
|
||||
if (renaming) {
|
||||
// Wait for the input to be mounted before selecting.
|
||||
const id = window.setTimeout(() => inputRef.current?.select(), 0);
|
||||
return () => window.clearTimeout(id);
|
||||
}
|
||||
}, [renaming]);
|
||||
|
||||
const startRename = useCallback(() => {
|
||||
setRenameValue(chat.title || "");
|
||||
setRenaming(true);
|
||||
}, [chat.title]);
|
||||
|
||||
const submitRename = useCallback(() => {
|
||||
const trimmed = renameValue.trim();
|
||||
if (trimmed && trimmed !== chat.title) {
|
||||
onRename(chat.id, trimmed);
|
||||
}
|
||||
setRenaming(false);
|
||||
}, [renameValue, chat.id, chat.title, onRename]);
|
||||
|
||||
if (renaming) {
|
||||
return (
|
||||
<div className={classes.chatItem} data-active={isActive || undefined}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
placeholder={t("Chat name")}
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submitRename();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
setRenaming(false);
|
||||
}
|
||||
}}
|
||||
onBlur={submitRename}
|
||||
classNames={{ input: classes.chatItemRenameInput }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/ai/chat/${chat.id}`}
|
||||
className={classes.chatItem}
|
||||
data-active={isActive || undefined}
|
||||
>
|
||||
<span className={classes.chatItemTitle}>{chatTitle}</span>
|
||||
<span className={classes.chatItemDate}>{formattedDate}</span>
|
||||
<div className={classes.chatItemActions}>
|
||||
<Menu position="bottom-end" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
color="gray"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
aria-label={t("Chat menu for {{title}}", { title: chatTitle })}
|
||||
>
|
||||
<IconDots size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconEdit size={14} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startRename();
|
||||
}}
|
||||
>
|
||||
{t("Rename")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
color="red"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelete(chat.id, chat.title);
|
||||
}}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
ActionIcon,
|
||||
Center,
|
||||
Text,
|
||||
TextInput,
|
||||
Loader,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconPlus, IconSearch, IconMessageCircle2 } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useChatsQuery,
|
||||
useDeleteChatMutation,
|
||||
useUpdateChatTitleMutation,
|
||||
useSearchChatsQuery,
|
||||
} from "../queries/ai-chat-query";
|
||||
import AiChatSidebarItem from "./ai-chat-sidebar-item";
|
||||
import { groupChatsByAge } from "../utils/group-chats-by-age";
|
||||
import classes from "../styles/chat-sidebar.module.css";
|
||||
|
||||
export default function AiChatSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300);
|
||||
const chatsQuery = useChatsQuery();
|
||||
const searchQuery = useSearchChatsQuery(debouncedSearch);
|
||||
const deleteMutation = useDeleteChatMutation();
|
||||
const renameMutation = useUpdateChatTitleMutation();
|
||||
|
||||
const chats = useMemo(() => {
|
||||
if (debouncedSearch) {
|
||||
return searchQuery.data || [];
|
||||
}
|
||||
return chatsQuery.data?.pages.flatMap((p) => p.items) || [];
|
||||
}, [debouncedSearch, searchQuery.data, chatsQuery.data]);
|
||||
|
||||
const groupedChats = useMemo(() => groupChatsByAge(chats, t), [chats, t]);
|
||||
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const { hasNextPage, fetchNextPage, isFetchingNextPage } = chatsQuery;
|
||||
const isSearching = Boolean(debouncedSearch);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) return;
|
||||
const sentinel = sentinelRef.current;
|
||||
if (!sentinel) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
observer.observe(sentinel);
|
||||
return () => observer.disconnect();
|
||||
}, [isSearching, hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
const handleNewChat = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
navigate("/ai");
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(id: string, title: string | null) => {
|
||||
modals.openConfirmModal({
|
||||
title: t("Delete chat"),
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
{t("Are you sure you want to delete '{{title}}'? This action cannot be undone.", {
|
||||
title: title || t("Untitled"),
|
||||
})}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
deleteMutation.mutate(id, {
|
||||
onSuccess: () => {
|
||||
if (chatId === id) {
|
||||
navigate("/ai");
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[deleteMutation, chatId, navigate, t],
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
(chatId: string, title: string) => {
|
||||
renameMutation.mutate({ chatId, title });
|
||||
},
|
||||
[renameMutation],
|
||||
);
|
||||
|
||||
const isLoading = chatsQuery.isLoading || searchQuery.isLoading;
|
||||
|
||||
return (
|
||||
<div className={classes.sidebar}>
|
||||
<div className={classes.header}>
|
||||
<h2 className={classes.title}>{t("AI Chat")}</h2>
|
||||
<Tooltip label={t("New chat")} openDelay={250} withArrow>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to="/ai"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={handleNewChat}
|
||||
aria-label={t("New chat")}
|
||||
>
|
||||
<IconPlus size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={classes.searchInput}
|
||||
placeholder={t("Search chats...")}
|
||||
aria-label={t("Search chats")}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
size="xs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<div className={classes.chatList}>
|
||||
{isLoading && <Loader size="xs" mx="auto" mt="md" />}
|
||||
{!isLoading && chats.length === 0 && (
|
||||
<div className={classes.chatListEmpty}>
|
||||
<IconMessageCircle2
|
||||
size={28}
|
||||
stroke={1.5}
|
||||
className={classes.chatListEmptyIcon}
|
||||
/>
|
||||
<div className={classes.chatListEmptyTitle}>
|
||||
{isSearching ? t("No chats found") : t("No conversations yet")}
|
||||
</div>
|
||||
<div className={classes.chatListEmptyHint}>
|
||||
{isSearching
|
||||
? t("Try a different search term.")
|
||||
: t("Start a new chat to see it here.")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isSearching
|
||||
? chats.map((chat) => (
|
||||
<AiChatSidebarItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === chatId}
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
))
|
||||
: groupedChats.map((group) => (
|
||||
<div key={group.key} className={classes.chatGroup}>
|
||||
<h3 className={classes.chatGroupLabel}>{group.label}</h3>
|
||||
{group.chats.map((chat) => (
|
||||
<AiChatSidebarItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === chatId}
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{!isSearching && (
|
||||
<>
|
||||
<div ref={sentinelRef} style={{ height: 1 }} />
|
||||
{isFetchingNextPage && (
|
||||
<Center py="xs">
|
||||
<Loader size="xs" />
|
||||
</Center>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { TextInput, Loader, Text, ScrollArea } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useChatsQuery, useSearchChatsQuery } from "../queries/ai-chat-query";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "../styles/aside-chat-panel.module.css";
|
||||
|
||||
type Props = {
|
||||
activeChatId: string | undefined;
|
||||
onSelect: (chatId: string) => void;
|
||||
};
|
||||
|
||||
export default function AsideChatHistory({ activeChatId, onSelect }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(searchValue, 300);
|
||||
|
||||
const chatsQuery = useChatsQuery();
|
||||
const searchQuery = useSearchChatsQuery(debouncedSearch);
|
||||
|
||||
const isSearching = debouncedSearch.length > 0;
|
||||
const chats = isSearching
|
||||
? (searchQuery.data ?? [])
|
||||
: (chatsQuery.data?.pages.flatMap((p) => p.items) ?? []);
|
||||
const isLoading = isSearching ? searchQuery.isLoading : chatsQuery.isLoading;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextInput
|
||||
placeholder={t("Search chats...")}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
size="xs"
|
||||
mb="xs"
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: 16 }}>
|
||||
<Loader size="sm" />
|
||||
</div>
|
||||
) : chats.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="md">
|
||||
{isSearching ? t("No chats found") : t("No chat history")}
|
||||
</Text>
|
||||
) : (
|
||||
<ScrollArea.Autosize mah={300} scrollbars="y">
|
||||
<div className={classes.historyList}>
|
||||
{chats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className={classes.historyItem}
|
||||
data-active={chat.id === activeChatId || undefined}
|
||||
onClick={() => onSelect(chat.id)}
|
||||
>
|
||||
<span className={classes.historyItemTitle}>
|
||||
{chat.title || t("Untitled chat")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea.Autosize>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ActionIcon, Popover, Tooltip, UnstyledButton } from "@mantine/core";
|
||||
import {
|
||||
IconPlus,
|
||||
IconChevronDown,
|
||||
IconArrowsDiagonal,
|
||||
IconX,
|
||||
IconSparkles,
|
||||
IconFileText,
|
||||
IconLanguage,
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import { useAtom } from "jotai";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { useChatStream } from "../hooks/use-chat-stream";
|
||||
import { useChatInfoQuery } from "../queries/ai-chat-query";
|
||||
import ChatMessageList from "./chat-message-list";
|
||||
import ChatInput from "./chat-input";
|
||||
import AsideChatHistory from "./aside-chat-history";
|
||||
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
|
||||
import classes from "../styles/aside-chat-panel.module.css";
|
||||
|
||||
type QuickAction = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export default function AsideChatPanel() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [, setAsideState] = useAtom(asideStateAtom);
|
||||
const [chatId, setChatId] = useState<string | undefined>(undefined);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [contextPages, setContextPages] = useState<PageMention[]>([]);
|
||||
const { pageSlug } = useParams();
|
||||
const slugId = extractPageSlugId(pageSlug);
|
||||
const { data: page } = usePageQuery({ pageId: slugId });
|
||||
|
||||
const chatInfoQuery = useChatInfoQuery(chatId);
|
||||
const {
|
||||
messages,
|
||||
streamingContent,
|
||||
streamingToolCalls,
|
||||
isStreaming,
|
||||
error,
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
hydrateFromServer,
|
||||
} = useChatStream(chatId, {
|
||||
onChatCreated: (newChatId) => {
|
||||
setChatId(newChatId);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (page && !chatId) {
|
||||
setContextPages([{ id: page.id, title: page.title || "", slugId: page.slugId }]);
|
||||
}
|
||||
}, [page, chatId]);
|
||||
|
||||
const handleRemoveContextPage = useCallback((pageId: string) => {
|
||||
setContextPages((prev) => prev.filter((p) => p.id !== pageId));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatInfoQuery.data?.messages) {
|
||||
hydrateFromServer(chatInfoQuery.data.messages);
|
||||
}
|
||||
}, [chatInfoQuery.data, hydrateFromServer]);
|
||||
|
||||
// Drop the open chatId if the current user lost access to it (404/403 on
|
||||
// the info fetch). Reverts the panel to a fresh chat instead of presenting
|
||||
// an input tied to a chat the user does not own.
|
||||
useEffect(() => {
|
||||
if (chatId && chatInfoQuery.isError) {
|
||||
setChatId(undefined);
|
||||
}
|
||||
}, [chatId, chatInfoQuery.isError]);
|
||||
|
||||
const handleNewChat = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setChatId(undefined);
|
||||
if (page) {
|
||||
setContextPages([
|
||||
{ id: page.id, title: page.title || "", slugId: page.slugId },
|
||||
]);
|
||||
}
|
||||
},
|
||||
[page],
|
||||
);
|
||||
|
||||
const handleSelectChat = useCallback((selectedChatId: string) => {
|
||||
setChatId(selectedChatId);
|
||||
setHistoryOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
if (chatId) {
|
||||
navigate(`/ai/chat/${chatId}`);
|
||||
} else {
|
||||
navigate("/ai");
|
||||
}
|
||||
setAsideState({ tab: "", isAsideOpen: false });
|
||||
}, [chatId, navigate, setAsideState]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setAsideState({ tab: "", isAsideOpen: false });
|
||||
}, [setAsideState]);
|
||||
|
||||
const handleSend = useCallback(
|
||||
(content: string, mentions: PageMention[], attachments: ChatAttachment[]) => {
|
||||
const contextPageId = contextPages.length > 0 ? contextPages[0].id : undefined;
|
||||
sendMessage(content, mentions, attachments, contextPageId);
|
||||
},
|
||||
[sendMessage, contextPages],
|
||||
);
|
||||
|
||||
const handleQuickAction = useCallback(
|
||||
(prompt: string) => {
|
||||
handleSend(prompt, [], []);
|
||||
},
|
||||
[handleSend],
|
||||
);
|
||||
|
||||
const hasMessages = messages.length > 0 || isStreaming;
|
||||
|
||||
const quickActions: QuickAction[] = [
|
||||
{ icon: <IconFileText size={16} />, label: t("Summarize this page"), prompt: "Summarize this page" },
|
||||
{ icon: <IconLanguage size={16} />, label: t("Translate this page"), prompt: "Translate this page" },
|
||||
{ icon: <IconSearch size={16} />, label: t("Analyze for insights"), prompt: "Analyze this page for insights" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={classes.panel}>
|
||||
<div className={classes.toolbar}>
|
||||
<Popover
|
||||
opened={historyOpen}
|
||||
onChange={setHistoryOpen}
|
||||
position="bottom-start"
|
||||
width={280}
|
||||
shadow="md"
|
||||
>
|
||||
<Popover.Target>
|
||||
<UnstyledButton
|
||||
className={classes.titleButton}
|
||||
onClick={() => setHistoryOpen((o) => !o)}
|
||||
>
|
||||
<span className={classes.titleText}>
|
||||
{chatInfoQuery.data?.chat?.title || t("New chat")}
|
||||
</span>
|
||||
<IconChevronDown size={16} stroke={1.75} />
|
||||
</UnstyledButton>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<AsideChatHistory activeChatId={chatId} onSelect={handleSelectChat} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<div className={classes.toolbarSpacer} />
|
||||
|
||||
<Tooltip label={t("New chat")} openDelay={250}>
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="/ai"
|
||||
variant="subtle"
|
||||
color="dark"
|
||||
aria-label={t("New chat")}
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<IconPlus size={20} stroke={1.75} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Open full page")} openDelay={250}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="dark"
|
||||
aria-label={t("Open full page")}
|
||||
onClick={handleExpand}
|
||||
>
|
||||
<IconArrowsDiagonal size={18} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Close")} openDelay={250}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="dark"
|
||||
aria-label={t("Close")}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<IconX size={20} stroke={1.75} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: "var(--mantine-spacing-xs) var(--mantine-spacing-sm)",
|
||||
color: "var(--mantine-color-red-6)",
|
||||
fontSize: "var(--mantine-font-size-xs)",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMessages ? (
|
||||
<>
|
||||
<div className={classes.messages} data-aside-chat>
|
||||
<ChatMessageList
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
streamingContent={streamingContent}
|
||||
streamingToolCalls={streamingToolCalls}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={classes.emptyState}>
|
||||
<IconSparkles size={36} stroke={1.5} className={classes.emptyStateIcon} />
|
||||
<div className={classes.emptyStateTitle}>{t("How can I help you today?")}</div>
|
||||
<div className={classes.quickActions}>
|
||||
{quickActions.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
className={classes.quickAction}
|
||||
onClick={() => handleQuickAction(action.prompt)}
|
||||
>
|
||||
<span className={classes.quickActionIcon}>{action.icon}</span>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={classes.inputArea}>
|
||||
<ChatInput
|
||||
isStreaming={isStreaming}
|
||||
onSend={handleSend}
|
||||
onStop={stopGeneration}
|
||||
placeholder={t("Ask anything...")}
|
||||
autofocus={false}
|
||||
contextPages={contextPages}
|
||||
onRemoveContextPage={handleRemoveContextPage}
|
||||
variant="flat"
|
||||
chatId={chatId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
IconSparkles,
|
||||
IconSearch,
|
||||
IconFilePlus,
|
||||
IconEdit,
|
||||
IconFileText,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ChatInput from "./chat-input";
|
||||
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
|
||||
import classes from "../styles/ai-chat.module.css";
|
||||
|
||||
type Suggestion = {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
const SUGGESTIONS: Suggestion[] = [
|
||||
{
|
||||
icon: <IconSearch size={16} />,
|
||||
text: "Search across all pages",
|
||||
prompt: "Search for pages about ",
|
||||
},
|
||||
{
|
||||
icon: <IconFilePlus size={16} />,
|
||||
text: "Create a new page",
|
||||
prompt: "Create a new page titled ",
|
||||
},
|
||||
{
|
||||
icon: <IconFileText size={16} />,
|
||||
text: "Summarize a page",
|
||||
prompt: "Summarize the page @",
|
||||
},
|
||||
{
|
||||
icon: <IconEdit size={16} />,
|
||||
text: "Update page content",
|
||||
prompt: "Update the page @",
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
isStreaming: boolean;
|
||||
onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void;
|
||||
onStop: () => void;
|
||||
};
|
||||
|
||||
export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSuggestionClick = (prompt: string) => {
|
||||
onSend(prompt, [], []);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.emptyState}>
|
||||
<IconSparkles size={48} stroke={1.5} className={classes.emptyStateIcon} />
|
||||
<div className={classes.emptyStateBrand}>{t("Docmost AI")}</div>
|
||||
<h1 className={classes.emptyStateTitle}>
|
||||
{t("What can I help you with?")}
|
||||
</h1>
|
||||
|
||||
<div className={classes.emptyStateInput}>
|
||||
<ChatInput
|
||||
isStreaming={isStreaming}
|
||||
onSend={onSend}
|
||||
onStop={onStop}
|
||||
placeholder={t("Ask anything... Use @ to mention pages")}
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={classes.suggestionsSection}>
|
||||
<h2 className={classes.suggestionsLabel}>{t("Get started")}</h2>
|
||||
<div className={classes.suggestionsGrid}>
|
||||
{SUGGESTIONS.map((s) => (
|
||||
<button
|
||||
key={s.text}
|
||||
type="button"
|
||||
className={classes.suggestionCard}
|
||||
onClick={() => handleSuggestionClick(s.prompt)}
|
||||
>
|
||||
<span className={classes.suggestionIcon}>{s.icon}</span>
|
||||
<span className={classes.suggestionText}>{s.text}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
import { useCallback, useId, useRef, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react";
|
||||
import { Popover } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { EditorContent, ReactNodeViewRenderer, useEditor } from "@tiptap/react";
|
||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||
import { CharacterCount } from "@tiptap/extensions";
|
||||
import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { Mention, LinkExtension } from "@docmost/editor-ext";
|
||||
import EmojiCommand from "@/features/editor/extensions/emoji-command";
|
||||
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion";
|
||||
import MentionView from "@/features/editor/components/mention/mention-view";
|
||||
import { uploadChatFile } from "../services/ai-chat-service";
|
||||
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
|
||||
import classes from "../styles/chat-input.module.css";
|
||||
|
||||
type PendingAttachment = ChatAttachment & { uploading: boolean };
|
||||
|
||||
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
|
||||
const ACCEPTED_FILE_TYPES = ".pdf,.docx,.txt,.csv,.md,.png,.jpg,.jpeg,.webp";
|
||||
// Kept in sync with MAX_ATTACHMENTS_PER_MESSAGE in apps/server/src/ee/ai-chat/ai-chat-limits.ts
|
||||
const MAX_ATTACHMENTS_PER_MESSAGE = 5;
|
||||
|
||||
type Props = {
|
||||
isStreaming: boolean;
|
||||
onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void;
|
||||
onStop: () => void;
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
contextPages?: PageMention[];
|
||||
onRemoveContextPage?: (pageId: string) => void;
|
||||
variant?: "card" | "flat";
|
||||
showDisclaimer?: boolean;
|
||||
chatId?: string;
|
||||
};
|
||||
|
||||
function extractMentions(json: any): PageMention[] {
|
||||
const mentions: PageMention[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
function walk(node: any) {
|
||||
if (node.type === "mention" && node.attrs?.entityType === "page" && node.attrs?.entityId) {
|
||||
if (!seen.has(node.attrs.entityId)) {
|
||||
seen.add(node.attrs.entityId);
|
||||
mentions.push({
|
||||
id: node.attrs.entityId,
|
||||
title: node.attrs.label || "",
|
||||
slugId: node.attrs.slugId || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(json);
|
||||
return mentions;
|
||||
}
|
||||
|
||||
function editorJsonToText(json: any): string {
|
||||
let text = "";
|
||||
|
||||
function walk(node: any) {
|
||||
if (node.type === "text") {
|
||||
text += node.text || "";
|
||||
} else if (node.type === "mention") {
|
||||
text += `@${node.attrs?.label || ""}`;
|
||||
} else if (node.type === "paragraph") {
|
||||
if (text.length > 0) text += "\n";
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(json);
|
||||
return text;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
isStreaming,
|
||||
onSend,
|
||||
onStop,
|
||||
placeholder,
|
||||
autofocus = true,
|
||||
contextPages,
|
||||
onRemoveContextPage,
|
||||
variant = "card",
|
||||
showDisclaimer = true,
|
||||
chatId,
|
||||
}: Props) {
|
||||
const chatIdRef = useRef(chatId);
|
||||
chatIdRef.current = chatId;
|
||||
const { t } = useTranslation();
|
||||
const [isEmpty, setIsEmpty] = useState(true);
|
||||
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
|
||||
const [plusMenuOpen, setPlusMenuOpen] = useState(false);
|
||||
const plusMenuId = useId();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const onSendRef = useRef(onSend);
|
||||
onSendRef.current = onSend;
|
||||
|
||||
const handleFileSelect = useCallback(async (files: FileList | null) => {
|
||||
if (!files?.length) return;
|
||||
|
||||
const room = MAX_ATTACHMENTS_PER_MESSAGE - pendingAttachments.length;
|
||||
if (room <= 0) {
|
||||
notifications.show({
|
||||
color: "yellow",
|
||||
message: t("You can attach up to {{max}} files per message.", {
|
||||
max: MAX_ATTACHMENTS_PER_MESSAGE,
|
||||
}),
|
||||
});
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const incoming = Array.from(files);
|
||||
const accepted = incoming.slice(0, room);
|
||||
|
||||
if (incoming.length > accepted.length) {
|
||||
notifications.show({
|
||||
color: "yellow",
|
||||
message: t(
|
||||
"Only the first {{n}} file(s) were added (max {{max}} per message).",
|
||||
{ n: accepted.length, max: MAX_ATTACHMENTS_PER_MESSAGE },
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of accepted) {
|
||||
const tempId = `uploading-${Date.now()}-${Math.random()}`;
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
|
||||
const placeholder: PendingAttachment = {
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileExt: ext,
|
||||
fileSize: file.size,
|
||||
mimeType: file.type,
|
||||
uploading: true,
|
||||
};
|
||||
|
||||
setPendingAttachments((prev) => [...prev, placeholder]);
|
||||
|
||||
try {
|
||||
const uploaded = await uploadChatFile(file, chatIdRef.current);
|
||||
setPendingAttachments((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === tempId ? { ...uploaded, uploading: false } : a,
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
setPendingAttachments((prev) => prev.filter((a) => a.id !== tempId));
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}, [pendingAttachments.length, t]);
|
||||
|
||||
const removeAttachment = useCallback((id: string) => {
|
||||
setPendingAttachments((prev) => prev.filter((a) => a.id !== id));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!editor || editor.isDestroyed || isStreaming) return;
|
||||
const json = editor.getJSON();
|
||||
const text = editorJsonToText(json).trim();
|
||||
const readyAttachments = pendingAttachments.filter((a) => !a.uploading);
|
||||
if (!text && readyAttachments.length === 0) return;
|
||||
|
||||
const mentions = extractMentions(json);
|
||||
onSendRef.current(text, mentions, readyAttachments);
|
||||
editor.commands.clearContent();
|
||||
editor.commands.focus();
|
||||
setPendingAttachments([]);
|
||||
}, [isStreaming, pendingAttachments]);
|
||||
|
||||
const handleSubmitRef = useRef(handleSubmit);
|
||||
handleSubmitRef.current = handleSubmit;
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
gapcursor: false,
|
||||
dropcursor: false,
|
||||
link: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder || t("Ask anything... Use @ to mention pages"),
|
||||
}),
|
||||
CharacterCount.configure({
|
||||
limit: 50000,
|
||||
}),
|
||||
LinkExtension,
|
||||
EmojiCommand,
|
||||
Mention.configure({
|
||||
suggestion: {
|
||||
allowSpaces: true,
|
||||
items: () => [],
|
||||
// @ts-ignore
|
||||
render: mentionRenderItems,
|
||||
},
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
}).extend({
|
||||
addNodeView() {
|
||||
this.editor.isInitialized = true;
|
||||
return ReactNodeViewRenderer(MentionView);
|
||||
},
|
||||
}),
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
role: "textbox",
|
||||
"aria-label": placeholder || t("Ask anything... Use @ to mention pages"),
|
||||
"aria-multiline": "true",
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => {
|
||||
if (
|
||||
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"].includes(
|
||||
event.key,
|
||||
)
|
||||
) {
|
||||
const emojiCommand = document.querySelector("#emoji-command");
|
||||
const mentionPopup = document.querySelector("#mention");
|
||||
if (emojiCommand || mentionPopup) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmitRef.current();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
content: "",
|
||||
editable: true,
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus: autofocus ? "end" : false,
|
||||
onUpdate: ({ editor: e }) => {
|
||||
setIsEmpty(!e.getText().trim());
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && !editor.isDestroyed && autofocus) {
|
||||
editor.commands.focus();
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0;
|
||||
|
||||
const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={wrapperClass} data-chat-input>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={ACCEPTED_FILE_TYPES}
|
||||
multiple
|
||||
aria-label={t("Add files")}
|
||||
tabIndex={-1}
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
/>
|
||||
|
||||
{((contextPages?.length ?? 0) > 0 || pendingAttachments.length > 0) && (
|
||||
<div className={classes.attachmentChips}>
|
||||
{contextPages?.map((page) => (
|
||||
<div key={page.id} className={classes.attachmentChip}>
|
||||
<IconFileText size={14} />
|
||||
<span className={classes.attachmentChipName}>
|
||||
{page.title || "Untitled"}
|
||||
</span>
|
||||
{onRemoveContextPage && (
|
||||
<button
|
||||
type="button"
|
||||
className={classes.attachmentChipRemove}
|
||||
onClick={() => onRemoveContextPage(page.id)}
|
||||
aria-label={`Remove ${page.title}`}
|
||||
>
|
||||
<IconX size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{pendingAttachments.map((attachment) => (
|
||||
<div
|
||||
key={attachment.id}
|
||||
className={`${classes.attachmentChip} ${attachment.uploading ? classes.attachmentChipUploading : ""}`}
|
||||
>
|
||||
{IMAGE_EXTENSIONS.includes(attachment.fileExt) ? (
|
||||
<IconPhoto size={14} />
|
||||
) : (
|
||||
<IconFile size={14} />
|
||||
)}
|
||||
<span className={classes.attachmentChipName}>
|
||||
{attachment.fileName}
|
||||
</span>
|
||||
{!attachment.uploading && (
|
||||
<button
|
||||
type="button"
|
||||
className={classes.attachmentChipRemove}
|
||||
onClick={() => removeAttachment(attachment.id)}
|
||||
aria-label={`Remove ${attachment.fileName}`}
|
||||
>
|
||||
<IconX size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EditorContent editor={editor} className={classes.editorContent} />
|
||||
<div className={classes.actions}>
|
||||
<Popover
|
||||
opened={plusMenuOpen}
|
||||
onChange={setPlusMenuOpen}
|
||||
position="top-start"
|
||||
width={220}
|
||||
shadow="md"
|
||||
withRoles={false}
|
||||
trapFocus
|
||||
returnFocus
|
||||
>
|
||||
<Popover.Target>
|
||||
<button
|
||||
type="button"
|
||||
className={classes.plusButton}
|
||||
onClick={() => setPlusMenuOpen((o) => !o)}
|
||||
aria-label="Add content"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={plusMenuOpen}
|
||||
aria-controls={plusMenuOpen ? plusMenuId : undefined}
|
||||
>
|
||||
<IconPlus size={14} />
|
||||
</button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown id={plusMenuId} role="menu" p={4}>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={classes.plusMenuItem}
|
||||
onClick={() => {
|
||||
fileInputRef.current?.click();
|
||||
setPlusMenuOpen(false);
|
||||
}}
|
||||
disabled={pendingAttachments.length >= MAX_ATTACHMENTS_PER_MESSAGE}
|
||||
title={
|
||||
pendingAttachments.length >= MAX_ATTACHMENTS_PER_MESSAGE
|
||||
? t("Max {{max}} files per message", {
|
||||
max: MAX_ATTACHMENTS_PER_MESSAGE,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<IconPaperclip size={16} className={classes.plusMenuIcon} />
|
||||
{t("Add files")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={classes.plusMenuItem}
|
||||
onClick={() => {
|
||||
editor?.commands.insertContent("@");
|
||||
editor?.commands.focus();
|
||||
setPlusMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<IconAt size={16} className={classes.plusMenuIcon} />
|
||||
{t("Mention a page")}
|
||||
</button>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classes.stopButton}
|
||||
onClick={onStop}
|
||||
aria-label="Stop generation"
|
||||
>
|
||||
<IconPlayerStopFilled size={14} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={classes.sendButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={!hasContent}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<IconArrowUp size={16} stroke={2.5} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showDisclaimer && (
|
||||
<div className={classes.disclaimer}>
|
||||
{t("AI-generated content may not be accurate.")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import { useEffect, useRef, useCallback, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { IconArrowDown, IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { VisuallyHidden } from "@mantine/core";
|
||||
import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types";
|
||||
import ChatMessage from "./chat-message";
|
||||
import classes from "../styles/ai-chat.module.css";
|
||||
|
||||
function ChatMessageErrorFallback() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={classes.messageErrorFallback}>
|
||||
<IconAlertTriangle size={14} />
|
||||
<span>{t("Failed to render this message.")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
messages: AiChatMessage[];
|
||||
isStreaming: boolean;
|
||||
streamingContent: string;
|
||||
streamingToolCalls: AiChatToolCall[];
|
||||
};
|
||||
|
||||
const BOTTOM_THRESHOLD_PX = 32;
|
||||
const SCROLL_UP_THRESHOLD_PX = 5;
|
||||
const SMOOTH_SCROLL_SETTLE_MS = 600;
|
||||
|
||||
export default function ChatMessageList({
|
||||
messages,
|
||||
isStreaming,
|
||||
streamingContent,
|
||||
streamingToolCalls,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const isAtBottomRef = useRef(true);
|
||||
const isAutoScrollingRef = useRef(false);
|
||||
const prevScrollTopRef = useRef(0);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
|
||||
// Dedicated status-region announcement for screen readers. Rather than
|
||||
// putting aria-live on the whole transcript (which re-fires for every
|
||||
// streamed token), announce "AI is thinking…" when streaming starts and
|
||||
// the full assistant reply once streaming completes — a single, clean read.
|
||||
const [statusAnnouncement, setStatusAnnouncement] = useState("");
|
||||
const wasStreamingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const justStartedStreaming = isStreaming && !wasStreamingRef.current;
|
||||
const justFinishedStreaming = !isStreaming && wasStreamingRef.current;
|
||||
|
||||
if (justStartedStreaming) {
|
||||
setStatusAnnouncement(t("AI is thinking..."));
|
||||
} else if (justFinishedStreaming) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage?.role === "assistant" && lastMessage.content) {
|
||||
// Strip markdown punctuation so screen readers don't read symbols
|
||||
// like # * _ ` ~ aloud. A plain-text version is fine — the styled
|
||||
// version stays in the DOM for visual users.
|
||||
const plainText = lastMessage.content
|
||||
.replace(/[#*_`~]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
setStatusAnnouncement(plainText);
|
||||
} else {
|
||||
setStatusAnnouncement("");
|
||||
}
|
||||
}
|
||||
|
||||
wasStreamingRef.current = isStreaming;
|
||||
}, [isStreaming, messages, t]);
|
||||
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
isAutoScrollingRef.current = true;
|
||||
const target = container.scrollHeight - container.clientHeight;
|
||||
container.scrollTo({ top: target, behavior });
|
||||
prevScrollTopRef.current = target;
|
||||
isAtBottomRef.current = true;
|
||||
setShowScrollButton(false);
|
||||
|
||||
if (behavior === "smooth") {
|
||||
setTimeout(() => {
|
||||
isAutoScrollingRef.current = false;
|
||||
if (containerRef.current) {
|
||||
prevScrollTopRef.current = containerRef.current.scrollTop;
|
||||
}
|
||||
}, SMOOTH_SCROLL_SETTLE_MS);
|
||||
} else {
|
||||
isAutoScrollingRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (isAutoScrollingRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const currentScrollTop = container.scrollTop;
|
||||
const scrolledUp =
|
||||
currentScrollTop < prevScrollTopRef.current - SCROLL_UP_THRESHOLD_PX;
|
||||
prevScrollTopRef.current = currentScrollTop;
|
||||
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - currentScrollTop - container.clientHeight;
|
||||
const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX;
|
||||
|
||||
if (scrolledUp) {
|
||||
isAtBottomRef.current = atBottom;
|
||||
} else if (atBottom) {
|
||||
isAtBottomRef.current = true;
|
||||
}
|
||||
|
||||
setShowScrollButton(!atBottom);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => container.removeEventListener("scroll", handleScroll);
|
||||
}, [handleScroll]);
|
||||
|
||||
// Instant scroll during streaming to keep up with rapid updates
|
||||
useEffect(() => {
|
||||
if (isAtBottomRef.current) {
|
||||
scrollToBottom("instant");
|
||||
}
|
||||
}, [streamingContent, streamingToolCalls.length, scrollToBottom]);
|
||||
|
||||
// Smooth scroll for new messages. Always force-scroll when the latest
|
||||
// message is from the user (they just sent it), even if they were reading
|
||||
// scrollback.
|
||||
useEffect(() => {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const lastIsUser = lastMessage?.role === "user";
|
||||
if (lastIsUser || isAtBottomRef.current) {
|
||||
scrollToBottom("smooth");
|
||||
return;
|
||||
}
|
||||
|
||||
// No auto-scroll: recompute from actual layout so that chat switches to
|
||||
// content that doesn't overflow correctly hide the button even when no
|
||||
// scroll event fires.
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX;
|
||||
isAtBottomRef.current = atBottom;
|
||||
setShowScrollButton(!atBottom);
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
return (
|
||||
<div className={classes.messageListWrapper}>
|
||||
{/* Single status region for chat announcements. Kept outside the
|
||||
scrolling transcript so changes here trigger one polite read per
|
||||
state change instead of re-announcing every streamed token. */}
|
||||
<VisuallyHidden role="status" aria-live="polite">
|
||||
{statusAnnouncement}
|
||||
</VisuallyHidden>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classes.messageList}
|
||||
aria-label={t("Chat transcript")}
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
<ErrorBoundary
|
||||
key={msg.id}
|
||||
fallback={<ChatMessageErrorFallback />}
|
||||
>
|
||||
<ChatMessage message={msg} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
{isStreaming && (
|
||||
<ErrorBoundary
|
||||
resetKeys={[streamingContent, streamingToolCalls.length]}
|
||||
fallback={<ChatMessageErrorFallback />}
|
||||
>
|
||||
<ChatMessage
|
||||
message={{
|
||||
id: "streaming",
|
||||
chatId: "",
|
||||
role: "assistant",
|
||||
content: null,
|
||||
toolCalls: null,
|
||||
metadata: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
}}
|
||||
isStreaming
|
||||
streamingContent={streamingContent}
|
||||
streamingToolCalls={streamingToolCalls}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
{showScrollButton && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("Scroll to bottom")}
|
||||
className={classes.scrollToBottomButton}
|
||||
onClick={() => scrollToBottom("smooth")}
|
||||
>
|
||||
<IconArrowDown size={16} stroke={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DOMPurify from "dompurify";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconCheck,
|
||||
IconCopy,
|
||||
IconFile,
|
||||
IconLoader2,
|
||||
IconPhoto,
|
||||
} from "@tabler/icons-react";
|
||||
import { markdownToHtml } from "@docmost/editor-ext";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types";
|
||||
import ChatToolGroup from "./chat-tool-group";
|
||||
import classes from "../styles/chat-message.module.css";
|
||||
import CopyTextButton from "@/components/common/copy.tsx";
|
||||
|
||||
const PAGE_PATH_RE = /\/s\/[^/?#]+\/p\/[^/?#]+/;
|
||||
|
||||
const chatSanitizer = DOMPurify();
|
||||
chatSanitizer.addHook("afterSanitizeAttributes", (node) => {
|
||||
if (node.tagName !== "A") return;
|
||||
const href = node.getAttribute("href") || "";
|
||||
|
||||
// Recover the canonical /s/{slug}/p/{slugId} path if the model wrapped it
|
||||
// in a fabricated host (https://s/..., https://yoursite.com/s/..., //s/...).
|
||||
const m = href.match(PAGE_PATH_RE);
|
||||
if (m) {
|
||||
node.setAttribute("href", m[0]);
|
||||
node.removeAttribute("target");
|
||||
node.removeAttribute("rel");
|
||||
return;
|
||||
}
|
||||
|
||||
if (href.startsWith("http://") || href.startsWith("https://")) {
|
||||
node.setAttribute("target", "_blank");
|
||||
node.setAttribute("rel", "noopener noreferrer");
|
||||
}
|
||||
});
|
||||
|
||||
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
|
||||
|
||||
type Props = {
|
||||
message: AiChatMessage;
|
||||
isStreaming?: boolean;
|
||||
streamingContent?: string;
|
||||
streamingToolCalls?: AiChatToolCall[];
|
||||
};
|
||||
|
||||
export default function ChatMessage({
|
||||
message,
|
||||
isStreaming,
|
||||
streamingContent,
|
||||
streamingToolCalls,
|
||||
}: Props) {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleContentClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const anchor = target.closest("a");
|
||||
if (!anchor) return;
|
||||
|
||||
const href = anchor.getAttribute("href");
|
||||
if (href && (href.startsWith("/s/") || href.startsWith("/p/"))) {
|
||||
e.preventDefault();
|
||||
navigate(href);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
if (message.role === "tool") return null;
|
||||
|
||||
const isUser = message.role === "user";
|
||||
const content = isStreaming ? streamingContent : message.content;
|
||||
const toolCalls = isStreaming ? streamingToolCalls : message.toolCalls;
|
||||
|
||||
if (isUser) {
|
||||
const displayContent = (content || "").replace(
|
||||
/\n\n<referenced_pages>[\s\S]*<\/referenced_pages>$/,
|
||||
"",
|
||||
);
|
||||
const attachments =
|
||||
(message.metadata?.attachments as {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileExt: string;
|
||||
}[]) || [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.userMessage}
|
||||
role="article"
|
||||
aria-label={t("You said:")}
|
||||
>
|
||||
<div className={classes.userBubble}>
|
||||
{attachments.length > 0 && (
|
||||
<div className={classes.messageAttachments}>
|
||||
{attachments.map((a) => (
|
||||
<span key={a.id} className={classes.messageAttachmentChip}>
|
||||
{IMAGE_EXTENSIONS.includes(a.fileExt) ? (
|
||||
<IconPhoto size={13} />
|
||||
) : (
|
||||
<IconFile size={13} />
|
||||
)}
|
||||
{a.fileName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{displayContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Only label the article when there's something meaningful to announce.
|
||||
// Tool-only assistant turns (no text) shouldn't announce "Assistant said:" with empty content.
|
||||
const hasAnnouncableContent = Boolean(content);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.assistantMessage}
|
||||
role="article"
|
||||
aria-label={hasAnnouncableContent ? t("Assistant said:") : undefined}
|
||||
>
|
||||
<div className={classes.messageContent}>
|
||||
{toolCalls && toolCalls.length > 0 && (
|
||||
<ChatToolGroup toolCalls={toolCalls} isStreaming={isStreaming} />
|
||||
)}
|
||||
{content && (
|
||||
<div
|
||||
onClick={handleContentClick}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: chatSanitizer.sanitize(
|
||||
markdownToHtml(content) as string,
|
||||
{ ADD_ATTR: ["target", "rel"] },
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isStreaming && (
|
||||
<>
|
||||
{!content && (
|
||||
<span className={classes.processingIndicator}>
|
||||
<IconLoader2 size={16} className={classes.processingSpinner} />
|
||||
Thinking
|
||||
</span>
|
||||
)}
|
||||
<span className={classes.streamingCursor} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isStreaming && message.content && (
|
||||
<div className={classes.messageActions}>
|
||||
<CopyTextButton
|
||||
text={message?.content}
|
||||
label={t("Copy assistant response")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconChevronDown,
|
||||
IconLoader2,
|
||||
} from "@tabler/icons-react";
|
||||
import type { AiChatToolCall } from "../types/ai-chat.types";
|
||||
import ChatToolResult, { TOOL_LABELS } from "./chat-tool-result";
|
||||
import classes from "../styles/chat-message.module.css";
|
||||
|
||||
type Props = {
|
||||
toolCalls: AiChatToolCall[];
|
||||
isStreaming?: boolean;
|
||||
};
|
||||
|
||||
export default function ChatToolGroup({ toolCalls, isStreaming }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
if (!toolCalls || toolCalls.length === 0) return null;
|
||||
|
||||
const activeCall =
|
||||
isStreaming && toolCalls.length > 0
|
||||
? [...toolCalls].reverse().find((tc) => tc.result === undefined)
|
||||
: undefined;
|
||||
|
||||
const activeLabel = activeCall
|
||||
? TOOL_LABELS[activeCall.name] || activeCall.name
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={classes.toolGroup}>
|
||||
<div
|
||||
className={classes.toolGroupHeader}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
setExpanded((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{activeLabel ? (
|
||||
<IconLoader2 size={12} className={classes.processingSpinner} />
|
||||
) : expanded ? (
|
||||
<IconChevronDown size={12} />
|
||||
) : (
|
||||
<IconChevronRight size={12} />
|
||||
)}
|
||||
<span className={classes.toolGroupLabel}>
|
||||
{activeLabel ? `${activeLabel}…` : `Steps ${toolCalls.length}`}
|
||||
</span>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className={classes.toolGroupSteps}>
|
||||
{toolCalls.map((tc) => (
|
||||
<ChatToolResult key={tc.id} toolCall={tc} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { IconChevronRight, IconChevronDown } from "@tabler/icons-react";
|
||||
import type { AiChatToolCall } from "../types/ai-chat.types";
|
||||
import classes from "../styles/chat-message.module.css";
|
||||
|
||||
export const TOOL_LABELS: Record<string, string> = {
|
||||
list_spaces: "Listed spaces",
|
||||
search_pages: "Searched pages",
|
||||
get_page: "Read page",
|
||||
create_page: "Created page",
|
||||
update_page: "Updated page",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
toolCall: AiChatToolCall;
|
||||
};
|
||||
|
||||
export default function ChatToolResult({ toolCall }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const label = TOOL_LABELS[toolCall.name] || toolCall.name;
|
||||
|
||||
return (
|
||||
<div className={classes.toolStep}>
|
||||
<div
|
||||
className={classes.toolStepRow}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
>
|
||||
<span className={classes.toolStepBullet}>·</span>
|
||||
{expanded ? (
|
||||
<IconChevronDown size={12} />
|
||||
) : (
|
||||
<IconChevronRight size={12} />
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className={classes.toolStepDetails}>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{JSON.stringify(
|
||||
{ args: toolCall.args, result: toolCall.result },
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function EnableAiChat() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="md">{t("AI Chat")}</Text>
|
||||
<Badge color="gray" variant="light" size="sm" radius="sm">
|
||||
{t("Beta")}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Enable AI Chat to allow users to have multi-turn conversations with AI about your workspace content.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<AiChatToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function AiChatToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.settings?.ai?.chat);
|
||||
const hasAccess = useHasFeature(Feature.AI);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({ aiChat: value } as any);
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err: any) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle AI Chat")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { sendChatMessage } from "../services/ai-chat-service";
|
||||
import type {
|
||||
AiChatMessage,
|
||||
AiChatStreamEvent,
|
||||
AiChatToolCall,
|
||||
ChatAttachment,
|
||||
PageMention,
|
||||
} from "../types/ai-chat.types";
|
||||
|
||||
type ChatStreamOptions = {
|
||||
onChatCreated?: (chatId: string) => void;
|
||||
};
|
||||
|
||||
export function useChatStream(
|
||||
chatId: string | undefined,
|
||||
options?: ChatStreamOptions,
|
||||
) {
|
||||
const [messages, setMessages] = useState<AiChatMessage[]>([]);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [streamingToolCalls, setStreamingToolCalls] = useState<AiChatToolCall[]>(
|
||||
[],
|
||||
);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [errorCode, setErrorCode] = useState<string | null>(null);
|
||||
const [isRetryable, setIsRetryable] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const currentChatIdRef = useRef(chatId);
|
||||
currentChatIdRef.current = chatId;
|
||||
// Tracks which chatId the local `messages` state currently represents.
|
||||
// Set when we seed from a server fetch AND when we optimistically own a
|
||||
// freshly-created chat after `chat_created`. This is the single authority
|
||||
// marker that keeps server-state effects from clobbering in-flight streams.
|
||||
const hydratedChatIdRef = useRef<string | undefined>(undefined);
|
||||
|
||||
// Reset local state when the consumer switches to a different chat.
|
||||
// Skip the reset if the new chatId is one the hook itself already claimed
|
||||
// during a new-chat flow — in that case our optimistic state is the truth.
|
||||
useEffect(() => {
|
||||
if (chatId && chatId === hydratedChatIdRef.current) return;
|
||||
hydratedChatIdRef.current = undefined;
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
setErrorCode(null);
|
||||
setIsRetryable(false);
|
||||
}, [chatId]);
|
||||
|
||||
const hydrateFromServer = useCallback((msgs: AiChatMessage[]) => {
|
||||
const forId = currentChatIdRef.current;
|
||||
if (!forId) return;
|
||||
if (hydratedChatIdRef.current === forId) return;
|
||||
hydratedChatIdRef.current = forId;
|
||||
setMessages(msgs);
|
||||
}, []);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(content: string, mentions: PageMention[] = [], attachments: ChatAttachment[] = [], contextPageId?: string) => {
|
||||
if (isStreaming || (!content.trim() && attachments.length === 0)) return;
|
||||
|
||||
setError(null);
|
||||
setErrorCode(null);
|
||||
setIsRetryable(false);
|
||||
setIsStreaming(true);
|
||||
setStreamingContent("");
|
||||
setStreamingToolCalls([]);
|
||||
|
||||
const metadata: Record<string, unknown> = {};
|
||||
if (mentions.length) {
|
||||
metadata.mentionedPageIds = mentions.map((m) => m.id);
|
||||
}
|
||||
if (attachments.length) {
|
||||
metadata.attachments = attachments.map((a) => ({
|
||||
id: a.id,
|
||||
fileName: a.fileName,
|
||||
fileExt: a.fileExt,
|
||||
}));
|
||||
}
|
||||
|
||||
const userMessage: AiChatMessage = {
|
||||
id: `temp-${Date.now()}`,
|
||||
chatId: currentChatIdRef.current || "",
|
||||
role: "user",
|
||||
content,
|
||||
toolCalls: null,
|
||||
metadata: Object.keys(metadata).length ? metadata : null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
|
||||
const attachmentIds = attachments.map((a) => a.id);
|
||||
|
||||
const abortController = sendChatMessage(
|
||||
{
|
||||
chatId: currentChatIdRef.current,
|
||||
content,
|
||||
mentionedPageIds: mentions.map((m) => m.id),
|
||||
...(contextPageId && { contextPageId }),
|
||||
...(attachmentIds.length && { attachmentIds }),
|
||||
},
|
||||
(event: AiChatStreamEvent) => {
|
||||
switch (event.type) {
|
||||
case "chat_created":
|
||||
currentChatIdRef.current = event.chatId;
|
||||
// Claim authority over this new chatId so when the consumer's
|
||||
// prop catches up via navigation/onChatCreated, the reset effect
|
||||
// sees a match and preserves our optimistic messages.
|
||||
hydratedChatIdRef.current = event.chatId;
|
||||
if (options?.onChatCreated) {
|
||||
options.onChatCreated(event.chatId);
|
||||
} else {
|
||||
navigate(`/ai/chat/${event.chatId}`, { replace: true });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
|
||||
break;
|
||||
case "content":
|
||||
setStreamingContent((prev) => prev + event.text);
|
||||
break;
|
||||
case "tool_call":
|
||||
setStreamingToolCalls((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
args: event.args,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "tool_result":
|
||||
setStreamingToolCalls((prev) =>
|
||||
prev.map((tc) =>
|
||||
tc.id === event.id ? { ...tc, result: event.result } : tc,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "done": {
|
||||
setStreamingContent((currentContent) => {
|
||||
setStreamingToolCalls((currentToolCalls) => {
|
||||
const assistantMessage: AiChatMessage = {
|
||||
id: event.messageId,
|
||||
chatId: currentChatIdRef.current || "",
|
||||
role: "assistant",
|
||||
content: currentContent || null,
|
||||
toolCalls: currentToolCalls.length
|
||||
? currentToolCalls
|
||||
: null,
|
||||
metadata: event.usage ? { tokenUsage: event.usage } : null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
return [];
|
||||
});
|
||||
return "";
|
||||
});
|
||||
setIsStreaming(false);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ai-chat", currentChatIdRef.current],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "error":
|
||||
setError(event.message);
|
||||
setErrorCode(event.code || null);
|
||||
setIsRetryable(event.retryable || false);
|
||||
setIsStreaming(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
(errorMsg) => {
|
||||
setError(errorMsg);
|
||||
setIsStreaming(false);
|
||||
},
|
||||
() => {
|
||||
setIsStreaming(false);
|
||||
},
|
||||
);
|
||||
|
||||
abortRef.current = abortController;
|
||||
},
|
||||
[isStreaming, navigate, queryClient],
|
||||
);
|
||||
|
||||
const stopGeneration = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = null;
|
||||
|
||||
setStreamingContent((currentContent) => {
|
||||
setStreamingToolCalls((currentToolCalls) => {
|
||||
if (currentContent || currentToolCalls.length > 0) {
|
||||
const partialMessage: AiChatMessage = {
|
||||
id: `stopped-${Date.now()}`,
|
||||
chatId: currentChatIdRef.current || "",
|
||||
role: "assistant",
|
||||
content: currentContent || null,
|
||||
toolCalls: currentToolCalls.length ? currentToolCalls : null,
|
||||
metadata: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, partialMessage]);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
return "";
|
||||
});
|
||||
|
||||
setIsStreaming(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
messages,
|
||||
streamingContent,
|
||||
streamingToolCalls,
|
||||
isStreaming,
|
||||
error,
|
||||
errorCode,
|
||||
isRetryable,
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
hydrateFromServer,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Button } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AiChatLayout from "../components/ai-chat-layout";
|
||||
import { EmptyState } from "@/components/ui/empty-state.tsx";
|
||||
import classes from "../styles/ai-chat.module.css";
|
||||
|
||||
export default function AiChat() {
|
||||
const { t } = useTranslation();
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
|
||||
return (
|
||||
<div className={classes.layout}>
|
||||
<ErrorBoundary
|
||||
resetKeys={[chatId]}
|
||||
fallbackRender={({ resetErrorBoundary }) => (
|
||||
<EmptyState
|
||||
icon={IconAlertTriangle}
|
||||
title={t("Failed to load chat. An error occurred.")}
|
||||
action={
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
mt="xs"
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
{t("Try again")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<AiChatLayout />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
useQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
useInfiniteQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
listChats,
|
||||
getChatInfo,
|
||||
deleteChat,
|
||||
updateChatTitle,
|
||||
searchChats,
|
||||
} from "../services/ai-chat-service";
|
||||
|
||||
export function useChatsQuery() {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["ai-chats"],
|
||||
queryFn: ({ pageParam }) =>
|
||||
listChats({ cursor: pageParam, limit: 30 }),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function useChatInfoQuery(chatId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["ai-chat", chatId],
|
||||
queryFn: () => getChatInfo(chatId!),
|
||||
enabled: !!chatId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteChatMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (chatId: string) => deleteChat(chatId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateChatTitleMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ chatId, title }: { chatId: string; title: string }) =>
|
||||
updateChatTitle(chatId, title),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSearchChatsQuery(query: string) {
|
||||
return useQuery({
|
||||
queryKey: ["ai-chats-search", query],
|
||||
queryFn: () => searchChats(query),
|
||||
enabled: query.length > 0,
|
||||
});
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import type {
|
||||
AiChat,
|
||||
AiChatMessage,
|
||||
AiChatStreamEvent,
|
||||
ChatAttachment,
|
||||
} from "../types/ai-chat.types";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
|
||||
export async function createChat(): Promise<AiChat> {
|
||||
const req = await api.post<AiChat>("/ai/chats/create");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function listChats(params?: {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}): Promise<IPagination<AiChat>> {
|
||||
const req = await api.post("/ai/chats", params);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getChatInfo(
|
||||
chatId: string,
|
||||
): Promise<{ chat: AiChat; messages: AiChatMessage[] }> {
|
||||
const req = await api.post("/ai/chats/info", { chatId });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function deleteChat(chatId: string): Promise<void> {
|
||||
await api.post("/ai/chats/delete", { chatId });
|
||||
}
|
||||
|
||||
export async function updateChatTitle(
|
||||
chatId: string,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
await api.post("/ai/chats/update", { chatId, title });
|
||||
}
|
||||
|
||||
export async function searchChats(query: string): Promise<AiChat[]> {
|
||||
const req = await api.post("/ai/chats/search", { query });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function uploadChatFile(
|
||||
file: File,
|
||||
chatId?: string,
|
||||
): Promise<ChatAttachment> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
if (chatId) {
|
||||
formData.append("chatId", chatId);
|
||||
}
|
||||
return await api.post("/ai/chats/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
|
||||
export function sendChatMessage(
|
||||
params: {
|
||||
chatId?: string;
|
||||
content: string;
|
||||
mentionedPageIds?: string[];
|
||||
contextPageId?: string;
|
||||
attachmentIds?: string[];
|
||||
},
|
||||
onEvent: (event: AiChatStreamEvent) => void,
|
||||
onError?: (error: string) => void,
|
||||
onComplete?: () => void,
|
||||
): AbortController {
|
||||
const abortController = new AbortController();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/ai/chats/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
let errorMessage = `HTTP error ${response.status}`;
|
||||
try {
|
||||
const parsed = JSON.parse(errorBody);
|
||||
errorMessage = parsed.message || errorMessage;
|
||||
} catch {
|
||||
// use default
|
||||
}
|
||||
onError?.(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (!reader) {
|
||||
onError?.("Response body is not readable");
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6);
|
||||
if (data === "[DONE]") {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data) as AiChatStreamEvent;
|
||||
onEvent(parsed);
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
} catch (error: any) {
|
||||
if (error.name !== "AbortError") {
|
||||
onError?.(error.message);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return abortController;
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 45px - 2 * var(--mantine-spacing-md));
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.messageListWrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.messageList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--mantine-spacing-md) var(--mantine-spacing-lg);
|
||||
}
|
||||
|
||||
.messageErrorFallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: var(--mantine-spacing-lg);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
}
|
||||
|
||||
.scrollToBottomButton {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.scrollToBottomButton:hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
||||
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
|
||||
}
|
||||
|
||||
.scrollToBottomButton:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-lg) var(--mantine-spacing-lg);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--mantine-spacing-xl) var(--mantine-spacing-lg);
|
||||
}
|
||||
|
||||
.emptyStateIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
}
|
||||
|
||||
.emptyStateBrand {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mantine-color-dimmed);
|
||||
margin-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.emptyStateTitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--mantine-spacing-xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyStateInput {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-bottom: var(--mantine-spacing-xl);
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.suggestionsSection {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.suggestionsLabel {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--mantine-color-dimmed);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.suggestionsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.suggestionCard {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--mantine-spacing-sm);
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-md);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
transition: background-color 150ms, border-color 150ms;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
@mixin hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
}
|
||||
|
||||
.suggestionIcon {
|
||||
flex-shrink: 0;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.suggestionText {
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 0 var(--mantine-spacing-sm) 0;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.toolbarSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.titleButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
max-width: 60%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleButton:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
.titleText {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--mantine-spacing-sm) 0;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
padding-top: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--mantine-spacing-md);
|
||||
padding: var(--mantine-spacing-xl) var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.emptyStateIcon {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
}
|
||||
|
||||
.emptyStateTitle {
|
||||
font-size: var(--mantine-font-size-lg);
|
||||
font-weight: 600;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quickActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quickAction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--mantine-spacing-sm);
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-md);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: background-color 150ms, border-color 150ms;
|
||||
|
||||
@mixin hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
}
|
||||
|
||||
.quickActionIcon {
|
||||
flex-shrink: 0;
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
}
|
||||
|
||||
.historyList {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.historyItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
cursor: pointer;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
transition: background-color 150ms;
|
||||
|
||||
@mixin hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
|
||||
.historyItemTitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid
|
||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: 16px;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
box-shadow: light-dark(
|
||||
0 2px 40px 4px rgba(0, 0, 0, 0.07),
|
||||
0 2px 40px 4px rgba(0, 0, 0, 0.5)
|
||||
);
|
||||
transition:
|
||||
border-color 150ms,
|
||||
box-shadow 150ms;
|
||||
|
||||
&:focus-within {
|
||||
border-color: light-dark(
|
||||
var(--mantine-color-gray-3),
|
||||
var(--mantine-color-dark-4)
|
||||
);
|
||||
box-shadow: light-dark(
|
||||
0 4px 48px 6px rgba(0, 0, 0, 0.09),
|
||||
0 4px 48px 6px rgba(0, 0, 0, 0.6)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapperFlat {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: 12px;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
box-shadow: none;
|
||||
transition: border-color 150ms;
|
||||
|
||||
&:focus-within {
|
||||
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: var(--mantine-color-dimmed);
|
||||
}
|
||||
|
||||
.attachmentChips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
|
||||
.attachmentChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.attachmentChipUploading {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.attachmentChipName {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachmentChipRemove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 2px;
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
border-radius: 50%;
|
||||
|
||||
@mixin hover {
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
}
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
overflow: hidden;
|
||||
|
||||
:global(.ProseMirror) {
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 14px 18px 8px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
min-height: 24px;
|
||||
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
|
||||
}
|
||||
|
||||
:global(.ProseMirror p) {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
:global(.ProseMirror p.is-editor-empty:first-child::before) {
|
||||
color: var(--mantine-color-placeholder);
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 12px 10px;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms, opacity 150ms;
|
||||
background: light-dark(var(--mantine-color-dark-9), var(--mantine-color-gray-0));
|
||||
color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-9));
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@mixin hover {
|
||||
&:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
transition: color 150ms;
|
||||
|
||||
@mixin hover {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
}
|
||||
}
|
||||
|
||||
.plusButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
|
||||
transition: color 150ms, background-color 150ms;
|
||||
|
||||
@mixin hover {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
}
|
||||
|
||||
.plusMenuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--mantine-spacing-sm);
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
transition: background-color 150ms;
|
||||
|
||||
@mixin hover {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.plusMenuIcon {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
}
|
||||
|
||||
.stopButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
|
||||
@mixin hover {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
.message {
|
||||
margin-bottom: var(--mantine-spacing-lg);
|
||||
}
|
||||
|
||||
.userMessage {
|
||||
composes: message;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.userBubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 16px;
|
||||
border-radius: 18px;
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
[data-aside-chat] .userBubble {
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.userBubble p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.messageAttachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.messageAttachmentChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistantMessage {
|
||||
composes: message;
|
||||
}
|
||||
|
||||
.messageContent {
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-1));
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.messageContent p {
|
||||
margin: 0 0 0.75em 0;
|
||||
}
|
||||
|
||||
.messageContent p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.messageContent ul,
|
||||
.messageContent ol {
|
||||
margin: 0.5em 0 0.75em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.messageContent li {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.messageContent h1,
|
||||
.messageContent h2,
|
||||
.messageContent h3 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-weight: 600;
|
||||
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
|
||||
}
|
||||
|
||||
.messageContent h1 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.messageContent h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.messageContent h3 {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
.messageContent pre {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
overflow-x: auto;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.messageContent code {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.messageContent pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.messageContent blockquote {
|
||||
border-left: 3px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin: 0.75em 0;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
|
||||
}
|
||||
|
||||
.messageContent a {
|
||||
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4));
|
||||
text-decoration: none;
|
||||
|
||||
@mixin hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.messageContent a[href^="/s/"],
|
||||
.messageContent a[href^="/p/"] {
|
||||
color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
@mixin light {
|
||||
border-bottom: 0.05em solid var(--mantine-color-dark-0);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
border-bottom: 0.05em solid var(--mantine-color-dark-2);
|
||||
}
|
||||
|
||||
@mixin hover {
|
||||
text-decoration: none;
|
||||
@mixin light {
|
||||
border-bottom-color: var(--mantine-color-dark-2);
|
||||
}
|
||||
@mixin dark {
|
||||
border-bottom-color: var(--mantine-color-dark-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageContent hr {
|
||||
border: none;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.toolGroup {
|
||||
margin: 6px 0;
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
}
|
||||
|
||||
.toolGroupHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
line-height: 1.4;
|
||||
transition: color 120ms ease;
|
||||
}
|
||||
|
||||
.toolGroupHeader:hover {
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
}
|
||||
|
||||
.toolGroupLabel {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolGroupSteps {
|
||||
margin-top: 4px;
|
||||
padding-left: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolStep {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
}
|
||||
|
||||
.toolStepRow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
line-height: 1.5;
|
||||
transition: color 120ms ease;
|
||||
}
|
||||
|
||||
.toolStepRow:hover {
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
}
|
||||
|
||||
.toolStepBullet {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.toolStepDetails {
|
||||
margin-top: 4px;
|
||||
margin-left: 18px;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.messageActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
}
|
||||
|
||||
.processingIndicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
}
|
||||
|
||||
.processingSpinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.streamingCursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 1em;
|
||||
background: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
animation: blink 1s step-end infinite;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
margin-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.chatList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chatGroup + .chatGroup {
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.chatGroupLabel {
|
||||
margin: 0;
|
||||
padding: 4px var(--mantine-spacing-xs);
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--mantine-color-dimmed);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chatListEmpty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chatListEmptyIcon {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
margin-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.chatListEmptyTitle {
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 600;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
}
|
||||
|
||||
.chatListEmptyHint {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chatItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px var(--mantine-spacing-xs);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
user-select: none;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-2),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.chatItemTitle {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chatItemDate {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: var(--mantine-color-dimmed);
|
||||
white-space: nowrap;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.chatItemRenameInput {
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.chatItem:hover .chatItemDate,
|
||||
.chatItem:focus-within .chatItemDate {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.chatItemActions {
|
||||
position: absolute;
|
||||
right: var(--mantine-spacing-xs);
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.chatItem {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chatItem:hover .chatItemActions,
|
||||
.chatItem:focus-within .chatItemActions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chatItemActions :global(.mantine-ActionIcon-root):focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
export type AiChat = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
creatorId: string;
|
||||
title: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type AiChatToolCall = {
|
||||
id: string;
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
};
|
||||
|
||||
export type AiChatMessage = {
|
||||
id: string;
|
||||
chatId: string;
|
||||
role: 'user' | 'assistant' | 'tool';
|
||||
content: string | null;
|
||||
toolCalls: AiChatToolCall[] | null;
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type AiChatStreamEvent =
|
||||
| { type: 'chat_created'; chatId: string }
|
||||
| { type: 'content'; text: string }
|
||||
| { type: 'tool_call'; id: string; name: string; args: Record<string, unknown> }
|
||||
| { type: 'tool_result'; id: string; result: unknown }
|
||||
| { type: 'done'; messageId: string; usage?: Record<string, number> }
|
||||
| { type: 'error'; message: string; code?: string; retryable?: boolean };
|
||||
|
||||
export type PageMention = {
|
||||
id: string;
|
||||
title: string;
|
||||
slugId: string;
|
||||
spaceSlug?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export type ChatAttachment = {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileExt: string;
|
||||
fileSize: number;
|
||||
mimeType: string;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { AiChat } from "../types/ai-chat.types";
|
||||
|
||||
export type ChatGroup = { key: string; label: string; chats: AiChat[] };
|
||||
|
||||
export function groupChatsByAge(
|
||||
chats: AiChat[],
|
||||
t: (key: string) => string,
|
||||
): ChatGroup[] {
|
||||
if (chats.length === 0) return [];
|
||||
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
).getTime();
|
||||
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
||||
const startOfLast7 = startOfToday - 7 * 24 * 60 * 60 * 1000;
|
||||
const startOfLast30 = startOfToday - 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const buckets: Record<string, ChatGroup> = {
|
||||
today: { key: "today", label: t("Today"), chats: [] },
|
||||
yesterday: { key: "yesterday", label: t("Yesterday"), chats: [] },
|
||||
last7: { key: "last7", label: t("Previous 7 days"), chats: [] },
|
||||
last30: { key: "last30", label: t("Previous 30 days"), chats: [] },
|
||||
older: { key: "older", label: t("Older"), chats: [] },
|
||||
};
|
||||
|
||||
for (const chat of chats) {
|
||||
const ts = new Date(chat.updatedAt).getTime();
|
||||
if (ts >= startOfToday) buckets.today.chats.push(chat);
|
||||
else if (ts >= startOfYesterday) buckets.yesterday.chats.push(chat);
|
||||
else if (ts >= startOfLast7) buckets.last7.chats.push(chat);
|
||||
else if (ts >= startOfLast30) buckets.last30.chats.push(chat);
|
||||
else buckets.older.chats.push(chat);
|
||||
}
|
||||
|
||||
return [
|
||||
buckets.today,
|
||||
buckets.yesterday,
|
||||
buckets.last7,
|
||||
buckets.last30,
|
||||
buckets.older,
|
||||
].filter((b) => b.chats.length > 0);
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { ActionIcon, TextInput } from "@mantine/core";
|
||||
import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type JSX,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { IconArrowUp } from "@tabler/icons-react";
|
||||
@@ -21,7 +14,7 @@ import { ResultPreview } from "./result-preview.tsx";
|
||||
import classes from "./ai-menu.module.css";
|
||||
import { marked } from "marked";
|
||||
import { DOMSerializer } from "@tiptap/pm/model";
|
||||
import { copyToClipboard, htmlToMarkdown, isEditorReady } from "@docmost/editor-ext";
|
||||
import { copyToClipboard, htmlToMarkdown } from "@docmost/editor-ext";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
interface EditorAiMenuProps {
|
||||
@@ -56,7 +49,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
});
|
||||
}, [prompt, output, activeCommandSet]);
|
||||
const updateMenuPlacement = useCallback(() => {
|
||||
if (!isEditorReady(editor) || !showAiMenu) return;
|
||||
if (!editor || !showAiMenu) return;
|
||||
|
||||
const { view } = editor;
|
||||
const { from, to } = editor.state.selection;
|
||||
@@ -102,7 +95,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
);
|
||||
const handleGenerate = useCallback(
|
||||
(item?: CommandItem) => {
|
||||
if (!isEditorReady(editor) || isLoading) return;
|
||||
if (!editor || isLoading) return;
|
||||
|
||||
let command: CommandItem | null = item || null;
|
||||
|
||||
@@ -165,7 +158,6 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
return setActiveCommandSet("main");
|
||||
}
|
||||
if (item.id === "result-replace") {
|
||||
if (!isEditorReady(editor)) return setShowAiMenu(false);
|
||||
const chain = editor.chain().focus();
|
||||
|
||||
if (lastAction.action === AiAction.CONTINUE_WRITING) {
|
||||
@@ -191,7 +183,6 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
if (item.id === "result-insert-below") {
|
||||
if (!isEditorReady(editor)) return setShowAiMenu(false);
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@@ -255,7 +246,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
if (!editor) return;
|
||||
|
||||
const handleClose = () => setShowAiMenu(false);
|
||||
const observer = new ResizeObserver(() => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
|
||||
import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
|
||||
import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx";
|
||||
import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
|
||||
import { Alert, Stack, Tabs } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
@@ -72,7 +71,6 @@ export default function AiSettings() {
|
||||
<Stack gap="md">
|
||||
{!isCloud() && <EnableAiSearch />}
|
||||
<EnableGenerativeAi />
|
||||
<EnableAiChat />
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
||||
@@ -31,9 +31,8 @@ export function ApiKeyCreatedModal({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("{{credential}} created", { credential: t("API key") })}
|
||||
title={t("API key created")}
|
||||
size="lg"
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
@@ -42,8 +41,7 @@ export function ApiKeyCreatedModal({
|
||||
color="red"
|
||||
>
|
||||
{t(
|
||||
"Make sure to copy your {{credential}} now. You won't be able to see it again!",
|
||||
{ credential: t("API key") },
|
||||
"Make sure to copy your API key now. You won't be able to see it again!",
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
@@ -66,7 +64,7 @@ export function ApiKeyCreatedModal({
|
||||
</div>
|
||||
|
||||
<Button fullWidth onClick={onClose} mt="md">
|
||||
{t("I've saved my {{credential}}", { credential: t("API key") })}
|
||||
{t("I've saved my API key")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
||||
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { format } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IApiKey } from "@/ee/api-key";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import React from "react";
|
||||
import NoTableResults from "@/components/common/no-table-results";
|
||||
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
|
||||
|
||||
interface ApiKeyTableProps {
|
||||
apiKeys: IApiKey[];
|
||||
@@ -23,11 +23,10 @@ export function ApiKeyTable({
|
||||
onRevoke,
|
||||
}: ApiKeyTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const locale = useDateFnsLocale();
|
||||
|
||||
const formatDate = (date: Date | string | null) => {
|
||||
if (!date) return t("Never");
|
||||
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
|
||||
return format(new Date(date), "MMM dd, yyyy");
|
||||
};
|
||||
|
||||
const isExpired = (expiresAt: string | null) => {
|
||||
@@ -45,7 +44,7 @@ export function ApiKeyTable({
|
||||
<Table.Th>{t("Last used")}</Table.Th>
|
||||
<Table.Th>{t("Expires")}</Table.Th>
|
||||
<Table.Th>{t("Created")}</Table.Th>
|
||||
<Table.Th aria-label={t("Action")} />
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@@ -107,11 +106,7 @@ export function ApiKeyTable({
|
||||
<Table.Td>
|
||||
<Menu position="bottom-end" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("API key menu")}
|
||||
>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function CreateApiKeyModal({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: CreateApiKeyModalProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [expirationOption, setExpirationOption] = useState<string>("30");
|
||||
const createApiKeyMutation = useCreateApiKeyMutation();
|
||||
|
||||
@@ -59,7 +59,7 @@ export function CreateApiKeyModal({
|
||||
const getExpirationLabel = (days: number) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + days);
|
||||
const formatted = date.toLocaleDateString(i18n.language, {
|
||||
const formatted = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
@@ -105,9 +105,8 @@ export function CreateApiKeyModal({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={handleClose}
|
||||
title={t("Create {{credential}}", { credential: t("API key") })}
|
||||
title={t("Create API Key")}
|
||||
size="md"
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<Stack gap="md">
|
||||
|
||||
@@ -30,15 +30,12 @@ export function RevokeApiKeyModal({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("Revoke {{credential}}", { credential: t("API key") })}
|
||||
title={t("Revoke API key")}
|
||||
size="md"
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
{t("Are you sure you want to revoke this {{credential}}", {
|
||||
credential: t("API key"),
|
||||
})}{" "}
|
||||
{t("Are you sure you want to revoke this API key")}{" "}
|
||||
<strong>{apiKey?.name}</strong>?
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
|
||||
@@ -53,9 +53,8 @@ export function UpdateApiKeyModal({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("Update {{credential}}", { credential: t("API key") })}
|
||||
title={t("Update API key")}
|
||||
size="md"
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<Stack gap="md">
|
||||
|
||||
@@ -63,11 +63,7 @@ export function useCreateApiKeyMutation() {
|
||||
return useMutation<IApiKey, Error, ICreateApiKeyRequest>({
|
||||
mutationFn: (data) => createApiKey(data),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
message: t("{{credential}} created successfully", {
|
||||
credential: t("API key"),
|
||||
}),
|
||||
});
|
||||
notifications.show({ message: t("API key created successfully") });
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (item) =>
|
||||
["api-key-list"].includes(item.queryKey[0] as string),
|
||||
|
||||
@@ -301,7 +301,7 @@ export default function AuditLogsTable({
|
||||
{expandable && (
|
||||
<Table.Tr className={classes.detailRow}>
|
||||
<Table.Td colSpan={4} p={0}>
|
||||
<Collapse expanded={isExpanded}>
|
||||
<Collapse in={isExpanded}>
|
||||
<Box
|
||||
px="md"
|
||||
py="sm"
|
||||
|
||||
@@ -33,10 +33,6 @@ export const auditEventLabels: Record<string, string> = {
|
||||
"api_key.updated": "Updated API key",
|
||||
"api_key.deleted": "Deleted API key",
|
||||
|
||||
"scim_token.created": "Created SCIM token",
|
||||
"scim_token.updated": "Updated SCIM token",
|
||||
"scim_token.deleted": "Deleted SCIM token",
|
||||
|
||||
"space.created": "Created space",
|
||||
"space.updated": "Updated space",
|
||||
"space.deleted": "Deleted space",
|
||||
@@ -62,13 +58,6 @@ export const auditEventLabels: Record<string, string> = {
|
||||
"page.restriction_removed": "Removed page restriction",
|
||||
"page.permission_added": "Added page permission",
|
||||
"page.permission_removed": "Removed page permission",
|
||||
"page.verification_created": "Created page verification",
|
||||
"page.verification_updated": "Updated page verification",
|
||||
"page.verification_removed": "Removed page verification",
|
||||
"page.verified": "Verified page",
|
||||
"page.approval_requested": "Requested page approval",
|
||||
"page.approval_rejected": "Rejected page approval",
|
||||
"page.marked_obsolete": "Marked page as obsolete",
|
||||
|
||||
"share.created": "Created share link",
|
||||
"share.deleted": "Deleted share link",
|
||||
@@ -147,13 +136,6 @@ export const eventFilterOptions: EventGroup[] = [
|
||||
{ value: "page.restriction_removed", label: "Removed page restriction" },
|
||||
{ value: "page.permission_added", label: "Added page permission" },
|
||||
{ value: "page.permission_removed", label: "Removed page permission" },
|
||||
{ value: "page.verification_created", label: "Created page verification" },
|
||||
{ value: "page.verification_updated", label: "Updated page verification" },
|
||||
{ value: "page.verification_removed", label: "Removed page verification" },
|
||||
{ value: "page.verified", label: "Verified page" },
|
||||
{ value: "page.approval_requested", label: "Requested page approval" },
|
||||
{ value: "page.approval_rejected", label: "Rejected page approval" },
|
||||
{ value: "page.marked_obsolete", label: "Marked page as obsolete" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -178,14 +160,6 @@ export const eventFilterOptions: EventGroup[] = [
|
||||
{ value: "api_key.deleted", label: "Deleted API key" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "SCIM token",
|
||||
items: [
|
||||
{ value: "scim_token.created", label: "Created SCIM token" },
|
||||
{ value: "scim_token.updated", label: "Updated SCIM token" },
|
||||
{ value: "scim_token.deleted", label: "Deleted SCIM token" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "License",
|
||||
items: [
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { EditingCell, FocusedCell } from "@/ee/base/types/base.types";
|
||||
|
||||
// Atoms are scoped per-base via `pageId` so that two BaseTable instances on
|
||||
// the same page don't share UI state.
|
||||
|
||||
export const activeViewIdAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<string | null>(null),
|
||||
);
|
||||
|
||||
export const editingCellAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<EditingCell>(null),
|
||||
);
|
||||
|
||||
export type FormulaEditorTarget = {
|
||||
propertyId: string;
|
||||
rowId: string | null;
|
||||
} | null;
|
||||
|
||||
export const activeFormulaEditorAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<FormulaEditorTarget>(null),
|
||||
);
|
||||
|
||||
export const activePropertyMenuAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<string | null>(null),
|
||||
);
|
||||
|
||||
export const propertyMenuDirtyAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<boolean>(false),
|
||||
);
|
||||
|
||||
export const propertyMenuCloseRequestAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<number>(0),
|
||||
);
|
||||
|
||||
export const selectedRowIdsAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<Set<string>>(new Set<string>()),
|
||||
);
|
||||
|
||||
export const lastToggledRowIndexAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<number | null>(null),
|
||||
);
|
||||
|
||||
export const focusedCellAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<FocusedCell>(null),
|
||||
);
|
||||
|
||||
export type PendingTypeInsert = {
|
||||
rowId: string;
|
||||
propertyId: string;
|
||||
char: string;
|
||||
} | null;
|
||||
|
||||
export const pendingTypeInsertAtom = atom<PendingTypeInsert>(null);
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const formulaRecomputeAtom = atom<Record<string, string[]>>({});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import type { RowReferences } from "@/ee/base/types/base.types";
|
||||
|
||||
// Per-base normalized store of resolved reference entities, hydrated from each
|
||||
// rows-page `references`. Keyed by pageId, matching base-atoms.ts.
|
||||
export const referenceStoreAtomFamily = atomFamily((_pageId: string) =>
|
||||
atom<RowReferences>({ users: {}, pages: {} }),
|
||||
);
|
||||
|
||||
export function mergeReferences(
|
||||
prev: RowReferences,
|
||||
next: RowReferences | undefined,
|
||||
): RowReferences {
|
||||
if (!next) return prev;
|
||||
return {
|
||||
users: { ...prev.users, ...next.users },
|
||||
pages: { ...prev.pages, ...next.pages },
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { atomFamily, atomWithStorage } from "jotai/utils";
|
||||
import { BaseViewDraft } from "@/ee/base/types/base.types";
|
||||
|
||||
export type ViewDraftKey = {
|
||||
userId: string;
|
||||
pageId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const viewDraftStorageKey = (k: ViewDraftKey) =>
|
||||
`docmost:base-view-draft:v1:${k.userId}:${k.pageId}:${k.viewId}`;
|
||||
|
||||
// atomWithStorage handles JSON serialization and cross-tab sync. The custom
|
||||
// comparator ensures the same userId/pageId/viewId triple resolves to the
|
||||
// same atom instance, so Jotai's identity-equality cache hits still work.
|
||||
export const viewDraftAtomFamily = atomFamily(
|
||||
(k: ViewDraftKey) =>
|
||||
atomWithStorage<BaseViewDraft | null>(viewDraftStorageKey(k), null),
|
||||
(a, b) =>
|
||||
a.userId === b.userId && a.pageId === b.pageId && a.viewId === b.viewId,
|
||||
);
|
||||
@@ -1,91 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import {
|
||||
usePageQuery,
|
||||
useUpdateTitlePageMutation,
|
||||
updatePageData,
|
||||
} from "@/features/page/queries/page-query";
|
||||
import { useQueryEmit } from "@/features/websocket/use-query-emit";
|
||||
import { UpdateEvent } from "@/features/websocket/types";
|
||||
import localEmitter from "@/lib/local-emitter";
|
||||
import classes from "@/ee/base/styles/grid.module.css";
|
||||
|
||||
// Editable base name for the inline embed. Follows the TitleEditor convention
|
||||
// (updatePageData + localEmitter + websocket emit) so the sidebar and other
|
||||
// clients stay in sync. Standalone pages use the page TitleEditor instead.
|
||||
export function BaseEmbedTitle({ pageId }: { pageId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const { data: page } = usePageQuery({ pageId });
|
||||
const { mutateAsync: updateTitleAsync } = useUpdateTitlePageMutation();
|
||||
const emit = useQueryEmit();
|
||||
const [value, setValue] = useState("");
|
||||
const focusedRef = useRef(false);
|
||||
|
||||
// Keep in sync with the persisted title but never clobber active user input.
|
||||
useEffect(() => {
|
||||
if (!focusedRef.current) setValue(page?.title ?? "");
|
||||
}, [page?.title]);
|
||||
|
||||
const commit = useCallback(() => {
|
||||
const trimmed = value.trim();
|
||||
if (!page || trimmed === (page.title ?? "")) return;
|
||||
updateTitleAsync({ pageId, title: trimmed }).then((updated) => {
|
||||
if (updated.title !== trimmed) return;
|
||||
const event: UpdateEvent = {
|
||||
operation: "updateOne",
|
||||
spaceId: updated.spaceId,
|
||||
entity: ["pages"],
|
||||
id: updated.id,
|
||||
payload: {
|
||||
title: updated.title,
|
||||
slugId: updated.slugId,
|
||||
parentPageId: updated.parentPageId,
|
||||
icon: updated.icon,
|
||||
},
|
||||
};
|
||||
updatePageData(updated);
|
||||
localEmitter.emit("message", event);
|
||||
emit(event);
|
||||
});
|
||||
}, [value, page, pageId, updateTitleAsync, emit]);
|
||||
|
||||
const debouncedCommit = useDebouncedCallback(commit, 500);
|
||||
|
||||
// Force-save any pending edit on unmount (e.g. navigating away mid-type).
|
||||
const commitRef = useRef(commit);
|
||||
useEffect(() => {
|
||||
commitRef.current = commit;
|
||||
}, [commit]);
|
||||
useEffect(() => () => commitRef.current(), []);
|
||||
|
||||
return (
|
||||
<input
|
||||
className={classes.embedTitleInput}
|
||||
value={value}
|
||||
placeholder={t("Untitled base")}
|
||||
aria-label={t("Base name")}
|
||||
onChange={(e) => {
|
||||
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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
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 (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.toolbar}>
|
||||
<div className={classes.toolbarTabs}>
|
||||
<Skeleton height={22} width={44} radius="sm" />
|
||||
<Skeleton height={22} width={64} radius="sm" />
|
||||
<Skeleton height={22} width={48} radius="sm" />
|
||||
</div>
|
||||
<div className={classes.toolbarActions}>
|
||||
<Skeleton height={22} width={22} circle />
|
||||
<Skeleton height={22} width={22} circle />
|
||||
<Skeleton height={22} width={22} circle />
|
||||
<Skeleton height={22} width={22} circle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.gridWrapper}>
|
||||
<div className={classes.grid} style={{ gridTemplateColumns }}>
|
||||
<div className={gridClasses.headerCell}>
|
||||
<div className={classes.headerCellInner}>
|
||||
<Skeleton height={14} width={14} circle />
|
||||
</div>
|
||||
</div>
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<div key={`h-${colIndex}`} className={gridClasses.headerCell}>
|
||||
<div className={classes.headerCellInner}>
|
||||
<Skeleton height={14} width={14} circle />
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={`${HEADER_WIDTH_RATIOS[colIndex % HEADER_WIDTH_RATIOS.length] * 100}%`}
|
||||
radius="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} style={{ display: "contents" }}>
|
||||
<div className={gridClasses.cell}>
|
||||
<div className={classes.cellInner}>
|
||||
<Skeleton height={10} width={18} radius="sm" />
|
||||
</div>
|
||||
</div>
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<div
|
||||
key={`cell-${rowIndex}-${colIndex}`}
|
||||
className={gridClasses.cell}
|
||||
>
|
||||
<div className={classes.cellInner}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={`${CELL_WIDTH_RATIOS[(rowIndex + colIndex) % CELL_WIDTH_RATIOS.length] * 100}%`}
|
||||
radius="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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<IBaseRow>;
|
||||
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<HTMLDivElement>;
|
||||
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 (
|
||||
<GridContainer
|
||||
table={table}
|
||||
properties={base.properties}
|
||||
onCellUpdate={onCellUpdate}
|
||||
onAddRow={onAddRow}
|
||||
pageId={pageId}
|
||||
isFiltered={isFiltered}
|
||||
onColumnReorder={onColumnReorder}
|
||||
onResizeEnd={onResizeEnd}
|
||||
onRowReorder={onRowReorder}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={onFetchNextPage}
|
||||
scrollElement={embedded ? window : scrollportRef.current}
|
||||
aboveBand={aboveBand ?? null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
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<IBaseRow>;
|
||||
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<FilterCondition[]>(() => {
|
||||
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 (
|
||||
<div className={classes.toolbar}>
|
||||
<ViewTabs
|
||||
views={views}
|
||||
activeViewId={activeView?.id}
|
||||
pageId={base.id}
|
||||
onViewChange={onViewChange}
|
||||
onAddView={onAddView}
|
||||
base={base}
|
||||
canAddView={canAddView}
|
||||
getViewShareUrl={getViewShareUrl}
|
||||
/>
|
||||
|
||||
<div className={classes.toolbarRight}>
|
||||
<Tooltip label={t("Export CSV")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color="gray"
|
||||
loading={exporting}
|
||||
onClick={handleExport}
|
||||
>
|
||||
<IconDownload size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<ViewFilterConfigPopover
|
||||
opened={filterOpened}
|
||||
onClose={() => setFilterOpened(false)}
|
||||
conditions={conditions}
|
||||
properties={base.properties}
|
||||
onChange={handleFiltersChange}
|
||||
>
|
||||
<Tooltip label={t("Filter")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color={conditions.length > 0 ? "blue" : "gray"}
|
||||
onClick={() => openToolbar("filter")}
|
||||
>
|
||||
<IconFilter size={16} />
|
||||
{conditions.length > 0 && (
|
||||
<Badge
|
||||
size="xs"
|
||||
circle
|
||||
color="blue"
|
||||
className={toolbarClasses.badgeDot}
|
||||
>
|
||||
{conditions.length}
|
||||
</Badge>
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ViewFilterConfigPopover>
|
||||
|
||||
{isKanban && activeView && (
|
||||
<>
|
||||
<KanbanGroupByPicker base={base} view={activeView} pageId={base.id}>
|
||||
<Tooltip label={t("Group by")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color="gray"
|
||||
>
|
||||
<IconLayoutColumns size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</KanbanGroupByPicker>
|
||||
|
||||
<KanbanCardProperties
|
||||
opened={cardPropertiesOpened}
|
||||
onClose={() => setCardPropertiesOpened(false)}
|
||||
base={base}
|
||||
view={activeView}
|
||||
pageId={base.id}
|
||||
>
|
||||
<Tooltip label={t("Card properties")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={() => setCardPropertiesOpened((v) => !v)}
|
||||
>
|
||||
<IconAdjustments size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</KanbanCardProperties>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isKanban && (
|
||||
<>
|
||||
<ViewSortConfigPopover
|
||||
opened={sortOpened}
|
||||
onClose={() => setSortOpened(false)}
|
||||
sorts={sorts}
|
||||
properties={base.properties}
|
||||
onChange={handleSortsChange}
|
||||
>
|
||||
<Tooltip label={t("Sort")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color={sorts.length > 0 ? "blue" : "gray"}
|
||||
onClick={() => openToolbar("sort")}
|
||||
>
|
||||
<IconSortAscending size={16} />
|
||||
{sorts.length > 0 && (
|
||||
<Badge
|
||||
size="xs"
|
||||
circle
|
||||
color="blue"
|
||||
className={toolbarClasses.badgeDot}
|
||||
>
|
||||
{sorts.length}
|
||||
</Badge>
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ViewSortConfigPopover>
|
||||
|
||||
{table && (
|
||||
<ViewPropertyVisibility
|
||||
opened={propertiesOpened}
|
||||
onClose={() => setPropertiesOpened(false)}
|
||||
table={table}
|
||||
properties={base.properties}
|
||||
onPersist={onPersistViewConfig}
|
||||
>
|
||||
<Tooltip label={t("Hide properties")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color={hiddenPropertyCount > 0 ? "blue" : "gray"}
|
||||
onClick={() => openToolbar("properties")}
|
||||
>
|
||||
<IconEye size={16} />
|
||||
{hiddenPropertyCount > 0 && (
|
||||
<Badge
|
||||
size="xs"
|
||||
circle
|
||||
color="blue"
|
||||
className={toolbarClasses.badgeDot}
|
||||
>
|
||||
{hiddenPropertyCount}
|
||||
</Badge>
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ViewPropertyVisibility>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{onExpand && (
|
||||
<Tooltip label={t("Open as page")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={onExpand}
|
||||
>
|
||||
<IconArrowsDiagonal size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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 (
|
||||
<Group justify="flex-end" gap="xs" px="md" py={6} wrap="nowrap">
|
||||
<Button variant="subtle" color="gray" size="xs" onClick={onReset}>
|
||||
{t("Reset")}
|
||||
</Button>
|
||||
{canSave && (
|
||||
<Tooltip
|
||||
label={t("Filter and sort changes are visible only to you")}
|
||||
position="top"
|
||||
withArrow
|
||||
>
|
||||
<Button
|
||||
variant="light"
|
||||
color="orange"
|
||||
size="xs"
|
||||
onClick={onSave}
|
||||
loading={saving}
|
||||
>
|
||||
{t("Save for everyone")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
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=<id> 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<HTMLDivElement>(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 <BaseTableSkeleton />;
|
||||
}
|
||||
if (baseError) {
|
||||
return (
|
||||
<Stack align="center" gap="sm" p="xl">
|
||||
<IconTable size={40} color="var(--mantine-color-gray-5)" />
|
||||
<Text c="dimmed">{t("Failed to load base")}</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
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 = (
|
||||
<BaseViewDraftBanner
|
||||
isDirty={isDirty}
|
||||
canSave={canSave}
|
||||
onReset={resetDraft}
|
||||
onSave={handleSaveDraft}
|
||||
saving={updateViewMutation.isPending}
|
||||
/>
|
||||
);
|
||||
|
||||
const toolbar = (
|
||||
<BaseToolbar
|
||||
base={base}
|
||||
activeView={effectiveView}
|
||||
views={views}
|
||||
table={table}
|
||||
onViewChange={handleViewChange}
|
||||
canAddView={editable}
|
||||
onPersistViewConfig={guardedPersistViewConfig}
|
||||
onDraftSortsChange={handleDraftSortsChange}
|
||||
onDraftFiltersChange={handleDraftFiltersChange}
|
||||
onExpand={embedded ? handleExpand : undefined}
|
||||
getViewShareUrl={getViewShareUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
const kanbanBand = (
|
||||
<div className={kanbanClasses.bandWrap}>
|
||||
{embedded ? null : titleSlot}
|
||||
{banner}
|
||||
{toolbar}
|
||||
{embedded ? <BaseEmbedTitle pageId={pageId} /> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const viewRenderer = (folded: React.ReactNode) => (
|
||||
<ViewRenderer
|
||||
base={base}
|
||||
rows={rows}
|
||||
effectiveView={effectiveView}
|
||||
table={table}
|
||||
pageId={pageId}
|
||||
embedded={embedded}
|
||||
editable={editable}
|
||||
isFiltered={isFiltered}
|
||||
hasNextPage={!!hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
onCellUpdate={handleCellUpdate}
|
||||
onAddRow={handleAddRow}
|
||||
onColumnReorder={editable ? handleColumnReorder : undefined}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onRowReorder={editable ? handleRowReorder : undefined}
|
||||
persistViewConfig={guardedPersistViewConfig}
|
||||
scrollportRef={scrollportRef}
|
||||
kanbanFilter={activeFilter}
|
||||
aboveBand={folded}
|
||||
/>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
if (isKanban) {
|
||||
return (
|
||||
<BaseEditableProvider editable={editable}>
|
||||
<RowExpandProvider value={handleExpandRow}>
|
||||
{kanbanBand}
|
||||
{viewRenderer(null)}
|
||||
</RowExpandProvider>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
onNavigate={handleRowNavigate}
|
||||
/>
|
||||
</BaseEditableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<BaseEditableProvider editable={editable}>
|
||||
<RowExpandProvider value={handleExpandRow}>
|
||||
{viewRenderer(
|
||||
<>
|
||||
{banner}
|
||||
{toolbar}
|
||||
<BaseEmbedTitle pageId={pageId} />
|
||||
</>,
|
||||
)}
|
||||
</RowExpandProvider>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
onNavigate={handleRowNavigate}
|
||||
/>
|
||||
</BaseEditableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (isKanban) {
|
||||
return (
|
||||
<BaseEditableProvider editable={editable}>
|
||||
<div className={kanbanClasses.standalone}>
|
||||
<RowExpandProvider value={handleExpandRow}>
|
||||
{kanbanBand}
|
||||
{viewRenderer(null)}
|
||||
</RowExpandProvider>
|
||||
</div>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
onNavigate={handleRowNavigate}
|
||||
/>
|
||||
</BaseEditableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Standalone: title, banner, and toolbar go in aboveBand inside the scroll
|
||||
// container so they scroll away; only the column-header row stays pinned.
|
||||
return (
|
||||
<BaseEditableProvider editable={editable}>
|
||||
<div className={viewClasses.fullHeight}>
|
||||
<div className={classes.tableScrollport} ref={scrollportRef}>
|
||||
<RowExpandProvider value={handleExpandRow}>
|
||||
{viewRenderer(
|
||||
<>
|
||||
{titleSlot}
|
||||
{banner}
|
||||
{toolbar}
|
||||
</>,
|
||||
)}
|
||||
</RowExpandProvider>
|
||||
</div>
|
||||
</div>
|
||||
<RowDetailModal
|
||||
base={base}
|
||||
rows={rows}
|
||||
openRowId={openRowId}
|
||||
onClose={closeRow}
|
||||
onNavigate={handleRowNavigate}
|
||||
/>
|
||||
</BaseEditableProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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<HTMLDivElement>(null);
|
||||
const measureRef = useRef<HTMLDivElement>(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 (
|
||||
<Tooltip
|
||||
label={tooltipLabel}
|
||||
multiline
|
||||
withinPortal
|
||||
openDelay={400}
|
||||
disabled={!tooltipLabel || overflow <= 0}
|
||||
>
|
||||
<div className={cellClasses.badgeGroup} ref={containerRef}>
|
||||
<div className={cellClasses.badgeMeasure} ref={measureRef} aria-hidden>
|
||||
{chips}
|
||||
<span className={cellClasses.overflowCount}>+{chips.length}</span>
|
||||
</div>
|
||||
{visible}
|
||||
{overflow > 0 && (
|
||||
<span className={cellClasses.overflowCount}>+{overflow}</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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 (
|
||||
<div
|
||||
className={cellClasses.checkboxCell}
|
||||
onClick={handleChange}
|
||||
style={readOnly ? { cursor: "default" } : undefined}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={() => {}}
|
||||
size="xs"
|
||||
tabIndex={-1}
|
||||
styles={{
|
||||
input: {
|
||||
cursor: readOnly ? "default" : "pointer",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 <span className={cellClasses.emptyValue} />;
|
||||
}
|
||||
|
||||
return <span className={cellClasses.dateValue}>{formatted}</span>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user