Compare commits

..
12 Commits
Author SHA1 Message Date
Philipinho 6205bbeb90 fix: page tree reordering 2026-09-08 16:46:38 +01:00
Philipinho 9b3e5dd3dc v0.96.0 2026-09-08 14:58:41 +01:00
Philip Okugbe dfc38c8718 fix: ws relay (#2482) 2026-09-08 14:00:49 +01:00
Philip Okugbe 7c368c86c6 fix: h1 heading weight in editor (#2481)
* fix: h1 heading weight in editor
* cleanup
2026-09-08 13:24:57 +01:00
Philip Okugbe 1397e109ed New Crowdin updates (#2472) 2026-09-08 13:19:09 +01:00
Philip Okugbe 5792fc7ca2 fix: db lock operations (#2479)
* fix: advisory lock for page move

* fix: lock role count check
2026-09-08 13:16:56 +01:00
Salihu 949072744d fix: align input shortcuts (#2467)
* align input shortcuts

* minor fix

* minor fix
2026-09-08 01:54:41 +01:00
Philip Okugbe 0a87db4f1f fix: meta title for shared subpages (#2478) 2026-09-07 16:43:02 +01:00
Philip Okugbe 89e27a1a07 fix(editor): allow gap cursors between top-level blocks (#2477) 2026-09-06 19:22:28 +01:00
Philip Okugbe 27fa9af959 fix: public page print (#2476)
* fix pdf-print for public pages

* fix print in dark mode
2026-09-05 17:28:45 +01:00
SalihuandPhilipinho 84e1e56cf6 feat: jump to search (#2453)
* jump to search init

* support multiple text matching

* search navigation dialog

* remove search text

* typesense support

* typesense support

* add search guard

* use search key check

* minor fix

* minor fix

* fix: clean up jump search state on close

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-09-05 16:06:52 +01:00
Philip Okugbe 6ff8c7cdfe refactor(favorites): move favorite filtering into the repository query (#2475)
* refactor(favorites): move favorite filtering into the repository query

* perf: resolve cleanup targets by primary key
2026-09-05 14:40:10 +01:00
52 changed files with 2804 additions and 405 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "0.95.0",
"version": "0.96.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -793,6 +793,10 @@
"Removed page restriction": "Seitenbeschränkung entfernt",
"Added page permission": "Seitenberechtigung hinzugefügt",
"Removed page permission": "Seitenberechtigung entfernt",
"Changed page permission": "Seitenberechtigung geändert",
"Requested password reset": "Zurücksetzen des Passworts angefordert",
"Created template": "Vorlage erstellt",
"Deleted template": "Vorlage gelöscht",
"day": "Tag",
"days": "Tage",
"week": "Woche",
@@ -880,6 +884,9 @@
"<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",
"SIEM destination <bold>{{name}}</bold> is failing": "SIEM-Ziel <bold>{{name}}</bold> schlägt fehl",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "SIEM-Ziel <bold>{{name}}</bold> wurde nach 24 Stunden mit Fehlern deaktiviert",
"SIEM destination <bold>{{name}}</bold> recovered": "SIEM-Ziel <bold>{{name}}</bold> wurde wiederhergestellt",
"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.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI-Chat kann Workspace-Inhalte durchsuchen und lesen, aber keine Seiten erstellen oder bearbeiten.",
"Toggle AI Chat read-only mode": "Schreibgeschützten Modus für AI-Chat umschalten",
"Title only": "Nur Titel",
"you": "du"
"you": "du",
"Allow public spaces": "Öffentliche Bereiche zulassen",
"Space admins can publish their spaces to the web.": "Bereichsadministratoren können ihre Bereiche im Web veröffentlichen.",
"Toggle allow public spaces": "Öffentliche Bereiche umschalten",
"Publish space to the web": "Bereich im Web veröffentlichen",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Jeder im Internet kann jede Seite in diesem Bereich lesen, außer eingeschränkten Seiten. Sind Sie sicher?",
"Make this space publicly readable by anyone on the internet.": "Diesen Bereich für jeden im Internet öffentlich lesbar machen.",
"Toggle publish space to the web": "Veröffentlichung des Bereichs im Web umschalten",
"Allow search engines to index": "Suchmaschinen das Indexieren erlauben",
"Let public pages in this space appear in search engine results.": "Öffentliche Seiten in diesem Bereich in Suchmaschinenergebnissen erscheinen lassen.",
"Toggle search engine indexing": "Suchmaschinenindexierung umschalten",
"Public space link": "Öffentlicher Bereichslink",
"Copy public space link": "Öffentlichen Bereichslink kopieren",
"Renaming the space slug will break public links.": "Das Umbenennen des Bereichs-Slugs führt dazu, dass öffentliche Links nicht mehr funktionieren.",
"Failed to update space": "Bereich konnte nicht aktualisiert werden",
"This space is public": "Dieser Bereich ist öffentlich",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Jeder im Internet kann die Seiten in diesem Bereich lesen, außer eingeschränkten Seiten.",
"Open public site": "Öffentliche Website öffnen",
"Public": "Öffentlich",
"This space has no public pages yet.": "Dieser Bereich hat noch keine öffentlichen Seiten.",
"On this page": "Auf dieser Seite",
"Previous": "Zurück",
"Next": "Weiter",
"Show hidden pages": "Ausgeblendete Seiten anzeigen",
"Page navigation": "Seitennavigation",
"Toggle sidebar": "Seitenleiste umschalten",
"Toggle table of contents": "Inhaltsverzeichnis umschalten",
"Appearance": "Erscheinungsbild",
"Forest": "Wald",
"Violet": "Violett",
"Light mode color": "Farbe für den hellen Modus",
"Dark mode color": "Farbe für den dunklen Modus",
"Choose the primary color of the public docs site.": "Wählen Sie die Primärfarbe der öffentlichen Dokumentationswebsite aus.",
"Show page author": "Seitenautor anzeigen",
"Display the page creator's name on public pages.": "Den Namen des Seitenerstellers auf öffentlichen Seiten anzeigen.",
"Toggle show page author": "Anzeige des Seitenautors umschalten",
"Show last updated": "Zuletzt aktualisiert anzeigen",
"Display when each page was last updated.": "Anzeigen, wann jede Seite zuletzt aktualisiert wurde.",
"Toggle show last updated": "Anzeige der letzten Aktualisierung umschalten",
"Open public page": "Öffentliche Seite öffnen",
"Toggle color scheme": "Farbschema umschalten",
"Ember": "Glut",
"Rose": "Rosa",
"Documentation": "Dokumentation",
"All published spaces.": "Alle veröffentlichten Bereiche.",
"No public spaces yet.": "Noch keine öffentlichen Bereiche.",
"Show public directory": "Öffentliches Verzeichnis anzeigen",
"List published spaces at /docs for anyone to browse.": "Veröffentlichte Bereiche unter /docs auflisten, damit sie von allen durchsucht werden können.",
"Toggle show public directory": "Anzeige des öffentlichen Verzeichnisses umschalten",
"Show in public directory": "Im öffentlichen Verzeichnis anzeigen",
"List this space in the public directory at /docs.": "Diesen Bereich im öffentlichen Verzeichnis unter /docs auflisten.",
"Toggle show in public directory": "Anzeige im öffentlichen Verzeichnis umschalten",
"Open public space link": "Öffentlichen Bereichslink öffnen",
"Disable public spaces": "Öffentliche Bereiche deaktivieren",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Bereichsadministratoren können ihre Bereiche für jeden im Internet öffentlich lesbar machen. Sind Sie sicher?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Dadurch wird die Veröffentlichung aller veröffentlichten Bereiche sofort aufgehoben. Ein späteres erneutes Aktivieren veröffentlicht sie nicht erneut. Sind Sie sicher?",
"Allow": "Zulassen",
"Shared pages": "Geteilte Seiten",
"Published spaces": "Veröffentlichte Bereiche",
"Spaces published to the web will appear here": "Hier werden im Web veröffentlichte Bereiche angezeigt",
"No published spaces": "Keine veröffentlichten Bereiche",
"Published by": "Veröffentlicht von",
"Published at": "Veröffentlicht am",
"Open space": "Bereich öffnen",
"Unpublish": "Veröffentlichung aufheben",
"Unpublish space": "Veröffentlichung des Bereichs aufheben",
"This space will no longer be publicly accessible. Are you sure?": "Dieser Bereich wird nicht mehr öffentlich zugänglich sein. Sind Sie sicher?",
"More options for {{name}}": "Weitere Optionen für {{name}}",
"Edit page": "Seite bearbeiten",
"Sign in": "Anmelden",
"Open app": "App öffnen",
"No spaces match your search.": "Keine Bereiche entsprechen Ihrer Suche.",
"Welcome to our documentation": "Willkommen in unserer Dokumentation",
"Guides, references and answers across all our published spaces.": "Anleitungen, Referenzen und Antworten aus all unseren veröffentlichten Bereichen.",
"Guides, references and answers across all our spaces.": "Anleitungen, Referenzen und Antworten aus all unseren Bereichen.",
"Search documentation...": "Dokumentation durchsuchen...",
"1 published space": "1 veröffentlichter Bereich",
"{{count}} published spaces": "{{count}} veröffentlichte Bereiche",
"Actions": "Aktionen",
"Add destination": "Ziel hinzufügen",
"Are you sure you want to delete the destination": "Sind Sie sicher, dass Sie das Ziel löschen möchten",
"Audit logs": "Audit-Protokolle",
"Audit logs & SIEM": "Audit-Protokolle & SIEM",
"Auth header name": "Name des Auth-Headers",
"Auth header prefix": "Präfix des Auth-Headers",
"Body format": "Body-Format",
"Created SIEM destination": "SIEM-Ziel erstellt",
"Datadog site": "Datadog-Website",
"Defaults to this instance's hostname": "Standardmäßig wird der Hostname dieser Instanz verwendet",
"Delete destination": "Ziel löschen",
"Deleted SIEM destination": "SIEM-Ziel gelöscht",
"Destination created": "Ziel erstellt",
"Destination deleted": "Ziel gelöscht",
"Destination updated": "Ziel aktualisiert",
"Disabled": "Deaktiviert",
"Edit destination": "Ziel bearbeiten",
"Endpoint URL": "Endpunkt-URL",
"Failing": "Fehlerhaft",
"Failing since {{time}}": "Fehlerhaft seit {{time}}",
"HEC token": "HEC-Token",
"HEC URL": "HEC-URL",
"Healthy": "Fehlerfrei",
"Hide advanced options": "Erweiterte Optionen ausblenden",
"Host": "Host",
"Index": "Index",
"Insecure: connections can be intercepted.": "Unsicher: Verbindungen können abgefangen werden.",
"JSON array": "JSON-Array",
"Last delivered": "Zuletzt zugestellt",
"Last error": "Letzter Fehler",
"Leave empty to use the token's default index": "Leer lassen, um den Standardindex des Tokens zu verwenden",
"Maximum of {{limit}} destinations reached": "Maximale Anzahl von {{limit}} Zielen erreicht",
"Could not load SIEM destinations: {{message}}": "SIEM-Ziele konnten nicht geladen werden: {{message}}",
"No destinations yet": "Noch keine Ziele",
"Preset": "Voreinstellung",
"Retry now": "Jetzt erneut versuchen",
"Retry scheduled": "Wiederholung geplant",
"Send test event": "Testereignis senden",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Wird im unten stehenden Auth-Header gesendet. Leer lassen, wenn Ihr Empfänger keinen benötigt.",
"Service": "Dienst",
"Show advanced options": "Erweiterte Optionen anzeigen",
"SIEM": "SIEM",
"SIEM streaming": "SIEM-Streaming",
"SIEM streaming requires an Enterprise license.": "SIEM-Streaming erfordert eine Enterprise-Lizenz.",
"Source": "Quelle",
"Sourcetype": "Quelltyp",
"Tags": "Tags",
"Test connection": "Verbindung testen",
"Test event delivered successfully.": "Testereignis erfolgreich zugestellt.",
"Test the connection before saving.": "Testen Sie die Verbindung vor dem Speichern.",
"Test event delivered to {{name}}": "Testereignis an {{name}} zugestellt",
"Updated SIEM destination": "SIEM-Ziel aktualisiert",
"Verify TLS certificate": "TLS-Zertifikat verifizieren",
"e.g. Splunk prod": "z. B. Splunk Prod"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Restricción de página eliminada",
"Added page permission": "Permiso de página añadido",
"Removed page permission": "Permiso de página eliminado",
"Changed page permission": "Permiso de página cambiado",
"Requested password reset": "Se solicitó el restablecimiento de contraseña",
"Created template": "Plantilla creada",
"Deleted template": "Plantilla eliminada",
"day": "día",
"days": "días",
"week": "semana",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> devolvió una página para revisión",
"Page verification expires soon": "La verificación de la página caduca pronto",
"Page verification has expired": "La verificación de la página ha caducado",
"SIEM destination <bold>{{name}}</bold> is failing": "El destino SIEM <bold>{{name}}</bold> está fallando",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "El destino SIEM <bold>{{name}}</bold> se deshabilitó después de 24 horas de fallos",
"SIEM destination <bold>{{name}}</bold> recovered": "El destino SIEM <bold>{{name}}</bold> se recuperó",
"Verifying your email": "Verificando tu correo electrónico",
"Please wait...": "Por favor, espera...",
"Verification failed. The link may have expired.": "La verificación ha fallado. Es posible que el enlace haya expirado.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat puede buscar y leer el contenido del espacio de trabajo, pero no puede crear ni editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo de solo lectura de AI Chat",
"Title only": "Solo título",
"you": "tú"
"you": "tú",
"Allow public spaces": "Permitir espacios públicos",
"Space admins can publish their spaces to the web.": "Los administradores de espacios pueden publicar sus espacios en la web.",
"Toggle allow public spaces": "Alternar permitir espacios públicos",
"Publish space to the web": "Publicar espacio en la web",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Cualquier persona en internet podrá leer todas las páginas de este espacio, excepto las páginas restringidas. ¿Está seguro?",
"Make this space publicly readable by anyone on the internet.": "Haga que este espacio sea legible públicamente por cualquier persona en internet.",
"Toggle publish space to the web": "Alternar publicar espacio en la web",
"Allow search engines to index": "Permitir que los motores de búsqueda indexen",
"Let public pages in this space appear in search engine results.": "Permita que las páginas públicas de este espacio aparezcan en los resultados de los motores de búsqueda.",
"Toggle search engine indexing": "Alternar indexación en motores de búsqueda",
"Public space link": "Enlace público del espacio",
"Copy public space link": "Copiar enlace público del espacio",
"Renaming the space slug will break public links.": "Cambiar el slug del espacio romperá los enlaces públicos.",
"Failed to update space": "No se pudo actualizar el espacio",
"This space is public": "Este espacio es público",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Cualquier persona en internet puede leer las páginas de este espacio, excepto las páginas restringidas.",
"Open public site": "Abrir sitio público",
"Public": "Público",
"This space has no public pages yet.": "Este espacio aún no tiene páginas públicas.",
"On this page": "En esta página",
"Previous": "Anterior",
"Next": "Siguiente",
"Show hidden pages": "Mostrar páginas ocultas",
"Page navigation": "Navegación de páginas",
"Toggle sidebar": "Alternar barra lateral",
"Toggle table of contents": "Alternar tabla de contenido",
"Appearance": "Apariencia",
"Forest": "Bosque",
"Violet": "Violeta",
"Light mode color": "Color del modo claro",
"Dark mode color": "Color del modo oscuro",
"Choose the primary color of the public docs site.": "Elija el color principal del sitio de documentación pública.",
"Show page author": "Mostrar autor de la página",
"Display the page creator's name on public pages.": "Mostrar el nombre del creador de la página en las páginas públicas.",
"Toggle show page author": "Alternar mostrar autor de la página",
"Show last updated": "Mostrar última actualización",
"Display when each page was last updated.": "Mostrar cuándo se actualizó por última vez cada página.",
"Toggle show last updated": "Alternar mostrar última actualización",
"Open public page": "Abrir página pública",
"Toggle color scheme": "Alternar esquema de color",
"Ember": "Ascua",
"Rose": "Rosa",
"Documentation": "Documentación",
"All published spaces.": "Todos los espacios publicados.",
"No public spaces yet.": "Aún no hay espacios públicos.",
"Show public directory": "Mostrar directorio público",
"List published spaces at /docs for anyone to browse.": "Enumere los espacios publicados en /docs para que cualquiera pueda explorarlos.",
"Toggle show public directory": "Alternar mostrar directorio público",
"Show in public directory": "Mostrar en el directorio público",
"List this space in the public directory at /docs.": "Enumere este espacio en el directorio público en /docs.",
"Toggle show in public directory": "Alternar mostrar en el directorio público",
"Open public space link": "Abrir enlace público del espacio",
"Disable public spaces": "Desactivar espacios públicos",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Los administradores de espacios podrán hacer que sus espacios sean legibles públicamente por cualquier persona en internet. ¿Está seguro?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Esto despublicará inmediatamente todos los espacios publicados. Volver a habilitarlos más tarde no los volverá a publicar. ¿Está seguro?",
"Allow": "Permitir",
"Shared pages": "Páginas compartidas",
"Published spaces": "Espacios publicados",
"Spaces published to the web will appear here": "Los espacios publicados en la web aparecerán aquí",
"No published spaces": "No hay espacios publicados",
"Published by": "Publicado por",
"Published at": "Publicado el",
"Open space": "Abrir espacio",
"Unpublish": "Despublicar",
"Unpublish space": "Despublicar espacio",
"This space will no longer be publicly accessible. Are you sure?": "Este espacio dejará de ser accesible públicamente. ¿Está seguro?",
"More options for {{name}}": "Más opciones para {{name}}",
"Edit page": "Editar página",
"Sign in": "Iniciar sesión",
"Open app": "Abrir aplicación",
"No spaces match your search.": "Ningún espacio coincide con su búsqueda.",
"Welcome to our documentation": "Bienvenido a nuestra documentación",
"Guides, references and answers across all our published spaces.": "Guías, referencias y respuestas en todos nuestros espacios publicados.",
"Guides, references and answers across all our spaces.": "Guías, referencias y respuestas en todos nuestros espacios.",
"Search documentation...": "Buscar en la documentación...",
"1 published space": "1 espacio publicado",
"{{count}} published spaces": "{{count, plural, one {# espacio publicado} other {# espacios publicados}}}",
"Actions": "Acciones",
"Add destination": "Añadir destino",
"Are you sure you want to delete the destination": "¿Está seguro de que desea eliminar el destino",
"Audit logs": "Registros de auditoría",
"Audit logs & SIEM": "Registros de auditoría y SIEM",
"Auth header name": "Nombre del encabezado de autenticación",
"Auth header prefix": "Prefijo del encabezado de autenticación",
"Body format": "Formato del cuerpo",
"Created SIEM destination": "Destino SIEM creado",
"Datadog site": "Sitio de Datadog",
"Defaults to this instance's hostname": "Usa de forma predeterminada el nombre de host de esta instancia",
"Delete destination": "Eliminar destino",
"Deleted SIEM destination": "Destino SIEM eliminado",
"Destination created": "Destino creado",
"Destination deleted": "Destino eliminado",
"Destination updated": "Destino actualizado",
"Disabled": "Deshabilitado",
"Edit destination": "Editar destino",
"Endpoint URL": "URL del endpoint",
"Failing": "Con fallos",
"Failing since {{time}}": "Con fallos desde {{time}}",
"HEC token": "Token HEC",
"HEC URL": "URL de HEC",
"Healthy": "Correcto",
"Hide advanced options": "Ocultar opciones avanzadas",
"Host": "Host",
"Index": "Índice",
"Insecure: connections can be intercepted.": "Inseguro: las conexiones pueden ser interceptadas.",
"JSON array": "Matriz JSON",
"Last delivered": "Última entrega",
"Last error": "Último error",
"Leave empty to use the token's default index": "Déjelo vacío para usar el índice predeterminado del token",
"Maximum of {{limit}} destinations reached": "Se alcanzó el máximo de {{limit}} destinos",
"Could not load SIEM destinations: {{message}}": "No se pudieron cargar los destinos SIEM: {{message}}",
"No destinations yet": "Aún no hay destinos",
"Preset": "Preajuste",
"Retry now": "Reintentar ahora",
"Retry scheduled": "Reintento programado",
"Send test event": "Enviar evento de prueba",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Se envía en el encabezado de autenticación de abajo. Déjelo vacío si su receptor no necesita uno.",
"Service": "Servicio",
"Show advanced options": "Mostrar opciones avanzadas",
"SIEM": "SIEM",
"SIEM streaming": "Transmisión SIEM",
"SIEM streaming requires an Enterprise license.": "La transmisión SIEM requiere una licencia Enterprise.",
"Source": "Origen",
"Sourcetype": "Tipo de origen",
"Tags": "Etiquetas",
"Test connection": "Probar conexión",
"Test event delivered successfully.": "El evento de prueba se entregó correctamente.",
"Test the connection before saving.": "Pruebe la conexión antes de guardar.",
"Test event delivered to {{name}}": "Evento de prueba entregado a {{name}}",
"Updated SIEM destination": "Destino SIEM actualizado",
"Verify TLS certificate": "Verificar certificado TLS",
"e.g. Splunk prod": "p. ej., Splunk producción"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Restriction de la page supprimée",
"Added page permission": "Autorisation de la page ajoutée",
"Removed page permission": "Autorisation de la page supprimée",
"Changed page permission": "Autorisation de page modifiée",
"Requested password reset": "Réinitialisation du mot de passe demandée",
"Created template": "Modèle créé",
"Deleted template": "Modèle supprimé",
"day": "jour",
"days": "jours",
"week": "semaine",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> a renvoyé une page pour révision",
"Page verification expires soon": "La vérification de la page expire bientôt",
"Page verification has expired": "La vérification de la page a expiré",
"SIEM destination <bold>{{name}}</bold> is failing": "La destination SIEM <bold>{{name}}</bold> est en échec",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "La destination SIEM <bold>{{name}}</bold> a été désactivée après 24 heures d’échecs",
"SIEM destination <bold>{{name}}</bold> recovered": "La destination SIEM <bold>{{name}}</bold> a récupéré",
"Verifying your email": "Vérification de votre e-mail",
"Please wait...": "Veuillez patienter...",
"Verification failed. The link may have expired.": "Échec de la vérification. Le lien a peut-être expiré.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat peut rechercher et lire le contenu de lespace de travail, mais ne peut pas créer ni modifier des pages.",
"Toggle AI Chat read-only mode": "Activer/désactiver le mode lecture seule dAI Chat",
"Title only": "Titre uniquement",
"you": "vous"
"you": "vous",
"Allow public spaces": "Autoriser les espaces publics",
"Space admins can publish their spaces to the web.": "Les administrateurs despace peuvent publier leurs espaces sur le Web.",
"Toggle allow public spaces": "Basculer lautorisation des espaces publics",
"Publish space to the web": "Publier lespace sur le Web",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Toute personne sur Internet pourra lire chaque page de cet espace, sauf les pages restreintes. Êtes-vous sûr ?",
"Make this space publicly readable by anyone on the internet.": "Rendre cet espace lisible publiquement par toute personne sur Internet.",
"Toggle publish space to the web": "Basculer la publication de lespace sur le Web",
"Allow search engines to index": "Autoriser les moteurs de recherche à indexer",
"Let public pages in this space appear in search engine results.": "Permettre aux pages publiques de cet espace dapparaître dans les résultats des moteurs de recherche.",
"Toggle search engine indexing": "Basculer lindexation par les moteurs de recherche",
"Public space link": "Lien public de lespace",
"Copy public space link": "Copier le lien public de lespace",
"Renaming the space slug will break public links.": "Renommer lidentifiant despace cassera les liens publics.",
"Failed to update space": "Échec de la mise à jour de lespace",
"This space is public": "Cet espace est public",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Toute personne sur Internet peut lire les pages de cet espace, sauf les pages restreintes.",
"Open public site": "Ouvrir le site public",
"Public": "Public",
"This space has no public pages yet.": "Cet espace na encore aucune page publique.",
"On this page": "Sur cette page",
"Previous": "Précédent",
"Next": "Suivant",
"Show hidden pages": "Afficher les pages masquées",
"Page navigation": "Navigation de page",
"Toggle sidebar": "Basculer la barre latérale",
"Toggle table of contents": "Basculer la table des matières",
"Appearance": "Apparence",
"Forest": "Forêt",
"Violet": "Violet",
"Light mode color": "Couleur du mode clair",
"Dark mode color": "Couleur du mode sombre",
"Choose the primary color of the public docs site.": "Choisissez la couleur principale du site de documentation public.",
"Show page author": "Afficher lauteur de la page",
"Display the page creator's name on public pages.": "Afficher le nom du créateur de la page sur les pages publiques.",
"Toggle show page author": "Basculer laffichage de lauteur de la page",
"Show last updated": "Afficher la dernière mise à jour",
"Display when each page was last updated.": "Afficher la date de dernière mise à jour de chaque page.",
"Toggle show last updated": "Basculer laffichage de la dernière mise à jour",
"Open public page": "Ouvrir la page publique",
"Toggle color scheme": "Basculer le schéma de couleurs",
"Ember": "Braise",
"Rose": "Rose",
"Documentation": "Documentation",
"All published spaces.": "Tous les espaces publiés.",
"No public spaces yet.": "Aucun espace public pour le moment.",
"Show public directory": "Afficher le répertoire public",
"List published spaces at /docs for anyone to browse.": "Lister les espaces publiés dans /docs pour que tout le monde puisse les parcourir.",
"Toggle show public directory": "Basculer laffichage du répertoire public",
"Show in public directory": "Afficher dans le répertoire public",
"List this space in the public directory at /docs.": "Lister cet espace dans le répertoire public à /docs.",
"Toggle show in public directory": "Basculer laffichage dans le répertoire public",
"Open public space link": "Ouvrir le lien public de lespace",
"Disable public spaces": "Désactiver les espaces publics",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Les administrateurs despace pourront rendre leurs espaces lisibles publiquement par toute personne sur Internet. Êtes-vous sûr ?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Cela dépubliera immédiatement chaque espace publié. Les réactiver plus tard ne les republiera pas. Êtes-vous sûr ?",
"Allow": "Autoriser",
"Shared pages": "Pages partagées",
"Published spaces": "Espaces publiés",
"Spaces published to the web will appear here": "Les espaces publiés sur le Web apparaîtront ici",
"No published spaces": "Aucun espace publié",
"Published by": "Publié par",
"Published at": "Publié le",
"Open space": "Ouvrir lespace",
"Unpublish": "Dépublier",
"Unpublish space": "Dépublier lespace",
"This space will no longer be publicly accessible. Are you sure?": "Cet espace ne sera plus accessible publiquement. Êtes-vous sûr ?",
"More options for {{name}}": "Plus doptions pour {{name}}",
"Edit page": "Modifier la page",
"Sign in": "Se connecter",
"Open app": "Ouvrir lapplication",
"No spaces match your search.": "Aucun espace ne correspond à votre recherche.",
"Welcome to our documentation": "Bienvenue dans notre documentation",
"Guides, references and answers across all our published spaces.": "Guides, références et réponses dans tous nos espaces publiés.",
"Guides, references and answers across all our spaces.": "Guides, références et réponses dans tous nos espaces.",
"Search documentation...": "Rechercher dans la documentation...",
"1 published space": "1 espace publié",
"{{count}} published spaces": "{{count}} published spaces",
"Actions": "Actions",
"Add destination": "Ajouter une destination",
"Are you sure you want to delete the destination": "Êtes-vous sûr de vouloir supprimer la destination",
"Audit logs": "Journaux daudit",
"Audit logs & SIEM": "Journaux daudit et SIEM",
"Auth header name": "Nom de len-tête dauthentification",
"Auth header prefix": "Préfixe de len-tête dauthentification",
"Body format": "Format du corps",
"Created SIEM destination": "Destination SIEM créée",
"Datadog site": "Site Datadog",
"Defaults to this instance's hostname": "Utilise par défaut le nom dhôte de cette instance",
"Delete destination": "Supprimer la destination",
"Deleted SIEM destination": "Destination SIEM supprimée",
"Destination created": "Destination créée",
"Destination deleted": "Destination supprimée",
"Destination updated": "Destination mise à jour",
"Disabled": "Désactivé",
"Edit destination": "Modifier la destination",
"Endpoint URL": "URL du point de terminaison",
"Failing": "En échec",
"Failing since {{time}}": "En échec depuis {{time}}",
"HEC token": "Jeton HEC",
"HEC URL": "URL HEC",
"Healthy": "Sain",
"Hide advanced options": "Masquer les options avancées",
"Host": "Hôte",
"Index": "Index",
"Insecure: connections can be intercepted.": "Non sécurisé : les connexions peuvent être interceptées.",
"JSON array": "Tableau JSON",
"Last delivered": "Dernière livraison",
"Last error": "Dernière erreur",
"Leave empty to use the token's default index": "Laissez vide pour utiliser lindex par défaut du jeton",
"Maximum of {{limit}} destinations reached": "Nombre maximal de destinations atteint : {{limit}}",
"Could not load SIEM destinations: {{message}}": "Impossible de charger les destinations SIEM : {{message}}",
"No destinations yet": "Aucune destination pour le moment",
"Preset": "Préréglage",
"Retry now": "Réessayer maintenant",
"Retry scheduled": "Nouvelle tentative planifiée",
"Send test event": "Envoyer un événement de test",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Envoyé dans len-tête dauthentification ci-dessous. Laissez vide si votre récepteur nen a pas besoin.",
"Service": "Service",
"Show advanced options": "Afficher les options avancées",
"SIEM": "SIEM",
"SIEM streaming": "Streaming SIEM",
"SIEM streaming requires an Enterprise license.": "Le streaming SIEM nécessite une licence Enterprise.",
"Source": "Source",
"Sourcetype": "Type de source",
"Tags": "Tags",
"Test connection": "Tester la connexion",
"Test event delivered successfully.": "Événement de test envoyé avec succès.",
"Test the connection before saving.": "Testez la connexion avant denregistrer.",
"Test event delivered to {{name}}": "Événement de test envoyé à {{name}}",
"Updated SIEM destination": "Destination SIEM mise à jour",
"Verify TLS certificate": "Vérifier le certificat TLS",
"e.g. Splunk prod": "p. ex. Splunk prod"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Restrizione della pagina rimossa",
"Added page permission": "Permesso sulla pagina aggiunto",
"Removed page permission": "Permesso sulla pagina rimosso",
"Changed page permission": "Autorizzazione pagina modificata",
"Requested password reset": "Richiesta reimpostazione password",
"Created template": "Modello creato",
"Deleted template": "Modello eliminato",
"day": "giorno",
"days": "giorni",
"week": "settimana",
@@ -880,6 +884,9 @@
"<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",
"SIEM destination <bold>{{name}}</bold> is failing": "La destinazione SIEM <bold>{{name}}</bold> non funziona",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "La destinazione SIEM <bold>{{name}}</bold> è stata disattivata dopo 24 ore di errori",
"SIEM destination <bold>{{name}}</bold> recovered": "La destinazione SIEM <bold>{{name}}</bold> è stata ripristinata",
"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.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat può cercare e leggere i contenuti del workspace, ma non può creare o modificare pagine.",
"Toggle AI Chat read-only mode": "Attiva/disattiva la modalità di sola lettura di AI Chat",
"Title only": "Solo titolo",
"you": "tu"
"you": "tu",
"Allow public spaces": "Consenti spazi pubblici",
"Space admins can publish their spaces to the web.": "Gli amministratori degli spazi possono pubblicare i loro spazi sul web.",
"Toggle allow public spaces": "Attiva/disattiva spazi pubblici",
"Publish space to the web": "Pubblica spazio sul web",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Chiunque su internet potrà leggere ogni pagina in questo spazio, eccetto le pagine con restrizioni. Sei sicuro?",
"Make this space publicly readable by anyone on the internet.": "Rendi questo spazio leggibile pubblicamente da chiunque su internet.",
"Toggle publish space to the web": "Attiva/disattiva pubblicazione dello spazio sul web",
"Allow search engines to index": "Consenti ai motori di ricerca di indicizzare",
"Let public pages in this space appear in search engine results.": "Consenti alle pagine pubbliche in questo spazio di apparire nei risultati dei motori di ricerca.",
"Toggle search engine indexing": "Attiva/disattiva indicizzazione dei motori di ricerca",
"Public space link": "Link pubblico dello spazio",
"Copy public space link": "Copia link pubblico dello spazio",
"Renaming the space slug will break public links.": "La ridenominazione dello slug dello spazio interromperà i link pubblici.",
"Failed to update space": "Impossibile aggiornare lo spazio",
"This space is public": "Questo spazio è pubblico",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Chiunque su internet può leggere le pagine in questo spazio, eccetto le pagine con restrizioni.",
"Open public site": "Apri sito pubblico",
"Public": "Pubblico",
"This space has no public pages yet.": "Questo spazio non ha ancora pagine pubbliche.",
"On this page": "In questa pagina",
"Previous": "Precedente",
"Next": "Successivo",
"Show hidden pages": "Mostra pagine nascoste",
"Page navigation": "Navigazione pagina",
"Toggle sidebar": "Attiva/disattiva barra laterale",
"Toggle table of contents": "Attiva/disattiva sommario",
"Appearance": "Aspetto",
"Forest": "Foresta",
"Violet": "Viola",
"Light mode color": "Colore modalità chiara",
"Dark mode color": "Colore modalità scura",
"Choose the primary color of the public docs site.": "Scegli il colore principale del sito pubblico della documentazione.",
"Show page author": "Mostra autore della pagina",
"Display the page creator's name on public pages.": "Mostra il nome del creatore della pagina nelle pagine pubbliche.",
"Toggle show page author": "Attiva/disattiva mostra autore della pagina",
"Show last updated": "Mostra ultimo aggiornamento",
"Display when each page was last updated.": "Mostra quando ogni pagina è stata aggiornata l'ultima volta.",
"Toggle show last updated": "Attiva/disattiva mostra ultimo aggiornamento",
"Open public page": "Apri pagina pubblica",
"Toggle color scheme": "Attiva/disattiva schema colori",
"Ember": "Brace",
"Rose": "Rosa",
"Documentation": "Documentazione",
"All published spaces.": "Tutti gli spazi pubblicati.",
"No public spaces yet.": "Nessuno spazio pubblico al momento.",
"Show public directory": "Mostra directory pubblica",
"List published spaces at /docs for anyone to browse.": "Elenca gli spazi pubblicati in /docs affinché chiunque possa consultarli.",
"Toggle show public directory": "Attiva/disattiva mostra directory pubblica",
"Show in public directory": "Mostra nella directory pubblica",
"List this space in the public directory at /docs.": "Elenca questo spazio nella directory pubblica in /docs.",
"Toggle show in public directory": "Attiva/disattiva mostra nella directory pubblica",
"Open public space link": "Apri link pubblico dello spazio",
"Disable public spaces": "Disabilita spazi pubblici",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Gli amministratori degli spazi potranno rendere i loro spazi leggibili pubblicamente da chiunque su internet. Sei sicuro?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Questo annullerà immediatamente la pubblicazione di ogni spazio pubblicato. Riattivarlo in seguito non li ripubblicherà. Sei sicuro?",
"Allow": "Consenti",
"Shared pages": "Pagine condivise",
"Published spaces": "Spazi pubblicati",
"Spaces published to the web will appear here": "Gli spazi pubblicati sul web appariranno qui",
"No published spaces": "Nessuno spazio pubblicato",
"Published by": "Pubblicato da",
"Published at": "Pubblicato il",
"Open space": "Apri spazio",
"Unpublish": "Annulla pubblicazione",
"Unpublish space": "Annulla pubblicazione dello spazio",
"This space will no longer be publicly accessible. Are you sure?": "Questo spazio non sarà più accessibile pubblicamente. Sei sicuro?",
"More options for {{name}}": "Altre opzioni per {{name}}",
"Edit page": "Modifica pagina",
"Sign in": "Accedi",
"Open app": "Apri app",
"No spaces match your search.": "Nessuno spazio corrisponde alla tua ricerca.",
"Welcome to our documentation": "Benvenuto nella nostra documentazione",
"Guides, references and answers across all our published spaces.": "Guide, riferimenti e risposte in tutti i nostri spazi pubblicati.",
"Guides, references and answers across all our spaces.": "Guide, riferimenti e risposte in tutti i nostri spazi.",
"Search documentation...": "Cerca nella documentazione...",
"1 published space": "1 published space",
"{{count}} published spaces": "{{count}} published spaces",
"Actions": "Azioni",
"Add destination": "Aggiungi destinazione",
"Are you sure you want to delete the destination": "Sei sicuro di voler eliminare la destinazione",
"Audit logs": "Log di controllo",
"Audit logs & SIEM": "Log di controllo e SIEM",
"Auth header name": "Nome intestazione di autenticazione",
"Auth header prefix": "Prefisso intestazione di autenticazione",
"Body format": "Formato del corpo",
"Created SIEM destination": "Destinazione SIEM creata",
"Datadog site": "Sito Datadog",
"Defaults to this instance's hostname": "Per impostazione predefinita usa il nome host di questa istanza",
"Delete destination": "Elimina destinazione",
"Deleted SIEM destination": "Destinazione SIEM eliminata",
"Destination created": "Destinazione creata",
"Destination deleted": "Destinazione eliminata",
"Destination updated": "Destinazione aggiornata",
"Disabled": "Disattivata",
"Edit destination": "Modifica destinazione",
"Endpoint URL": "URL endpoint",
"Failing": "Non funzionante",
"Failing since {{time}}": "Non funzionante da {{time}}",
"HEC token": "Token HEC",
"HEC URL": "URL HEC",
"Healthy": "Funzionante",
"Hide advanced options": "Nascondi opzioni avanzate",
"Host": "Host",
"Index": "Indice",
"Insecure: connections can be intercepted.": "Non sicuro: le connessioni possono essere intercettate.",
"JSON array": "Array JSON",
"Last delivered": "Ultima consegna",
"Last error": "Ultimo errore",
"Leave empty to use the token's default index": "Lascia vuoto per usare l'indice predefinito del token",
"Maximum of {{limit}} destinations reached": "Raggiunto il numero massimo di {{limit}} destinazioni",
"Could not load SIEM destinations: {{message}}": "Impossibile caricare le destinazioni SIEM: {{message}}",
"No destinations yet": "Nessuna destinazione al momento",
"Preset": "Predefinito",
"Retry now": "Riprova ora",
"Retry scheduled": "Riprova pianificata",
"Send test event": "Invia evento di test",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Inviato nell'intestazione di autenticazione qui sotto. Lascia vuoto se il ricevente non ne ha bisogno.",
"Service": "Servizio",
"Show advanced options": "Mostra opzioni avanzate",
"SIEM": "SIEM",
"SIEM streaming": "Streaming SIEM",
"SIEM streaming requires an Enterprise license.": "Lo streaming SIEM richiede una licenza Enterprise.",
"Source": "Origine",
"Sourcetype": "Tipo di origine",
"Tags": "Tag",
"Test connection": "Testa connessione",
"Test event delivered successfully.": "Evento di test inviato con successo.",
"Test the connection before saving.": "Testa la connessione prima di salvare.",
"Test event delivered to {{name}}": "Evento di test inviato a {{name}}",
"Updated SIEM destination": "Destinazione SIEM aggiornata",
"Verify TLS certificate": "Verifica certificato TLS",
"e.g. Splunk prod": "ad es. Splunk produzione"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "ページの制限を解除しました",
"Added page permission": "ページの権限を追加しました",
"Removed page permission": "ページの権限を削除しました",
"Changed page permission": "ページの権限を変更しました",
"Requested password reset": "パスワードのリセットをリクエストしました",
"Created template": "テンプレートを作成しました",
"Deleted template": "テンプレートを削除しました",
"day": "日",
"days": "日",
"week": "週",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold>がページを修正のため差し戻しました",
"Page verification expires soon": "ページ検証の期限が間もなく切れます",
"Page verification has expired": "ページ検証の期限が切れています",
"SIEM destination <bold>{{name}}</bold> is failing": "SIEM 宛先 <bold>{{name}}</bold> で障害が発生しています",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "SIEM 宛先 <bold>{{name}}</bold> は24時間障害が続いたため無効化されました",
"SIEM destination <bold>{{name}}</bold> recovered": "SIEM 宛先 <bold>{{name}}</bold> が復旧しました",
"Verifying your email": "メールアドレスを確認しています",
"Please wait...": "お待ちください…",
"Verification failed. The link may have expired.": "認証に失敗しました。リンクの有効期限が切れている可能性があります。",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AIチャットはワークスペースのコンテンツを検索および閲覧できますが、ページの作成や編集はできません。",
"Toggle AI Chat read-only mode": "AIチャットの読み取り専用モードを切り替え",
"Title only": "タイトルのみ",
"you": "あなた"
"you": "あなた",
"Allow public spaces": "公開スペースを許可",
"Space admins can publish their spaces to the web.": "スペース管理者は、自分のスペースをウェブに公開できるようになります。",
"Toggle allow public spaces": "公開スペースの許可を切り替える",
"Publish space to the web": "スペースをウェブに公開",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "インターネット上の誰でも、このスペース内の制限付きページを除くすべてのページを読めるようになります。よろしいですか?",
"Make this space publicly readable by anyone on the internet.": "このスペースをインターネット上の誰でも読めるようにします。",
"Toggle publish space to the web": "スペースのウェブ公開を切り替える",
"Allow search engines to index": "検索エンジンによるインデックスを許可",
"Let public pages in this space appear in search engine results.": "このスペース内の公開ページを検索エンジンの検索結果に表示できるようにします。",
"Toggle search engine indexing": "検索エンジンのインデックス登録を切り替える",
"Public space link": "公開スペースリンク",
"Copy public space link": "公開スペースリンクをコピー",
"Renaming the space slug will break public links.": "スペースのスラッグ名を変更すると公開リンクは使えなくなります。",
"Failed to update space": "スペースの更新に失敗しました",
"This space is public": "このスペースは公開されています",
"Anyone on the internet can read the pages in this space, except restricted pages.": "インターネット上の誰でも、このスペース内の制限付きページを除くページを読めます。",
"Open public site": "公開サイトを開く",
"Public": "公開",
"This space has no public pages yet.": "このスペースにはまだ公開ページがありません。",
"On this page": "このページ内",
"Previous": "前へ",
"Next": "次へ",
"Show hidden pages": "非表示のページを表示",
"Page navigation": "ページナビゲーション",
"Toggle sidebar": "サイドバーを切り替える",
"Toggle table of contents": "目次を切り替える",
"Appearance": "外観",
"Forest": "フォレスト",
"Violet": "バイオレット",
"Light mode color": "ライトモードの色",
"Dark mode color": "ダークモードの色",
"Choose the primary color of the public docs site.": "公開ドキュメントサイトのメインカラーを選択します。",
"Show page author": "ページ作成者を表示",
"Display the page creator's name on public pages.": "公開ページにページ作成者の名前を表示します。",
"Toggle show page author": "ページ作成者の表示を切り替える",
"Show last updated": "最終更新を表示",
"Display when each page was last updated.": "各ページが最後に更新された日時を表示します。",
"Toggle show last updated": "最終更新の表示を切り替える",
"Open public page": "公開ページを開く",
"Toggle color scheme": "カラースキームを切り替える",
"Ember": "アンバー",
"Rose": "ローズ",
"Documentation": "ドキュメント",
"All published spaces.": "公開されているすべてのスペース。",
"No public spaces yet.": "まだ公開スペースはありません。",
"Show public directory": "公開ディレクトリを表示",
"List published spaces at /docs for anyone to browse.": "/docs に公開されたスペースを一覧表示し、誰でも閲覧できるようにします。",
"Toggle show public directory": "公開ディレクトリの表示を切り替える",
"Show in public directory": "公開ディレクトリに表示",
"List this space in the public directory at /docs.": "このスペースを /docs の公開ディレクトリに一覧表示します。",
"Toggle show in public directory": "公開ディレクトリへの表示を切り替える",
"Open public space link": "公開スペースリンクを開く",
"Disable public spaces": "公開スペースを無効にする",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "スペース管理者は、自分のスペースをインターネット上の誰でも読めるように設定できるようになります。よろしいですか?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "これにより、公開されているすべてのスペースが直ちに非公開になります。後で再度有効にしても、再公開はされません。よろしいですか?",
"Allow": "許可",
"Shared pages": "共有ページ",
"Published spaces": "公開スペース",
"Spaces published to the web will appear here": "ウェブに公開されたスペースがここに表示されます",
"No published spaces": "公開スペースはありません",
"Published by": "公開者",
"Published at": "公開日時",
"Open space": "スペースを開く",
"Unpublish": "公開を停止",
"Unpublish space": "スペースの公開を停止",
"This space will no longer be publicly accessible. Are you sure?": "このスペースには今後公開アクセスできなくなります。よろしいですか?",
"More options for {{name}}": "{{name}} のその他のオプション",
"Edit page": "ページを編集",
"Sign in": "サインイン",
"Open app": "アプリを開く",
"No spaces match your search.": "検索に一致するスペースはありません。",
"Welcome to our documentation": "ドキュメントへようこそ",
"Guides, references and answers across all our published spaces.": "公開されているすべてのスペースにわたるガイド、リファレンス、回答。",
"Guides, references and answers across all our spaces.": "すべてのスペースにわたるガイド、リファレンス、回答。",
"Search documentation...": "ドキュメントを検索…",
"1 published space": "1 件の公開スペース",
"{{count}} published spaces": "{{count}} published spaces",
"Actions": "操作",
"Add destination": "宛先を追加",
"Are you sure you want to delete the destination": "この宛先を削除してもよろしいですか",
"Audit logs": "監査ログ",
"Audit logs & SIEM": "監査ログと SIEM",
"Auth header name": "認証ヘッダー名",
"Auth header prefix": "認証ヘッダープレフィックス",
"Body format": "本文形式",
"Created SIEM destination": "SIEM 宛先を作成しました",
"Datadog site": "Datadog サイト",
"Defaults to this instance's hostname": "デフォルトではこのインスタンスのホスト名が使用されます",
"Delete destination": "宛先を削除",
"Deleted SIEM destination": "SIEM 宛先を削除しました",
"Destination created": "宛先を作成しました",
"Destination deleted": "宛先を削除しました",
"Destination updated": "宛先を更新しました",
"Disabled": "無効",
"Edit destination": "宛先を編集",
"Endpoint URL": "エンドポイント URL",
"Failing": "障害発生中",
"Failing since {{time}}": "{{time}} から障害発生中",
"HEC token": "HEC トークン",
"HEC URL": "HEC URL",
"Healthy": "正常",
"Hide advanced options": "詳細オプションを隠す",
"Host": "ホスト",
"Index": "インデックス",
"Insecure: connections can be intercepted.": "安全ではありません: 接続が傍受される可能性があります。",
"JSON array": "JSON 配列",
"Last delivered": "最終配信",
"Last error": "最後のエラー",
"Leave empty to use the token's default index": "トークンのデフォルトインデックスを使用するには空欄のままにしてください",
"Maximum of {{limit}} destinations reached": "宛先は最大 {{limit}} 件までです",
"Could not load SIEM destinations: {{message}}": "SIEM 宛先を読み込めませんでした: {{message}}",
"No destinations yet": "宛先はまだありません",
"Preset": "プリセット",
"Retry now": "今すぐ再試行",
"Retry scheduled": "再試行予定",
"Send test event": "テストイベントを送信",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "以下の認証ヘッダーで送信されます。受信側で不要な場合は空欄のままにしてください。",
"Service": "サービス",
"Show advanced options": "詳細オプションを表示",
"SIEM": "SIEM",
"SIEM streaming": "SIEM ストリーミング",
"SIEM streaming requires an Enterprise license.": "SIEM ストリーミングにはエンタープライズライセンスが必要です。",
"Source": "ソース",
"Sourcetype": "ソースタイプ",
"Tags": "タグ",
"Test connection": "接続をテスト",
"Test event delivered successfully.": "テストイベントは正常に配信されました。",
"Test the connection before saving.": "保存する前に接続をテストしてください。",
"Test event delivered to {{name}}": "テストイベントを {{name}} に配信しました",
"Updated SIEM destination": "SIEM 宛先を更新しました",
"Verify TLS certificate": "TLS 証明書を検証",
"e.g. Splunk prod": "例: Splunk 本番"
}
+167 -28
View File
@@ -6,21 +6,21 @@
"Add groups": "그룹 추가",
"Add members": "사용자 추가",
"Add to groups": "그룹에 추가",
"Add space members": "Space에 사용자 추가",
"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?": "이 페이지를 삭제하시겠습니까?",
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 그룹에서 제거하시겠습니까? 사용자는 이 그룹이 가진 모든 권한을 잃게 됩니다.",
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.",
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 스페이스에서 제거하시겠습니까? 사용자는 이 스페이스에 대한 모든 접근 권한을 잃게 됩니다.",
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.",
"Can become members of groups and spaces in workspace": "워크스페이스의 그룹 및 스페이스의 멤버가 될 수 있음",
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
"Can create and edit pages in space.": "스페이스에서 페이지를 생성하고 편집할 수 있습니다.",
"Can edit": "편집할 수 있음",
"Can manage workspace": "워크스페이스를 관리할 수 있음",
"Can manage workspace but cannot delete it": "워크스페이스를 관리할 수 있지만, 삭제할 수 없음",
"Can manage workspace but cannot delete it": "워크스페이스를 관리할 수 있지만, 삭제할 수 없음",
"Can view": "볼 수 있음",
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
"Can view pages in space but not edit.": "스페이스의 페이지를 볼 수 있지만, 편집은 할 수 없음.",
"Cancel": "취소",
"Change email": "이메일 변경",
"Change password": "비밀번호 변경",
@@ -35,7 +35,7 @@
"Create": "생성",
"Create group": "그룹 생성",
"Create page": "페이지 생성",
"Create space": "Space 생성",
"Create space": "스페이스 생성",
"Create workspace": "워크스페이스 생성",
"Current password": "현재 비밀번호",
"Dark": "어두운",
@@ -107,10 +107,10 @@
"Group description": "그룹 설명",
"Group name": "그룹 이름",
"Groups": "그룹",
"Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
"Has full access to space settings and pages.": "스페이스 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
"Home": "홈",
"Import pages": "페이지 가져오기",
"Import pages & space settings": "페이지 및 Space 설정 가져오기",
"Import pages & space settings": "페이지 및 스페이스 설정 가져오기",
"Importing pages": "페이지 가져오는 중",
"invalid invitation link": "유효하지 않은 초대 링크입니다",
"Invitation signup": "초대 가입",
@@ -167,7 +167,7 @@
"Remove": "제거",
"Remove from favorites": "즐겨찾기에서 제거",
"Remove group member": "그룹 멤버 제거",
"Remove space member": "Space에서 사용자 제거",
"Remove space member": "스페이스에서 사용자 제거",
"Restore": "복원",
"Role": "역할",
"Save": "저장",
@@ -276,22 +276,22 @@
"Duplicate": "복제",
"Select a user": "사용자 선택",
"Select a group": "그룹 선택",
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
"Delete space": "Space 삭제",
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
"Delete this space with all its pages and data.": "이 Space의 모든 페이지와 데이터를 삭제합니다.",
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "이 Space의 모든 페이지, 댓글, 첨부파일 및 권한이 영구적으로 삭제됩니다.",
"Export all pages and attachments in this space.": "이 스페이스의 모든 페이지와 첨부 파일을 내보냅니다.",
"Delete space": "스페이스 삭제",
"Are you sure you want to delete this space?": "이 스페이스를 삭제하시겠습니까?",
"Delete this space with all its pages and data.": "이 스페이스의 모든 페이지와 데이터를 삭제합니다.",
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "이 스페이스의 모든 페이지, 댓글, 첨부 파일 및 권한이 영구적으로 삭제됩니다.",
"Confirm space name": "스페이스 이름 확인",
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "작업을 진행하려면 Space 이름 <b>{{spaceName}}</b>을 입력하세요.",
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "작업을 진행하려면 스페이스 이름 <b>{{spaceName}}</b>을(를) 입력하세요.",
"Format": "형식",
"Include subpages": "하위 페이지 포함",
"Include attachments": "첨부파일 포함",
"Include attachments": "첨부 파일 포함",
"Select export format": "내보내기 형식 선택",
"Export failed:": "내보내기 실패:",
"export error": "내보내기 오류",
"Export page": "페이지 내보내기",
"Export successful": "내보내기 완료",
"Export space": "Space 내보내기",
"Export space": "스페이스 내보내기",
"Export {{type}}": "{{type}} 내보내기",
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
"Media": "미디어",
@@ -513,7 +513,7 @@
"Toggle viewer comments": "뷰어 댓글 전환",
"Public sharing is disabled at the workspace level": "공개 공유가 워크스페이스 수준에서 비활성화됨",
"Prevent pages in this space from being shared publicly.": "이 스페이스의 페이지가 공개 공유되지 않도록 합니다.",
"Page permissions": "페이지 권한},{",
"Page permissions": "페이지 권한",
"Control who can view and edit individual pages. Available with an enterprise license.": "개별 페이지의 조회 및 편집 권한을 제어합니다. 엔터프라이즈 라이선스에서 이용 가능합니다.",
"Enable public sharing": "공개 공유 활성화",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공개 공유를 활성화하시겠습니까? 멤버가 페이지를 공개적으로 공유할 수 있게 됩니다.",
@@ -629,14 +629,14 @@
"No subpages": "하위 페이지가 없습니다",
"Subpages (Child pages)": "하위 페이지 (자식 페이지)",
"List all subpages of the current page": "현재 페이지의 모든 하위 페이지 나열",
"Attachments": "첨부파일",
"Attachments": "첨부 파일",
"All spaces": "모든 스페이스",
"Unknown": "알 수 없음",
"Find a space": "스페이스 찾기",
"Search in all your spaces": "모든 스페이스에서 검색",
"Type": "유형",
"Enterprise": "엔터프라이즈",
"Download attachment": "첨부파일 다운로드",
"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": "쉼표 또는 공백으로 구분된 유효한 도메인 이름을 입력하세요",
@@ -791,8 +791,12 @@
"Trash retention updated": "휴지통 보관 기간이 업데이트되었습니다.",
"Failed to update trash retention": "휴지통 보관 기간 업데이트에 실패했습니다.",
"Removed page restriction": "페이지 제한이 제거됨",
"Added page permission": "페이지 권한 추가됨",
"Removed page permission": "페이지 권한 제거됨",
"Added page permission": "페이지 권한 추가됨",
"Removed page permission": "페이지 권한 제거됨",
"Changed page permission": "페이지 권한 변경됨",
"Requested password reset": "비밀번호 재설정 요청됨",
"Created template": "템플릿 생성됨",
"Deleted template": "템플릿 삭제됨",
"day": "일",
"days": "일",
"week": "주",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold>님이 수정을 위해 페이지를 반려했습니다",
"Page verification expires soon": "페이지 검토가 곧 만료됩니다",
"Page verification has expired": "페이지 검토가 만료되었습니다",
"SIEM destination <bold>{{name}}</bold> is failing": "SIEM 대상 <bold>{{name}}</bold> 장애 발생 중",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "24시간 동안 지속된 장애로 SIEM 대상 <bold>{{name}}</bold> 비활성화됨",
"SIEM destination <bold>{{name}}</bold> recovered": "SIEM 대상 <bold>{{name}}</bold> 복구됨",
"Verifying your email": "이메일 확인 중",
"Please wait...": "잠시만 기다려 주세요...",
"Verification failed. The link may have expired.": "인증에 실패했습니다. 링크가 만료되었을 수 있습니다.",
@@ -1122,7 +1129,7 @@
"Toggle allow personal spaces": "개인 공간 허용 전환",
"Create personal space": "개인 공간 만들기",
"Personal space": "개인 공간",
"{{name}}'s space": "{{name}}의 공간",
"{{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이 아닌 셀은 지워집니다.",
@@ -1301,9 +1308,9 @@
"Version actions for {{date}}": "{{date}} 버전 작업",
"Comparing {{newer}} and {{older}}": "{{newer}} 및 {{older}} 비교 중",
"Exit compare": "비교 종료",
"Search attachments...": "첨부파일 검색...",
"Error loading attachments.": "첨부파일을 불러오는 중 오류가 발생했습니다.",
"No attachments on this page yet.": "이 페이지에는 아직 첨부파일이 없습니다.",
"Search attachments...": "첨부 파일 검색...",
"Error loading attachments.": "첨부 파일을 불러오는 중 오류가 발생했습니다.",
"No attachments on this page yet.": "이 페이지에는 아직 첨부 파일이 없습니다.",
"Uploaded by {{name}}": "업로드한 사람: {{name}}",
"Download {{name}}": "{{name}} 다운로드",
"Access revoked": "액세스가 취소되었습니다",
@@ -1327,7 +1334,7 @@
"View content without making changes.": "변경하지 않고 콘텐츠를 봅니다.",
"Create and modify content.": "콘텐츠를 생성하고 수정합니다.",
"Applications and AI assistants you have authorized to access your account.": "회원님의 계정에 액세스하도록 승인한 애플리케이션 및 AI 도우미입니다.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "워크스페이스에서 MCP가 활성화되어 있습니다. OAuth를 통해 AI 도우미를 Docmost 계정에 연결하세요.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "워크스페이스에서 MCP가 활성화되어 있습니다. OAuth를 통해 AI 어시스턴트를 Docmost 계정에 연결하세요.",
"Authorized apps": "승인된 앱",
"No authorized apps yet.": "아직 승인된 앱이 없습니다.",
"Workspace knowledge only": "워크스페이스 지식만 사용",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 채팅은 워크스페이스 콘텐츠를 검색하고 읽을 수 있지만, 페이지를 생성하거나 편집할 수는 없습니다.",
"Toggle AI Chat read-only mode": "AI 채팅 읽기 전용 모드 전환",
"Title only": "제목만",
"you": "회원님"
"you": "회원님",
"Allow public spaces": "공개 스페이스 허용",
"Space admins can publish their spaces to the web.": "스페이스 관리자는 자신의 스페이스를 웹에 게시할 수 있습니다.",
"Toggle allow public spaces": "공개 스페이스 허용 전환",
"Publish space to the web": "스페이스를 웹에 게시",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "제한된 페이지를 제외하고 이 스페이스의 모든 페이지를 인터넷상의 누구나 읽을 수 있게 됩니다. 계속하시겠습니까?",
"Make this space publicly readable by anyone on the internet.": "이 스페이스를 인터넷상의 누구나 읽을 수 있도록 공개합니다.",
"Toggle publish space to the web": "스페이스 웹 게시 전환",
"Allow search engines to index": "검색 엔진 색인 허용",
"Let public pages in this space appear in search engine results.": "이 스페이스의 공개 페이지가 검색 엔진 결과에 표시되도록 합니다.",
"Toggle search engine indexing": "검색 엔진 색인 전환",
"Public space link": "공개 스페이스 링크",
"Copy public space link": "공개 스페이스 링크 복사",
"Renaming the space slug will break public links.": "스페이스 슬러그 이름을 변경하면 공개 링크가 작동하지 않게 됩니다.",
"Failed to update space": "스페이스 업데이트 실패",
"This space is public": "이 스페이스는 공개 상태입니다",
"Anyone on the internet can read the pages in this space, except restricted pages.": "제한된 페이지를 제외하고 이 스페이스의 페이지를 인터넷상의 누구나 읽을 수 있습니다.",
"Open public site": "공개 사이트 열기",
"Public": "공개",
"This space has no public pages yet.": "이 스페이스에는 아직 공개 페이지가 없습니다.",
"On this page": "이 페이지에서",
"Previous": "이전",
"Next": "다음",
"Show hidden pages": "숨겨진 페이지 표시",
"Page navigation": "페이지 탐색",
"Toggle sidebar": "사이드바 전환",
"Toggle table of contents": "목차 전환",
"Appearance": "모양",
"Forest": "포레스트",
"Violet": "바이올렛",
"Light mode color": "라이트 모드 색상",
"Dark mode color": "다크 모드 색상",
"Choose the primary color of the public docs site.": "공개 문서 사이트의 기본 색상을 선택하세요.",
"Show page author": "페이지 작성자 표시",
"Display the page creator's name on public pages.": "공개 페이지에 페이지 작성자의 이름을 표시합니다.",
"Toggle show page author": "페이지 작성자 표시 전환",
"Show last updated": "마지막 업데이트 표시",
"Display when each page was last updated.": "각 페이지가 마지막으로 업데이트된 시점을 표시합니다.",
"Toggle show last updated": "마지막 업데이트 표시 전환",
"Open public page": "공개 페이지 열기",
"Toggle color scheme": "색상 구성표 전환",
"Ember": "엠버",
"Rose": "로즈",
"Documentation": "문서",
"All published spaces.": "게시된 모든 스페이스입니다.",
"No public spaces yet.": "아직 공개 스페이스가 없습니다.",
"Show public directory": "공개 디렉터리 표시",
"List published spaces at /docs for anyone to browse.": "누구나 탐색할 수 있도록 /docs에 게시된 스페이스를 나열합니다.",
"Toggle show public directory": "공개 디렉터리 표시 전환",
"Show in public directory": "공개 디렉터리에 표시",
"List this space in the public directory at /docs.": "이 스페이스를 /docs의 공개 디렉터리에 나열합니다.",
"Toggle show in public directory": "공개 디렉터리에 표시 전환",
"Open public space link": "공개 스페이스 링크 열기",
"Disable public spaces": "공개 스페이스 비활성화",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "스페이스 관리자는 자신의 스페이스를 인터넷상의 누구나 읽을 수 있도록 공개할 수 있게 됩니다. 계속하시겠습니까?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "이 작업을 수행하면 현재 게시된 모든 스페이스의 게시가 즉시 취소됩니다. 나중에 다시 활성화해도 자동으로 다시 게시되지는 않습니다. 계속하시겠습니까?",
"Allow": "허용",
"Shared pages": "공유된 페이지",
"Published spaces": "게시된 스페이스",
"Spaces published to the web will appear here": "웹에 게시된 스페이스가 여기에 표시됩니다",
"No published spaces": "게시된 스페이스 없음",
"Published by": "게시자",
"Published at": "게시일",
"Open space": "스페이스 열기",
"Unpublish": "게시 취소",
"Unpublish space": "스페이스 게시 취소",
"This space will no longer be publicly accessible. Are you sure?": "이 스페이스는 더 이상 공개적으로 접근할 수 없게 됩니다. 계속하시겠습니까?",
"More options for {{name}}": "{{name}}에 대한 추가 옵션",
"Edit page": "페이지 편집",
"Sign in": "로그인",
"Open app": "앱 열기",
"No spaces match your search.": "검색과 일치하는 스페이스가 없습니다.",
"Welcome to our documentation": "문서에 오신 것을 환영합니다",
"Guides, references and answers across all our published spaces.": "게시된 모든 스페이스의 가이드, 참고 자료, 답변을 확인하세요.",
"Guides, references and answers across all our spaces.": "모든 스페이스의 가이드, 참고 자료, 답변을 확인하세요.",
"Search documentation...": "문서 검색...",
"1 published space": "게시된 스페이스 1개",
"{{count}} published spaces": "게시된 스페이스 {{count}}개",
"Actions": "작업",
"Add destination": "대상 추가",
"Are you sure you want to delete the destination": "대상을 삭제하시겠습니까?",
"Audit logs": "감사 로그",
"Audit logs & SIEM": "감사 로그 & SIEM",
"Auth header name": "인증 헤더 이름",
"Auth header prefix": "인증 헤더 접두사",
"Body format": "본문 형식",
"Created SIEM destination": "SIEM 대상 생성됨",
"Datadog site": "Datadog 사이트",
"Defaults to this instance's hostname": "기본값은 인스턴스의 호스트명입니다",
"Delete destination": "대상 삭제",
"Deleted SIEM destination": "SIEM 대상 삭제됨",
"Destination created": "대상 생성됨",
"Destination deleted": "대상 삭제됨",
"Destination updated": "대상 업데이트됨",
"Disabled": "비활성화됨",
"Edit destination": "대상 편집",
"Endpoint URL": "엔드포인트 URL",
"Failing": "장애 발생 중",
"Failing since {{time}}": "{{time}}부터 장애 발생 중",
"HEC token": "HEC 토큰",
"HEC URL": "HEC URL",
"Healthy": "정상",
"Hide advanced options": "고급 옵션 숨기기",
"Host": "호스트",
"Index": "인덱스",
"Insecure: connections can be intercepted.": "안전하지 않음: 연결이 가로채질 수 있습니다.",
"JSON array": "JSON 배열",
"Last delivered": "마지막 전송",
"Last error": "마지막 오류",
"Leave empty to use the token's default index": "토큰의 기본 인덱스를 사용하려면 비워 두세요",
"Maximum of {{limit}} destinations reached": "대상은 최대 {{limit}}개까지 추가할 수 있습니다",
"Could not load SIEM destinations: {{message}}": "SIEM 대상을 불러올 수 없습니다: {{message}}",
"No destinations yet": "등록된 대상이 없습니다",
"Preset": "프리셋",
"Retry now": "지금 재시도",
"Retry scheduled": "재시도 예정",
"Send test event": "테스트 이벤트 전송",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "아래 인증 헤더를 통해 전송하였습니다. 수신 측에서 요구하지 않는 경우 비워 두세요.",
"Service": "서비스",
"Show advanced options": "고급 옵션 표시",
"SIEM": "SIEM",
"SIEM streaming": "SIEM 스트리밍",
"SIEM streaming requires an Enterprise license.": "SIEM 스트리밍은 엔터프라이즈 라이선스가 필요합니다.",
"Source": "소스",
"Sourcetype": "소스 유형",
"Tags": "태그",
"Test connection": "연결 테스트",
"Test event delivered successfully.": "테스트 이벤트가 전송되었습니다.",
"Test the connection before saving.": "저장하기 전에 연결을 테스트하세요.",
"Test event delivered to {{name}}": "테스트 이벤트가 {{name}}에 전송되었습니다",
"Updated SIEM destination": "SIEM 대상 업데이트됨",
"Verify TLS certificate": "TLS 인증서 검증",
"e.g. Splunk prod": "예: Splunk 운영"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Pagina-restrictie verwijderd",
"Added page permission": "Paginatoestemming toegevoegd",
"Removed page permission": "Paginatoestemming verwijderd",
"Changed page permission": "Paginamachtiging gewijzigd",
"Requested password reset": "Wachtwoordreset aangevraagd",
"Created template": "Sjabloon aangemaakt",
"Deleted template": "Sjabloon verwijderd",
"day": "dag",
"days": "dagen",
"week": "week",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> heeft een pagina teruggestuurd voor revisie",
"Page verification expires soon": "Paginaverificatie verloopt binnenkort",
"Page verification has expired": "Paginaverificatie is verlopen",
"SIEM destination <bold>{{name}}</bold> is failing": "SIEM-bestemming <bold>{{name}}</bold> faalt",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "SIEM-bestemming <bold>{{name}}</bold> is uitgeschakeld na 24 uur met fouten",
"SIEM destination <bold>{{name}}</bold> recovered": "SIEM-bestemming <bold>{{name}}</bold> hersteld",
"Verifying your email": "Je e-mailadres wordt geverifieerd",
"Please wait...": "Even geduld...",
"Verification failed. The link may have expired.": "Verificatie mislukt. De link is mogelijk verlopen.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat kan werkruimte-inhoud doorzoeken en lezen, maar kan geen pagina's maken of bewerken.",
"Toggle AI Chat read-only mode": "Alleen-lezenmodus voor AI Chat in-/uitschakelen",
"Title only": "Alleen titel",
"you": "jij"
"you": "jij",
"Allow public spaces": "Openbare ruimtes toestaan",
"Space admins can publish their spaces to the web.": "Ruimtebeheerders kunnen hun ruimtes op het web publiceren.",
"Toggle allow public spaces": "Openbare ruimtes toestaan in-/uitschakelen",
"Publish space to the web": "Ruimte op het web publiceren",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Iedereen op internet kan elke pagina in deze ruimte lezen, behalve beperkte pagina's. Weet u het zeker?",
"Make this space publicly readable by anyone on the internet.": "Maak deze ruimte openbaar leesbaar voor iedereen op internet.",
"Toggle publish space to the web": "Publiceren van ruimte op het web in-/uitschakelen",
"Allow search engines to index": "Zoekmachines toestaan te indexeren",
"Let public pages in this space appear in search engine results.": "Laat openbare pagina's in deze ruimte verschijnen in zoekresultaten.",
"Toggle search engine indexing": "Indexering door zoekmachines in-/uitschakelen",
"Public space link": "Openbare ruimtelink",
"Copy public space link": "Openbare ruimtelink kopiëren",
"Renaming the space slug will break public links.": "Als u de slug van de ruimte wijzigt, werken openbare links niet meer.",
"Failed to update space": "Bijwerken van ruimte mislukt",
"This space is public": "Deze ruimte is openbaar",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Iedereen op internet kan de pagina's in deze ruimte lezen, behalve beperkte pagina's.",
"Open public site": "Open openbare site",
"Public": "Openbaar",
"This space has no public pages yet.": "Deze ruimte heeft nog geen openbare pagina's.",
"On this page": "Op deze pagina",
"Previous": "Vorige",
"Next": "Volgende",
"Show hidden pages": "Verborgen pagina's weergeven",
"Page navigation": "Paginanavigatie",
"Toggle sidebar": "Zijbalk in-/uitschakelen",
"Toggle table of contents": "Inhoudsopgave in-/uitschakelen",
"Appearance": "Weergave",
"Forest": "Bos",
"Violet": "Violet",
"Light mode color": "Kleur voor lichte modus",
"Dark mode color": "Kleur voor donkere modus",
"Choose the primary color of the public docs site.": "Kies de primaire kleur van de openbare documentatiesite.",
"Show page author": "Paginamaker weergeven",
"Display the page creator's name on public pages.": "Geef de naam van de maker van de pagina weer op openbare pagina's.",
"Toggle show page author": "Weergave van paginamaker in-/uitschakelen",
"Show last updated": "Laatste update weergeven",
"Display when each page was last updated.": "Geef weer wanneer elke pagina voor het laatst is bijgewerkt.",
"Toggle show last updated": "Weergave van laatste update in-/uitschakelen",
"Open public page": "Open openbare pagina",
"Toggle color scheme": "Kleurenschema in-/uitschakelen",
"Ember": "Gloeiend",
"Rose": "Roos",
"Documentation": "Documentatie",
"All published spaces.": "Alle gepubliceerde ruimtes.",
"No public spaces yet.": "Nog geen openbare ruimtes.",
"Show public directory": "Openbare directory weergeven",
"List published spaces at /docs for anyone to browse.": "Toon gepubliceerde ruimtes op /docs zodat iedereen erdoor kan bladeren.",
"Toggle show public directory": "Weergave van openbare directory in-/uitschakelen",
"Show in public directory": "Weergeven in openbare directory",
"List this space in the public directory at /docs.": "Toon deze ruimte in de openbare directory op /docs.",
"Toggle show in public directory": "Weergave in openbare directory in-/uitschakelen",
"Open public space link": "Open openbare ruimtelink",
"Disable public spaces": "Openbare ruimtes uitschakelen",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Ruimtebeheerders kunnen hun ruimtes openbaar leesbaar maken voor iedereen op internet. Weet u het zeker?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Hierdoor wordt de publicatie van elke gepubliceerde ruimte onmiddellijk ongedaan gemaakt. Als u dit later opnieuw inschakelt, worden ze niet opnieuw gepubliceerd. Weet u het zeker?",
"Allow": "Toestaan",
"Shared pages": "Gedeelde pagina's",
"Published spaces": "Gepubliceerde ruimtes",
"Spaces published to the web will appear here": "Hier verschijnen ruimtes die op het web zijn gepubliceerd",
"No published spaces": "Geen gepubliceerde ruimtes",
"Published by": "Gepubliceerd door",
"Published at": "Gepubliceerd op",
"Open space": "Ruimte openen",
"Unpublish": "Publicatie ongedaan maken",
"Unpublish space": "Publicatie van ruimte ongedaan maken",
"This space will no longer be publicly accessible. Are you sure?": "Deze ruimte zal niet langer openbaar toegankelijk zijn. Weet u het zeker?",
"More options for {{name}}": "Meer opties voor {{name}}",
"Edit page": "Pagina bewerken",
"Sign in": "Inloggen",
"Open app": "App openen",
"No spaces match your search.": "Geen ruimtes komen overeen met uw zoekopdracht.",
"Welcome to our documentation": "Welkom bij onze documentatie",
"Guides, references and answers across all our published spaces.": "Handleidingen, referenties en antwoorden in al onze gepubliceerde ruimtes.",
"Guides, references and answers across all our spaces.": "Handleidingen, referenties en antwoorden in al onze ruimtes.",
"Search documentation...": "Documentatie doorzoeken...",
"1 published space": "1 gepubliceerde ruimte",
"{{count}} published spaces": "{{count}} gepubliceerde ruimtes",
"Actions": "Acties",
"Add destination": "Bestemming toevoegen",
"Are you sure you want to delete the destination": "Weet u zeker dat u de bestemming wilt verwijderen",
"Audit logs": "Auditlogboeken",
"Audit logs & SIEM": "Auditlogboeken en SIEM",
"Auth header name": "Naam van auth-header",
"Auth header prefix": "Prefix van auth-header",
"Body format": "Body-indeling",
"Created SIEM destination": "SIEM-bestemming aangemaakt",
"Datadog site": "Datadog-site",
"Defaults to this instance's hostname": "Standaard ingesteld op de hostnaam van deze instantie",
"Delete destination": "Bestemming verwijderen",
"Deleted SIEM destination": "SIEM-bestemming verwijderd",
"Destination created": "Bestemming aangemaakt",
"Destination deleted": "Bestemming verwijderd",
"Destination updated": "Bestemming bijgewerkt",
"Disabled": "Uitgeschakeld",
"Edit destination": "Bestemming bewerken",
"Endpoint URL": "Endpoint-URL",
"Failing": "Foutstatus",
"Failing since {{time}}": "In foutstatus sinds {{time}}",
"HEC token": "HEC-token",
"HEC URL": "HEC-URL",
"Healthy": "Gezond",
"Hide advanced options": "Geavanceerde opties verbergen",
"Host": "Host",
"Index": "Index",
"Insecure: connections can be intercepted.": "Onveilig: verbindingen kunnen worden onderschept.",
"JSON array": "JSON-array",
"Last delivered": "Laatst afgeleverd",
"Last error": "Laatste fout",
"Leave empty to use the token's default index": "Laat leeg om de standaardindex van het token te gebruiken",
"Maximum of {{limit}} destinations reached": "Maximum van {{limit}} bestemmingen bereikt",
"Could not load SIEM destinations: {{message}}": "Kan SIEM-bestemmingen niet laden: {{message}}",
"No destinations yet": "Nog geen bestemmingen",
"Preset": "Voorinstelling",
"Retry now": "Nu opnieuw proberen",
"Retry scheduled": "Nieuwe poging gepland",
"Send test event": "Testgebeurtenis verzenden",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Wordt verzonden in de auth-header hieronder. Laat leeg als uw ontvanger er geen nodig heeft.",
"Service": "Service",
"Show advanced options": "Geavanceerde opties weergeven",
"SIEM": "SIEM",
"SIEM streaming": "SIEM-streaming",
"SIEM streaming requires an Enterprise license.": "SIEM-streaming vereist een Enterprise-licentie.",
"Source": "Bron",
"Sourcetype": "Brontype",
"Tags": "Tags",
"Test connection": "Verbinding testen",
"Test event delivered successfully.": "Testgebeurtenis succesvol afgeleverd.",
"Test the connection before saving.": "Test de verbinding voordat u opslaat.",
"Test event delivered to {{name}}": "Testgebeurtenis afgeleverd aan {{name}}",
"Updated SIEM destination": "SIEM-bestemming bijgewerkt",
"Verify TLS certificate": "TLS-certificaat verifiëren",
"e.g. Splunk prod": "bijv. Splunk prod"
}
@@ -793,6 +793,10 @@
"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",
"Changed page permission": "Permissão da página alterada",
"Requested password reset": "Redefinição de senha solicitada",
"Created template": "Modelo criado",
"Deleted template": "Modelo excluído",
"day": "dia",
"days": "dias",
"week": "semana",
@@ -880,6 +884,9 @@
"<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",
"SIEM destination <bold>{{name}}</bold> is failing": "O destino SIEM <bold>{{name}}</bold> está com falha",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "O destino SIEM <bold>{{name}}</bold> foi desativado após 24 horas de falhas",
"SIEM destination <bold>{{name}}</bold> recovered": "O destino SIEM <bold>{{name}}</bold> foi recuperado",
"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.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "O AI Chat pode pesquisar e ler o conteúdo do workspace, mas não pode criar nem editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo somente leitura do AI Chat",
"Title only": "Somente título",
"you": "você"
"you": "você",
"Allow public spaces": "Permitir espaços públicos",
"Space admins can publish their spaces to the web.": "Os administradores de espaços podem publicar seus espaços na web.",
"Toggle allow public spaces": "Alternar permissão de espaços públicos",
"Publish space to the web": "Publicar espaço na web",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Qualquer pessoa na internet poderá ler todas as páginas deste espaço, exceto as páginas restritas. Tem certeza?",
"Make this space publicly readable by anyone on the internet.": "Torne este espaço publicamente legível para qualquer pessoa na internet.",
"Toggle publish space to the web": "Alternar publicação do espaço na web",
"Allow search engines to index": "Permitir que mecanismos de busca indexem",
"Let public pages in this space appear in search engine results.": "Permita que as páginas públicas deste espaço apareçam nos resultados dos mecanismos de busca.",
"Toggle search engine indexing": "Alternar indexação por mecanismos de busca",
"Public space link": "Link público do espaço",
"Copy public space link": "Copiar link público do espaço",
"Renaming the space slug will break public links.": "Renomear o slug do espaço quebrará os links públicos.",
"Failed to update space": "Falha ao atualizar o espaço",
"This space is public": "Este espaço é público",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Qualquer pessoa na internet pode ler as páginas deste espaço, exceto as páginas restritas.",
"Open public site": "Abrir site público",
"Public": "Público",
"This space has no public pages yet.": "Este espaço ainda não tem páginas públicas.",
"On this page": "Nesta página",
"Previous": "Anterior",
"Next": "Próximo",
"Show hidden pages": "Mostrar páginas ocultas",
"Page navigation": "Navegação da página",
"Toggle sidebar": "Alternar barra lateral",
"Toggle table of contents": "Alternar sumário",
"Appearance": "Aparência",
"Forest": "Floresta",
"Violet": "Violeta",
"Light mode color": "Cor do modo claro",
"Dark mode color": "Cor do modo escuro",
"Choose the primary color of the public docs site.": "Escolha a cor primária do site de documentação pública.",
"Show page author": "Mostrar autor da página",
"Display the page creator's name on public pages.": "Exiba o nome do criador da página nas páginas públicas.",
"Toggle show page author": "Alternar exibição do autor da página",
"Show last updated": "Mostrar última atualização",
"Display when each page was last updated.": "Exiba quando cada página foi atualizada pela última vez.",
"Toggle show last updated": "Alternar exibição da última atualização",
"Open public page": "Abrir página pública",
"Toggle color scheme": "Alternar esquema de cores",
"Ember": "Brasa",
"Rose": "Rosa",
"Documentation": "Documentação",
"All published spaces.": "Todos os espaços publicados.",
"No public spaces yet.": "Ainda não há espaços públicos.",
"Show public directory": "Mostrar diretório público",
"List published spaces at /docs for anyone to browse.": "Liste os espaços publicados em /docs para que qualquer pessoa possa navegar.",
"Toggle show public directory": "Alternar exibição do diretório público",
"Show in public directory": "Mostrar no diretório público",
"List this space in the public directory at /docs.": "Liste este espaço no diretório público em /docs.",
"Toggle show in public directory": "Alternar exibição no diretório público",
"Open public space link": "Abrir link público do espaço",
"Disable public spaces": "Desativar espaços públicos",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Os administradores de espaços poderão tornar seus espaços publicamente legíveis para qualquer pessoa na internet. Tem certeza?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Isso removerá imediatamente a publicação de todos os espaços publicados. Reativar mais tarde não os publicará novamente. Tem certeza?",
"Allow": "Permitir",
"Shared pages": "Páginas compartilhadas",
"Published spaces": "Espaços publicados",
"Spaces published to the web will appear here": "Os espaços publicados na web aparecerão aqui",
"No published spaces": "Nenhum espaço publicado",
"Published by": "Publicado por",
"Published at": "Publicado em",
"Open space": "Abrir espaço",
"Unpublish": "Despublicar",
"Unpublish space": "Despublicar espaço",
"This space will no longer be publicly accessible. Are you sure?": "Este espaço não ficará mais acessível publicamente. Tem certeza?",
"More options for {{name}}": "Mais opções para {{name}}",
"Edit page": "Editar página",
"Sign in": "Entrar",
"Open app": "Abrir aplicativo",
"No spaces match your search.": "Nenhum espaço corresponde à sua busca.",
"Welcome to our documentation": "Boas-vindas à nossa documentação",
"Guides, references and answers across all our published spaces.": "Guias, referências e respostas em todos os nossos espaços publicados.",
"Guides, references and answers across all our spaces.": "Guias, referências e respostas em todos os nossos espaços.",
"Search documentation...": "Pesquisar na documentação...",
"1 published space": "1 espaço publicado",
"{{count}} published spaces": "{{count}} espaços publicados",
"Actions": "Ações",
"Add destination": "Adicionar destino",
"Are you sure you want to delete the destination": "Tem certeza de que deseja excluir o destino",
"Audit logs": "Logs de auditoria",
"Audit logs & SIEM": "Logs de auditoria e SIEM",
"Auth header name": "Nome do cabeçalho de autenticação",
"Auth header prefix": "Prefixo do cabeçalho de autenticação",
"Body format": "Formato do corpo",
"Created SIEM destination": "Destino SIEM criado",
"Datadog site": "Site do Datadog",
"Defaults to this instance's hostname": "O padrão é o nome do host desta instância",
"Delete destination": "Excluir destino",
"Deleted SIEM destination": "Destino SIEM excluído",
"Destination created": "Destino criado",
"Destination deleted": "Destino excluído",
"Destination updated": "Destino atualizado",
"Disabled": "Desativado",
"Edit destination": "Editar destino",
"Endpoint URL": "URL do endpoint",
"Failing": "Com falha",
"Failing since {{time}}": "Com falha desde {{time}}",
"HEC token": "Token HEC",
"HEC URL": "URL do HEC",
"Healthy": "Saudável",
"Hide advanced options": "Ocultar opções avançadas",
"Host": "Host",
"Index": "Índice",
"Insecure: connections can be intercepted.": "Inseguro: as conexões podem ser interceptadas.",
"JSON array": "Matriz JSON",
"Last delivered": "Última entrega",
"Last error": "Último erro",
"Leave empty to use the token's default index": "Deixe em branco para usar o índice padrão do token",
"Maximum of {{limit}} destinations reached": "Número máximo de {{limit}} destinos atingido",
"Could not load SIEM destinations: {{message}}": "Não foi possível carregar os destinos SIEM: {{message}}",
"No destinations yet": "Ainda não há destinos",
"Preset": "Predefinição",
"Retry now": "Tentar novamente agora",
"Retry scheduled": "Nova tentativa agendada",
"Send test event": "Enviar evento de teste",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Enviado no cabeçalho de autenticação abaixo. Deixe em branco se o seu receptor não precisar de um.",
"Service": "Serviço",
"Show advanced options": "Mostrar opções avançadas",
"SIEM": "SIEM",
"SIEM streaming": "Streaming de SIEM",
"SIEM streaming requires an Enterprise license.": "O streaming de SIEM requer uma licença Enterprise.",
"Source": "Fonte",
"Sourcetype": "Tipo de fonte",
"Tags": "Tags",
"Test connection": "Testar conexão",
"Test event delivered successfully.": "Evento de teste entregue com sucesso.",
"Test the connection before saving.": "Teste a conexão antes de salvar.",
"Test event delivered to {{name}}": "Evento de teste entregue para {{name}}",
"Updated SIEM destination": "Destino SIEM atualizado",
"Verify TLS certificate": "Verificar certificado TLS",
"e.g. Splunk prod": "ex.: Splunk produção"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Ограничение доступа к странице удалено",
"Added page permission": "Добавлено разрешение доступа к странице",
"Removed page permission": "Удалено разрешение доступа к странице",
"Changed page permission": "Изменено разрешение доступа к странице",
"Requested password reset": "Запрошен сброс пароля",
"Created template": "Создан шаблон",
"Deleted template": "Удалён шаблон",
"day": "день",
"days": "дни",
"week": "неделя",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> вернул(а) страницу на доработку",
"Page verification expires soon": "Срок проверки страницы скоро истекает",
"Page verification has expired": "Срок проверки страницы истёк",
"SIEM destination <bold>{{name}}</bold> is failing": "Сбой в месте назначения SIEM <bold>{{name}}</bold>",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "Место назначения SIEM <bold>{{name}}</bold> было отключено после 24 часов сбоев",
"SIEM destination <bold>{{name}}</bold> recovered": "Место назначения SIEM <bold>{{name}}</bold> восстановлено",
"Verifying your email": "Подтверждение вашего адреса электронной почты",
"Please wait...": "Пожалуйста, подождите...",
"Verification failed. The link may have expired.": "Ошибка проверки. Ссылка могла устареть.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat может искать и читать содержимое рабочего пространства, но не может создавать или редактировать страницы.",
"Toggle AI Chat read-only mode": "Переключить режим только для чтения для AI Chat",
"Title only": "Только заголовок",
"you": "вы"
"you": "вы",
"Allow public spaces": "Разрешить публичные пространства",
"Space admins can publish their spaces to the web.": "Администраторы пространств смогут публиковать свои пространства в интернете.",
"Toggle allow public spaces": "Переключить разрешение публичных пространств",
"Publish space to the web": "Опубликовать пространство в интернете",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Любой пользователь в интернете сможет читать каждую страницу в этом пространстве, кроме страниц с ограниченным доступом. Вы уверены?",
"Make this space publicly readable by anyone on the internet.": "Сделать это пространство общедоступным для чтения любым пользователем в интернете.",
"Toggle publish space to the web": "Переключить публикацию пространства в интернете",
"Allow search engines to index": "Разрешить поисковым системам индексировать",
"Let public pages in this space appear in search engine results.": "Разрешить публичным страницам в этом пространстве появляться в результатах поиска.",
"Toggle search engine indexing": "Переключить индексацию поисковыми системами",
"Public space link": "Ссылка на публичное пространство",
"Copy public space link": "Скопировать ссылку на публичное пространство",
"Renaming the space slug will break public links.": "Переименование slug пространства приведёт к поломке публичных ссылок.",
"Failed to update space": "Не удалось обновить пространство",
"This space is public": "Это пространство является публичным",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Любой пользователь в интернете может читать страницы в этом пространстве, кроме страниц с ограниченным доступом.",
"Open public site": "Открыть публичный сайт",
"Public": "Публичное",
"This space has no public pages yet.": "В этом пространстве пока нет публичных страниц.",
"On this page": "На этой странице",
"Previous": "Назад",
"Next": "Далее",
"Show hidden pages": "Показать скрытые страницы",
"Page navigation": "Навигация по странице",
"Toggle sidebar": "Переключить боковую панель",
"Toggle table of contents": "Переключить оглавление",
"Appearance": "Внешний вид",
"Forest": "Лесной",
"Violet": "Фиолетовый",
"Light mode color": "Цвет светлой темы",
"Dark mode color": "Цвет тёмной темы",
"Choose the primary color of the public docs site.": "Выберите основной цвет публичного сайта документации.",
"Show page author": "Показывать автора страницы",
"Display the page creator's name on public pages.": "Отображать имя создателя страницы на публичных страницах.",
"Toggle show page author": "Переключить показ автора страницы",
"Show last updated": "Показывать последнее обновление",
"Display when each page was last updated.": "Отображать время последнего обновления каждой страницы.",
"Toggle show last updated": "Переключить показ последнего обновления",
"Open public page": "Открыть публичную страницу",
"Toggle color scheme": "Переключить цветовую схему",
"Ember": "Янтарный",
"Rose": "Розовый",
"Documentation": "Документация",
"All published spaces.": "Все опубликованные пространства.",
"No public spaces yet.": "Пока нет публичных пространств.",
"Show public directory": "Показывать публичный каталог",
"List published spaces at /docs for anyone to browse.": "Показывать опубликованные пространства по адресу /docs, чтобы их мог просматривать любой пользователь.",
"Toggle show public directory": "Переключить показ публичного каталога",
"Show in public directory": "Показывать в публичном каталоге",
"List this space in the public directory at /docs.": "Показывать это пространство в публичном каталоге по адресу /docs.",
"Toggle show in public directory": "Переключить показ в публичном каталоге",
"Open public space link": "Открыть ссылку на публичное пространство",
"Disable public spaces": "Отключить публичные пространства",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Администраторы пространств смогут делать свои пространства общедоступными для чтения любым пользователем в интернете. Вы уверены?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Это приведёт к немедленной отмене публикации всех опубликованных пространств. Повторное включение позже не опубликует их заново. Вы уверены?",
"Allow": "Разрешить",
"Shared pages": "Общие страницы",
"Published spaces": "Опубликованные пространства",
"Spaces published to the web will appear here": "Здесь будут отображаться пространства, опубликованные в интернете",
"No published spaces": "Нет опубликованных пространств",
"Published by": "Опубликовано пользователем",
"Published at": "Опубликовано",
"Open space": "Открыть пространство",
"Unpublish": "Снять с публикации",
"Unpublish space": "Снять пространство с публикации",
"This space will no longer be publicly accessible. Are you sure?": "Это пространство больше не будет общедоступным. Вы уверены?",
"More options for {{name}}": "Дополнительные параметры для {{name}}",
"Edit page": "Редактировать страницу",
"Sign in": "Войти",
"Open app": "Открыть приложение",
"No spaces match your search.": "Нет пространств, соответствующих вашему запросу.",
"Welcome to our documentation": "Добро пожаловать в нашу документацию",
"Guides, references and answers across all our published spaces.": "Руководства, справочные материалы и ответы во всех наших опубликованных пространствах.",
"Guides, references and answers across all our spaces.": "Руководства, справочные материалы и ответы во всех наших пространствах.",
"Search documentation...": "Поиск по документации...",
"1 published space": "1 опубликованное пространство",
"{{count}} published spaces": "{count, plural, one {# опубликованное пространство} few {# опубликованных пространства} many {# опубликованных пространств} other {# опубликованного пространства}}",
"Actions": "Действия",
"Add destination": "Добавить место назначения",
"Are you sure you want to delete the destination": "Вы уверены, что хотите удалить место назначения",
"Audit logs": "Журналы аудита",
"Audit logs & SIEM": "Журналы аудита и SIEM",
"Auth header name": "Имя заголовка авторизации",
"Auth header prefix": "Префикс заголовка авторизации",
"Body format": "Формат тела",
"Created SIEM destination": "Создано место назначения SIEM",
"Datadog site": "Сайт Datadog",
"Defaults to this instance's hostname": "По умолчанию используется имя хоста этого экземпляра",
"Delete destination": "Удалить место назначения",
"Deleted SIEM destination": "Удалено место назначения SIEM",
"Destination created": "Место назначения создано",
"Destination deleted": "Место назначения удалено",
"Destination updated": "Место назначения обновлено",
"Disabled": "Отключено",
"Edit destination": "Изменить место назначения",
"Endpoint URL": "URL конечной точки",
"Failing": "Сбой",
"Failing since {{time}}": "Сбой с {{time}}",
"HEC token": "Токен HEC",
"HEC URL": "URL HEC",
"Healthy": "Исправно",
"Hide advanced options": "Скрыть дополнительные параметры",
"Host": "Хост",
"Index": "Индекс",
"Insecure: connections can be intercepted.": "Небезопасно: соединения могут быть перехвачены.",
"JSON array": "Массив JSON",
"Last delivered": "Последняя доставка",
"Last error": "Последняя ошибка",
"Leave empty to use the token's default index": "Оставьте пустым, чтобы использовать индекс токена по умолчанию",
"Maximum of {{limit}} destinations reached": "Достигнуто максимальное количество мест назначения: {{limit}}",
"Could not load SIEM destinations: {{message}}": "Не удалось загрузить места назначения SIEM: {{message}}",
"No destinations yet": "Пока нет мест назначения",
"Preset": "Предустановка",
"Retry now": "Повторить сейчас",
"Retry scheduled": "Повторная попытка запланирована",
"Send test event": "Отправить тестовое событие",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Отправляется в указанном ниже заголовке авторизации. Оставьте пустым, если вашему получателю он не нужен.",
"Service": "Сервис",
"Show advanced options": "Показать дополнительные параметры",
"SIEM": "SIEM",
"SIEM streaming": "Потоковая передача в SIEM",
"SIEM streaming requires an Enterprise license.": "Для потоковой передачи в SIEM требуется лицензия Enterprise.",
"Source": "Источник",
"Sourcetype": "Тип источника",
"Tags": "Теги",
"Test connection": "Проверить соединение",
"Test event delivered successfully.": "Тестовое событие успешно доставлено.",
"Test the connection before saving.": "Проверьте соединение перед сохранением.",
"Test event delivered to {{name}}": "Тестовое событие доставлено в {{name}}",
"Updated SIEM destination": "Обновлено место назначения SIEM",
"Verify TLS certificate": "Проверять сертификат TLS",
"e.g. Splunk prod": "например, Splunk prod"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "Обмеження сторінки видалено",
"Added page permission": "Додано дозвіл на сторінку",
"Removed page permission": "Дозвіл на сторінку видалено",
"Changed page permission": "Змінено дозволи сторінки",
"Requested password reset": "Запитано скидання пароля",
"Created template": "Створено шаблон",
"Deleted template": "Видалено шаблон",
"day": "день",
"days": "дні",
"week": "тиждень",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> повернув(-ла) сторінку на доопрацювання",
"Page verification expires soon": "Термін перевірки сторінки скоро спливає",
"Page verification has expired": "Термін перевірки сторінки сплив",
"SIEM destination <bold>{{name}}</bold> is failing": "Збій у призначенні SIEM <bold>{{name}}</bold>",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "Призначення SIEM <bold>{{name}}</bold> було вимкнено після 24 годин збоїв",
"SIEM destination <bold>{{name}}</bold> recovered": "Роботу призначення SIEM <bold>{{name}}</bold> відновлено",
"Verifying your email": "Перевірка вашої електронної пошти",
"Please wait...": "Будь ласка, зачекайте...",
"Verification failed. The link may have expired.": "Підтвердження не вдалося. Посилання могло втратити чинність.",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat може шукати та читати вміст робочого простору, але не може створювати або редагувати сторінки.",
"Toggle AI Chat read-only mode": "Перемкнути режим лише читання для AI Chat",
"Title only": "Лише заголовок",
"you": "ви"
"you": "ви",
"Allow public spaces": "Дозволити публічні простори",
"Space admins can publish their spaces to the web.": "Адміністратори просторів можуть публікувати свої простори в інтернеті.",
"Toggle allow public spaces": "Перемкнути дозвіл на публічні простори",
"Publish space to the web": "Опублікувати простір в інтернеті",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "Будь-хто в інтернеті зможе читати кожну сторінку в цьому просторі, крім обмежених сторінок. Ви впевнені?",
"Make this space publicly readable by anyone on the internet.": "Зробити цей простір публічно доступним для читання будь-ким в інтернеті.",
"Toggle publish space to the web": "Перемкнути публікацію простору в інтернеті",
"Allow search engines to index": "Дозволити пошуковим системам індексувати",
"Let public pages in this space appear in search engine results.": "Дозволити публічним сторінкам у цьому просторі з’являтися в результатах пошуку.",
"Toggle search engine indexing": "Перемкнути індексацію пошуковими системами",
"Public space link": "Публічне посилання на простір",
"Copy public space link": "Скопіювати публічне посилання на простір",
"Renaming the space slug will break public links.": "Перейменування slug простору зламає публічні посилання.",
"Failed to update space": "Не вдалося оновити простір",
"This space is public": "Цей простір є публічним",
"Anyone on the internet can read the pages in this space, except restricted pages.": "Будь-хто в інтернеті може читати сторінки в цьому просторі, крім обмежених сторінок.",
"Open public site": "Відкрити публічний сайт",
"Public": "Публічний",
"This space has no public pages yet.": "У цьому просторі ще немає публічних сторінок.",
"On this page": "На цій сторінці",
"Previous": "Попередня",
"Next": "Далі",
"Show hidden pages": "Показати приховані сторінки",
"Page navigation": "Навігація сторінками",
"Toggle sidebar": "Перемкнути бічну панель",
"Toggle table of contents": "Перемкнути зміст",
"Appearance": "Зовнішній вигляд",
"Forest": "Лісовий",
"Violet": "Фіолетовий",
"Light mode color": "Колір світлого режиму",
"Dark mode color": "Колір темного режиму",
"Choose the primary color of the public docs site.": "Виберіть основний колір публічного сайту документації.",
"Show page author": "Показувати автора сторінки",
"Display the page creator's name on public pages.": "Показувати ім’я автора сторінки на публічних сторінках.",
"Toggle show page author": "Перемкнути показ автора сторінки",
"Show last updated": "Показувати останнє оновлення",
"Display when each page was last updated.": "Показувати, коли кожну сторінку було востаннє оновлено.",
"Toggle show last updated": "Перемкнути показ останнього оновлення",
"Open public page": "Відкрити публічну сторінку",
"Toggle color scheme": "Перемкнути колірну схему",
"Ember": "Жар",
"Rose": "Трояндовий",
"Documentation": "Документація",
"All published spaces.": "Усі опубліковані простори.",
"No public spaces yet.": "Публічних просторів ще немає.",
"Show public directory": "Показувати публічний каталог",
"List published spaces at /docs for anyone to browse.": "Показувати список опублікованих просторів за адресою /docs, щоб кожен міг їх переглядати.",
"Toggle show public directory": "Перемкнути показ публічного каталогу",
"Show in public directory": "Показувати в публічному каталозі",
"List this space in the public directory at /docs.": "Показувати цей простір у публічному каталозі за адресою /docs.",
"Toggle show in public directory": "Перемкнути показ у публічному каталозі",
"Open public space link": "Відкрити публічне посилання на простір",
"Disable public spaces": "Вимкнути публічні простори",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "Адміністратори просторів зможуть зробити свої простори публічно доступними для читання будь-ким в інтернеті. Ви впевнені?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "Це негайно скасує публікацію всіх опублікованих просторів. Повторне ввімкнення пізніше не опублікує їх знову. Ви впевнені?",
"Allow": "Дозволити",
"Shared pages": "Спільні сторінки",
"Published spaces": "Опубліковані простори",
"Spaces published to the web will appear here": "Тут з’являться простори, опубліковані в інтернеті",
"No published spaces": "Немає опублікованих просторів",
"Published by": "Опубліковано",
"Published at": "Опубліковано",
"Open space": "Відкрити простір",
"Unpublish": "Скасувати публікацію",
"Unpublish space": "Скасувати публікацію простору",
"This space will no longer be publicly accessible. Are you sure?": "Цей простір більше не буде публічно доступним. Ви впевнені?",
"More options for {{name}}": "Додаткові параметри для {{name}}",
"Edit page": "Редагувати сторінку",
"Sign in": "Увійти",
"Open app": "Відкрити застосунок",
"No spaces match your search.": "Жоден простір не відповідає вашому запиту.",
"Welcome to our documentation": "Ласкаво просимо до нашої документації",
"Guides, references and answers across all our published spaces.": "Посібники, довідкові матеріали та відповіді в усіх наших опублікованих просторах.",
"Guides, references and answers across all our spaces.": "Посібники, довідкові матеріали та відповіді в усіх наших просторах.",
"Search documentation...": "Шукати в документації...",
"1 published space": "1 опублікований простір",
"{{count}} published spaces": "{{count}} опублікованих просторів",
"Actions": "Дії",
"Add destination": "Додати призначення",
"Are you sure you want to delete the destination": "Ви впевнені, що хочете видалити призначення",
"Audit logs": "Журнали аудиту",
"Audit logs & SIEM": "Журнали аудиту й SIEM",
"Auth header name": "Назва заголовка авторизації",
"Auth header prefix": "Префікс заголовка авторизації",
"Body format": "Формат тіла",
"Created SIEM destination": "Створено призначення SIEM",
"Datadog site": "Сайт Datadog",
"Defaults to this instance's hostname": "За замовчуванням використовується ім’я хоста цього екземпляра",
"Delete destination": "Видалити призначення",
"Deleted SIEM destination": "Видалено призначення SIEM",
"Destination created": "Призначення створено",
"Destination deleted": "Призначення видалено",
"Destination updated": "Призначення оновлено",
"Disabled": "Вимкнено",
"Edit destination": "Редагувати призначення",
"Endpoint URL": "URL кінцевої точки",
"Failing": "Збій",
"Failing since {{time}}": "Збій із {{time}}",
"HEC token": "Токен HEC",
"HEC URL": "URL HEC",
"Healthy": "Справний",
"Hide advanced options": "Приховати додаткові параметри",
"Host": "Хост",
"Index": "Індекс",
"Insecure: connections can be intercepted.": "Небезпечно: з’єднання можуть бути перехоплені.",
"JSON array": "Масив JSON",
"Last delivered": "Остання доставка",
"Last error": "Остання помилка",
"Leave empty to use the token's default index": "Залиште порожнім, щоб використовувати індекс токена за замовчуванням",
"Maximum of {{limit}} destinations reached": "Досягнуто максимальної кількості призначень: {{limit}}",
"Could not load SIEM destinations: {{message}}": "Не вдалося завантажити призначення SIEM: {{message}}",
"No destinations yet": "Призначень ще немає",
"Preset": "Попереднє налаштування",
"Retry now": "Повторити зараз",
"Retry scheduled": "Повторну спробу заплановано",
"Send test event": "Надіслати тестову подію",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "Надсилається в наведеному нижче заголовку авторизації. Залиште порожнім, якщо вашому отримувачу він не потрібен.",
"Service": "Сервіс",
"Show advanced options": "Показати додаткові параметри",
"SIEM": "SIEM",
"SIEM streaming": "Потокова передача SIEM",
"SIEM streaming requires an Enterprise license.": "Для потокової передачі SIEM потрібна ліцензія Enterprise.",
"Source": "Джерело",
"Sourcetype": "Тип джерела",
"Tags": "Теги",
"Test connection": "Перевірити з’єднання",
"Test event delivered successfully.": "Тестову подію успішно доставлено.",
"Test the connection before saving.": "Перевірте з’єднання перед збереженням.",
"Test event delivered to {{name}}": "Тестову подію доставлено до {{name}}",
"Updated SIEM destination": "Оновлено призначення SIEM",
"Verify TLS certificate": "Перевіряти сертифікат TLS",
"e.g. Splunk prod": "напр., Splunk prod"
}
@@ -793,6 +793,10 @@
"Removed page restriction": "已移除页面限制",
"Added page permission": "已添加页面权限",
"Removed page permission": "已移除页面权限",
"Changed page permission": "已更改页面权限",
"Requested password reset": "已请求重置密码",
"Created template": "已创建模板",
"Deleted template": "已删除模板",
"day": "天",
"days": "天",
"week": "周",
@@ -880,6 +884,9 @@
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> 退回了一个页面以供修改",
"Page verification expires soon": "页面验证即将过期",
"Page verification has expired": "页面验证已过期",
"SIEM destination <bold>{{name}}</bold> is failing": "SIEM 目标 <bold>{{name}}</bold> 发生故障",
"SIEM destination <bold>{{name}}</bold> was disabled after 24 hours of failures": "SIEM 目标 <bold>{{name}}</bold> 在连续 24 小时故障后已被禁用",
"SIEM destination <bold>{{name}}</bold> recovered": "SIEM 目标 <bold>{{name}}</bold> 已恢复",
"Verifying your email": "正在验证您的邮箱",
"Please wait...": "请稍候……",
"Verification failed. The link may have expired.": "验证失败。该链接可能已过期。",
@@ -1337,5 +1344,137 @@
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 聊天可以搜索和读取工作区内容,但不能创建或编辑页面。",
"Toggle AI Chat read-only mode": "切换 AI 聊天只读模式",
"Title only": "仅标题",
"you": "你"
"you": "你",
"Allow public spaces": "允许公开空间",
"Space admins can publish their spaces to the web.": "空间管理员可以将其空间发布到 Web。",
"Toggle allow public spaces": "切换是否允许公开空间",
"Publish space to the web": "将空间发布到 Web",
"Anyone on the internet will be able to read every page in this space, except restricted pages. Are you sure?": "互联网上的任何人都将能够阅读此空间中的每个页面,但受限页面除外。您确定吗?",
"Make this space publicly readable by anyone on the internet.": "使此空间对互联网上的任何人公开可读。",
"Toggle publish space to the web": "切换将空间发布到 Web",
"Allow search engines to index": "允许搜索引擎编入索引",
"Let public pages in this space appear in search engine results.": "让此空间中的公开页面显示在搜索引擎结果中。",
"Toggle search engine indexing": "切换搜索引擎索引",
"Public space link": "公开空间链接",
"Copy public space link": "复制公开空间链接",
"Renaming the space slug will break public links.": "重命名空间 slug 会导致公开链接失效。",
"Failed to update space": "更新空间失败",
"This space is public": "此空间是公开的",
"Anyone on the internet can read the pages in this space, except restricted pages.": "互联网上的任何人都可以阅读此空间中的页面,但受限页面除外。",
"Open public site": "打开公开站点",
"Public": "公开",
"This space has no public pages yet.": "此空间中尚无公开页面。",
"On this page": "本页内容",
"Previous": "上一页",
"Next": "下一页",
"Show hidden pages": "显示隐藏页面",
"Page navigation": "页面导航",
"Toggle sidebar": "切换侧边栏",
"Toggle table of contents": "切换目录",
"Appearance": "外观",
"Forest": "森林绿",
"Violet": "紫罗兰",
"Light mode color": "浅色模式颜色",
"Dark mode color": "深色模式颜色",
"Choose the primary color of the public docs site.": "选择公开文档站点的主色。",
"Show page author": "显示页面作者",
"Display the page creator's name on public pages.": "在公开页面上显示页面创建者的姓名。",
"Toggle show page author": "切换显示页面作者",
"Show last updated": "显示最后更新时间",
"Display when each page was last updated.": "显示每个页面上次更新的时间。",
"Toggle show last updated": "切换显示最后更新时间",
"Open public page": "打开公开页面",
"Toggle color scheme": "切换配色方案",
"Ember": "余烬橙",
"Rose": "玫瑰红",
"Documentation": "文档",
"All published spaces.": "所有已发布的空间。",
"No public spaces yet.": "尚无公开空间。",
"Show public directory": "显示公开目录",
"List published spaces at /docs for anyone to browse.": "在 /docs 列出已发布的空间,供任何人浏览。",
"Toggle show public directory": "切换显示公开目录",
"Show in public directory": "在公开目录中显示",
"List this space in the public directory at /docs.": "在 /docs 的公开目录中列出此空间。",
"Toggle show in public directory": "切换在公开目录中显示",
"Open public space link": "打开公开空间链接",
"Disable public spaces": "禁用公开空间",
"Space admins will be able to make their spaces publicly readable by anyone on the internet. Are you sure?": "空间管理员将能够使其空间对互联网上的任何人公开可读。您确定吗?",
"This will immediately unpublish every published space. Re-enabling later will not republish them. Are you sure?": "这将立即取消发布所有已发布的空间。稍后重新启用也不会重新发布它们。您确定吗?",
"Allow": "允许",
"Shared pages": "共享页面",
"Published spaces": "已发布的空间",
"Spaces published to the web will appear here": "发布到 Web 的空间将显示在这里",
"No published spaces": "没有已发布的空间",
"Published by": "发布者",
"Published at": "发布时间",
"Open space": "打开空间",
"Unpublish": "取消发布",
"Unpublish space": "取消发布空间",
"This space will no longer be publicly accessible. Are you sure?": "此空间将不再可被公开访问。您确定吗?",
"More options for {{name}}": "{{name}} 的更多选项",
"Edit page": "编辑页面",
"Sign in": "登录",
"Open app": "打开应用",
"No spaces match your search.": "没有符合您搜索条件的空间。",
"Welcome to our documentation": "欢迎来到我们的文档",
"Guides, references and answers across all our published spaces.": "涵盖我们所有已发布空间的指南、参考资料和解答。",
"Guides, references and answers across all our spaces.": "涵盖我们所有空间的指南、参考资料和解答。",
"Search documentation...": "搜索文档……",
"1 published space": "1 个已发布空间",
"{{count}} published spaces": "{{count}} 个已发布空间",
"Actions": "操作",
"Add destination": "添加目标",
"Are you sure you want to delete the destination": "您确定要删除该目标吗",
"Audit logs": "审计日志",
"Audit logs & SIEM": "审计日志和 SIEM",
"Auth header name": "身份验证标头名称",
"Auth header prefix": "身份验证标头前缀",
"Body format": "正文格式",
"Created SIEM destination": "已创建 SIEM 目标",
"Datadog site": "Datadog 站点",
"Defaults to this instance's hostname": "默认为此实例的主机名",
"Delete destination": "删除目标",
"Deleted SIEM destination": "已删除 SIEM 目标",
"Destination created": "目标已创建",
"Destination deleted": "目标已删除",
"Destination updated": "目标已更新",
"Disabled": "已禁用",
"Edit destination": "编辑目标",
"Endpoint URL": "端点 URL",
"Failing": "故障中",
"Failing since {{time}}": "自 {{time}} 起发生故障",
"HEC token": "HEC 令牌",
"HEC URL": "HEC URL",
"Healthy": "正常",
"Hide advanced options": "隐藏高级选项",
"Host": "主机",
"Index": "索引",
"Insecure: connections can be intercepted.": "不安全:连接可能被拦截。",
"JSON array": "JSON 数组",
"Last delivered": "上次传送",
"Last error": "上次错误",
"Leave empty to use the token's default index": "留空以使用令牌的默认索引",
"Maximum of {{limit}} destinations reached": "最多只能添加 {{limit}} 个目标",
"Could not load SIEM destinations: {{message}}": "无法加载 SIEM 目标:{{message}}",
"No destinations yet": "暂无目标",
"Preset": "预设",
"Retry now": "立即重试",
"Retry scheduled": "已计划重试",
"Send test event": "发送测试事件",
"Sent in the auth header below. Leave empty if your receiver does not need one.": "将在下方的身份验证标头中发送。如果您的接收端不需要,请留空。",
"Service": "服务",
"Show advanced options": "显示高级选项",
"SIEM": "SIEM",
"SIEM streaming": "SIEM 流式传输",
"SIEM streaming requires an Enterprise license.": "SIEM 流式传输需要企业版许可证。",
"Source": "来源",
"Sourcetype": "源类型",
"Tags": "标签",
"Test connection": "测试连接",
"Test event delivered successfully.": "测试事件发送成功。",
"Test the connection before saving.": "请在保存前测试连接。",
"Test event delivered to {{name}}": "测试事件已发送到 {{name}}",
"Updated SIEM destination": "已更新 SIEM 目标",
"Verify TLS certificate": "验证 TLS 证书",
"e.g. Splunk prod": "例如:Splunk 生产环境"
}
@@ -18,6 +18,7 @@ export type FieldProps = {
rowId: string;
readOnly: boolean;
onChange: (value: unknown) => void;
onEditingChange?: (editing: boolean) => void;
};
type FieldShellProps = {
@@ -99,9 +100,10 @@ type DetailFieldProps = {
row: IBaseRow;
readOnly: boolean;
onUpdate: (propertyId: string, value: unknown) => void;
onEditingChange: (editing: boolean) => void;
};
export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldProps) {
export function DetailField({ property, row, readOnly, onUpdate, onEditingChange }: DetailFieldProps) {
const descriptor = getDescriptor(property.type);
const value = descriptor?.systemAccessor
? descriptor.systemAccessor(row)
@@ -112,6 +114,7 @@ export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldPr
rowId: row.id,
readOnly,
onChange: (next: unknown) => onUpdate(property.id, next),
onEditingChange
};
switch (property.type) {
@@ -9,7 +9,13 @@ const normalize = (s: string) => {
return trimmed.length ? trimmed : null;
};
export function FieldLongText({ property, value, readOnly, onChange }: FieldProps) {
export function FieldLongText({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const text = toText(value);
const [draft, setDraft] = useState(text);
const [focused, setFocused] = useState(false);
@@ -23,6 +29,7 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(text);
@@ -50,7 +57,10 @@ export function FieldLongText({ property, value, readOnly, onChange }: FieldProp
className={classes.fieldTextarea}
classNames={{ input: classes.fieldTextareaInput }}
value={draft}
onFocus={() => setFocused(true)}
onFocus={() => {
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => setDraft(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -11,7 +11,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
const toDraft = (value: unknown) =>
typeof value === "number" ? String(value) : "";
export function FieldNumber({ property, value, readOnly, onChange }: FieldProps) {
export function FieldNumber({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const typeOptions = property.typeOptions as NumberTypeOptions | undefined;
const numValue = typeof value === "number" ? value : null;
const [draft, setDraft] = useState(toDraft(value));
@@ -36,6 +42,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(toDraft(value));
@@ -54,6 +61,7 @@ export function FieldNumber({ property, value, readOnly, onChange }: FieldProps)
onFocus={() => {
setDraft(toDraft(value));
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => {
const v = e.target.value;
@@ -5,7 +5,13 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
const toText = (value: unknown) => (typeof value === "string" ? value : "");
export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
export function FieldText({
property,
value,
readOnly,
onChange,
onEditingChange,
}: FieldProps) {
const text = toText(value);
const [draft, setDraft] = useState(text);
const [focused, setFocused] = useState(false);
@@ -20,6 +26,7 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
const commit = () => {
setFocused(false);
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setDraft(text);
@@ -54,7 +61,10 @@ export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
className={classes.fieldInput}
value={draft}
maxLength={1000}
onFocus={() => setFocused(true)}
onFocus={() => {
setFocused(true);
onEditingChange?.(true);
}}
onChange={(e) => setDraft(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
@@ -17,6 +17,7 @@ type PropertyRowProps = {
onMenuOpenChange: (opened: boolean) => void;
onMenuDirtyChange: (dirty: boolean) => void;
onUpdate: (propertyId: string, value: unknown) => void;
onEditingChange?: (editing: boolean) => void;
autoFocusValue?: boolean;
onAutoFocused?: () => void;
};
@@ -29,6 +30,7 @@ export function PropertyRow({
onMenuOpenChange,
onMenuDirtyChange,
onUpdate,
onEditingChange,
autoFocusValue,
onAutoFocused,
}: PropertyRowProps) {
@@ -112,6 +114,7 @@ export function PropertyRow({
row={row}
readOnly={!canEdit}
onUpdate={onUpdate}
onEditingChange={onEditingChange}
/>
</div>
);
@@ -75,6 +75,7 @@ export function RowDetailModal({
const isSaving = updateRowMutation.isPending;
const opened = !!openRowId;
const [editingField, setEditingField] = useState(false);
// One field menu open at a time, mirroring the grid header's semantics.
// The shared closeRequest atom asks an open dirty PropertyMenuContent to
@@ -90,6 +91,7 @@ export function RowDetailModal({
useEffect(() => {
setOpenMenuId(null);
menuDirtyRef.current = false;
setEditingField(false);
}, [openRowId]);
const handleMenuDirtyChange = useCallback((dirty: boolean) => {
@@ -293,7 +295,7 @@ export function RowDetailModal({
row={row}
primaryProperty={primaryProperty}
canEdit={canEdit}
onClose={onClose}
onEditingChange={setEditingField}
onCommit={(value) => {
if (!primaryProperty) return;
updateRowMutation.mutate({
@@ -317,6 +319,7 @@ export function RowDetailModal({
autoFocusValue={property.id === newPropertyId}
onAutoFocused={clearNewProperty}
menuOpened={openMenuId === property.id}
onEditingChange={setEditingField}
onMenuOpenChange={(nextOpened) =>
handleMenuOpenChange(property.id, nextOpened)
}
@@ -367,16 +370,38 @@ export function RowDetailModal({
) : null}
</div>
<div className={classes.kbdHint}>
{rowIndex >= 0 && rows.length > 1 && (
{editingField ? (
<>
<kbd className={classes.kbd}></kbd>
<kbd className={classes.kbd}></kbd>
<span>{t("to navigate")}</span>
<span className={classes.kbdGroup}>
<kbd className={classes.kbd}>Ctrl/Cmd</kbd>
<span className={classes.kbdPlus} >+</span>
<kbd className={classes.kbd}>Enter</kbd>
<span>{t("to save")}</span>
</span>
<span className={classes.kbdSeparator} />
<span className={classes.kbdGroup}>
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to reset")}</span>
</span>
</>
) : (
<>
{rowIndex >= 0 && rows.length > 1 && (
<>
<kbd className={classes.kbd}></kbd>
<kbd className={classes.kbd}></kbd>
<span>{t("to navigate")}</span>
<span className={classes.kbdSeparator} />
</>
)}
<>
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to close")}</span>
</>
</>
)}
<kbd className={classes.kbd}>Esc</kbd>
<span>{t("to close")}</span>
</div>
</footer>
</>
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { IBaseProperty, IBaseRow } from "@/ee/base/types/base.types";
import { timeAgo } from "@/lib/time.ts";
@@ -9,7 +9,7 @@ type RowDetailTitleProps = {
primaryProperty: IBaseProperty | undefined;
canEdit: boolean;
onCommit: (value: string) => void;
onClose: () => void;
onEditingChange?: (editing: boolean) => void;
};
export function RowDetailTitle({
@@ -17,13 +17,24 @@ export function RowDetailTitle({
primaryProperty,
canEdit,
onCommit,
onClose,
onEditingChange,
}: RowDetailTitleProps) {
const { t } = useTranslation();
const initial = primaryProperty
? (((row.cells ?? {})[primaryProperty.id] as string) ?? "")
: "";
const [value, setValue] = useState(initial);
const cancelRef = useRef(false);
const commit = () => {
onEditingChange?.(false);
if (cancelRef.current) {
cancelRef.current = false;
setValue(initial);
return;
}
if (value !== initial) onCommit(value);
};
// Re-sync when the row changes underneath us (navigation or remote edit).
useEffect(() => {
@@ -43,18 +54,18 @@ export function RowDetailTitle({
aria-label={primaryProperty?.name ?? t("Untitled")}
value={value}
maxLength={1000}
onChange={(e) => setValue(e.currentTarget.value)}
onBlur={() => {
if (value !== initial) onCommit(value);
onFocus={() => {
onEditingChange?.(true);
}}
onChange={(e) => setValue(e.currentTarget.value)}
onBlur={commit}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === "Escape") {
cancelRef.current = true;
e.currentTarget.blur();
} else if (e.key === "Enter") {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
} else if (e.key === "Escape") {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
onClose();
e.currentTarget.blur();
}
}}
/>
@@ -416,9 +416,25 @@
}
.kbdHint {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
width: 100%;
}
.kbdGroup {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
height: fit-content;
}
.kbdPlus {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
font-size: 11px;
}
.kbdSeparator {
@@ -66,7 +66,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
}
// Clear search term in editor
if (isEditorReady(editor)) {
editor.commands.setSearchTerm("");
editor.commands.setSearchTerms([""]);
}
};
@@ -117,7 +117,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
useEffect(() => {
if (!isEditorReady(editor)) return;
editor.commands.setSearchTerm(searchText);
editor.commands.setSearchTerms([searchText]);
editor.commands.resetIndex();
editor.commands.selectCurrentItem();
}, [searchText]);
@@ -181,8 +181,10 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
const location = useLocation();
useEffect(() => {
closeDialog();
}, [location]);
if (pageFindState.isOpen) {
closeDialog();
}
}, [location.pathname]);
return (
<Dialog
@@ -0,0 +1,205 @@
import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core";
import {
IconArrowNarrowDown,
IconArrowNarrowUp,
IconX,
} from "@tabler/icons-react";
import { useEditor } from "@tiptap/react";
import { isEditorReady } from "@docmost/editor-ext";
import React, { useCallback, useEffect, useRef, useState } from "react";
import classes from "./search-replace.module.css";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
interface SearchNavigationDialogProps {
editor: ReturnType<typeof useEditor>;
}
interface SearchNavigationEvent extends CustomEvent {
detail: {
searchTerms: string[];
wholeWord?: boolean;
};
}
function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const openRef = useRef(false);
const [resultState, setResultState] = useState({
resultIndex: 0,
resultsLength: 0,
});
const goToSelection = () => {
if (!isEditorReady(editor)) return;
const { results, resultIndex } = editor.storage.searchAndReplace;
const position = results[resultIndex];
setResultState({
resultsLength: results.length,
resultIndex,
});
if (!position) return;
requestAnimationFrame(() => {
document
.querySelector(".search-result-current")
?.scrollIntoView({ behavior: "smooth", block: "center" });
});
};
const next = () => {
if (!isEditorReady(editor)) return;
editor.commands.nextSearchResult();
goToSelection();
};
const previous = () => {
if (!isEditorReady(editor)) return;
editor.commands.previousSearchResult();
goToSelection();
};
const close = useCallback(() => {
if (!openRef.current) return;
openRef.current = false;
setOpen(false);
if (isEditorReady(editor)) {
editor.commands.setSearchTerms([""]);
}
const nextParams = new URLSearchParams(location.search);
nextParams.delete("q");
nextParams.delete("m");
const nextSearch = nextParams.toString();
navigate(
{
pathname: location.pathname,
search: nextSearch ? `?${nextSearch}` : "",
hash: location.hash,
},
{ replace: true },
);
}, [editor, location.hash, location.pathname, location.search, navigate]);
useEffect(() => {
const handleOpen = (event: Event) => {
const { searchTerms: terms, wholeWord = true } = (
event as SearchNavigationEvent
).detail;
if (!terms?.length || !isEditorReady(editor)) return;
openRef.current = false;
editor.commands.setSearchTerms(terms);
editor.commands.setWholeWord(wholeWord);
editor.commands.resetIndex();
const { results, resultIndex } = editor.storage.searchAndReplace;
openRef.current = true;
if (results.length === 0) {
close();
return;
}
setOpen(true);
setResultState({
resultIndex,
resultsLength: results.length,
});
goToSelection();
};
const handleClose = () => {
if (openRef.current) {
close();
}
};
document.addEventListener("openSearchNavigationDialog", handleOpen);
document.addEventListener("openFindDialogFromEditor", handleClose);
document.addEventListener("closeFindDialogFromEditor", handleClose);
return () => {
document.removeEventListener("openSearchNavigationDialog", handleOpen);
document.removeEventListener("openFindDialogFromEditor", handleClose);
document.removeEventListener("closeFindDialogFromEditor", handleClose);
};
}, [close, editor]);
useEffect(() => {
const handleTransaction = () => {
if (!openRef.current || editor.isDestroyed) return;
const { results } = editor.storage.searchAndReplace;
if (results.length === 0) {
close();
}
};
editor.on("transaction", handleTransaction);
return () => {
editor.off("transaction", handleTransaction);
};
}, [close, editor]);
return (
<Dialog
className={classes.findDialog}
opened={open}
size="xs"
radius="md"
w="auto"
position={{ top: 90, right: 50 }}
withBorder
aria-label="Search navigation"
>
<Flex align="center" gap="xs">
<Text size="xs" style={{ flex: 1 }}>
{resultState.resultsLength > 0
? `${resultState.resultIndex + 1}/${resultState.resultsLength}`
: t("Not found")}
</Text>
<Tooltip label="Previous match">
<ActionIcon
variant="subtle"
color="gray"
onClick={previous}
aria-label="Previous match"
disabled={resultState.resultsLength === 0}
>
<IconArrowNarrowUp size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Next match">
<ActionIcon
variant="subtle"
color="gray"
onClick={next}
aria-label="Next match"
disabled={resultState.resultsLength === 0}
>
<IconArrowNarrowDown size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Close">
<ActionIcon
variant="subtle"
color="gray"
onClick={close}
aria-label="Close"
>
<IconX size={16} />
</ActionIcon>
</Tooltip>
</Flex>
</Dialog>
);
}
export default SearchNavigationDialog;
@@ -0,0 +1,47 @@
import { useEffect, useRef } from "react";
import type { useEditor } from "@tiptap/react";
interface UseSearchNavigationParamsProps {
editor: ReturnType<typeof useEditor>;
isSynced: boolean;
pageId: string;
searchParams: URLSearchParams;
showStatic: boolean;
}
export function useSearchNavigationParams({
editor,
isSynced,
pageId,
searchParams,
showStatic,
}: UseSearchNavigationParamsProps) {
const appliedSearchKeyRef = useRef<string | null>(null);
const searchKey = `${pageId}:${searchParams.toString()}`;
useEffect(() => {
const searchQueries = searchParams.getAll("q");
if (!searchQueries.length) {
appliedSearchKeyRef.current = null;
return;
}
if (
!editor ||
editor.isDestroyed ||
!editor.view.dom.isConnected ||
appliedSearchKeyRef.current === searchKey
) {
return;
}
const match = searchParams.get("m");
appliedSearchKeyRef.current = searchKey;
document.dispatchEvent(
new CustomEvent("openSearchNavigationDialog", {
detail: { searchTerms: searchQueries, wholeWord: match === "whole" },
}),
);
}, [editor, isSynced, searchKey, searchParams, showStatic]);
}
@@ -0,0 +1,112 @@
import { Editor, Node } from "@tiptap/core";
import { GapCursor } from "@tiptap/pm/gapcursor";
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
import { StarterKit } from "@tiptap/starter-kit";
import { describe, expect, it } from "vitest";
import { TiptapDocument } from "./document";
const Footnotes = Node.create({
name: "footnotes",
group: "",
content: "paragraph*",
isolating: true,
renderHTML() {
return ["ol", { class: "footnotes" }, 0];
},
});
const AtomBlock = Node.create({
name: "atomBlock",
group: "block",
atom: true,
renderHTML() {
return ["div", { "data-atom-block": "" }];
},
});
const IsolatingBlock = Node.create({
name: "isolatingBlock",
group: "block",
content: "paragraph+",
isolating: true,
renderHTML() {
return ["div", { "data-isolating-block": "" }, 0];
},
});
function createEditor(content: object[]) {
const element = document.createElement("div");
document.body.appendChild(element);
return new Editor({
element,
extensions: [
TiptapDocument,
StarterKit.configure({ document: false }),
Footnotes,
AtomBlock,
IsolatingBlock,
],
content: { type: "doc", content },
});
}
function pressKey(editor: Editor, key: string, keyCode: number) {
editor.view.dom.dispatchEvent(
new KeyboardEvent("keydown", {
key,
keyCode,
bubbles: true,
cancelable: true,
}),
);
}
describe("TiptapDocument", () => {
it("stops on the gap when arrowing down from a selected block node", () => {
const editor = createEditor([
{ type: "atomBlock" },
{ type: "atomBlock" },
{ type: "paragraph" },
]);
const gapPos = editor.state.doc.child(0).nodeSize;
editor.view.dispatch(
editor.state.tr.setSelection(
NodeSelection.create(editor.state.doc, 0),
),
);
pressKey(editor, "ArrowDown", 40);
expect(editor.state.selection).toBeInstanceOf(GapCursor);
expect(editor.state.selection.head).toBe(gapPos);
editor.destroy();
});
it("stops on the gap when arrowing right out of an isolating block", () => {
const paragraph = (text: string) => ({
type: "paragraph",
content: [{ type: "text", text }],
});
const editor = createEditor([
{ type: "isolatingBlock", content: [paragraph("a")] },
{ type: "isolatingBlock", content: [paragraph("b")] },
{ type: "paragraph" },
]);
const gapPos = editor.state.doc.child(0).nodeSize;
const endOfFirstText = gapPos - 2;
editor.view.dispatch(
editor.state.tr.setSelection(
TextSelection.create(editor.state.doc, endOfFirstText),
),
);
pressKey(editor, "ArrowRight", 39);
expect(editor.state.selection).toBeInstanceOf(GapCursor);
expect(editor.state.selection.head).toBe(gapPos);
editor.destroy();
});
});
@@ -0,0 +1,8 @@
import { Document } from "@tiptap/extension-document";
// With `block+ footnotes?`, ProseMirror's defaultType after the first block is
// `footnotes` (not a textblock), so GapCursor.valid() rejects every top-level gap.
export const TiptapDocument = Document.extend({
content: "block+ footnotes?",
allowGapCursor: true,
});
@@ -1,6 +1,6 @@
import { markInputRule } from "@tiptap/core";
import { StarterKit } from "@tiptap/starter-kit";
import { Document } from "@tiptap/extension-document";
import { TiptapDocument } from "@/features/editor/extensions/document";
import { Code } from "@tiptap/extension-code";
import { TextAlign } from "@tiptap/extension-text-align";
import { TaskList, TaskItem } from "@tiptap/extension-list";
@@ -148,9 +148,7 @@ export const mainExtensions = [
codeBlock: false,
code: false,
}),
Document.extend({
content: "block+ footnotes?",
}),
TiptapDocument,
// Override TipTap's Code extension to fix the inline code input rule.
// The upstream regex /(^|[^`])`([^`]+)`(?!`)$/ captures the character
// before the opening backtick as part of the match, causing markInputRule
@@ -65,11 +65,13 @@ import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu-lazy";
import DrawioMenu from "./components/drawio/drawio-menu";
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx";
import SearchNavigationDialog from "@/features/editor/components/search-and-replace/search-navigation-dialog.tsx";
import { useSearchNavigationParams } from "@/features/editor/components/search-and-replace/use-search-navigation-params.ts";
import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
import { useIdle } from "@/hooks/use-idle.ts";
import { queryClient } from "@/main.tsx";
import { IPage } from "@/features/page/types/page.types.ts";
import { useParams } from "react-router-dom";
import { useParams, useSearchParams } from "react-router-dom";
import { extractPageSlugId, platformModifierKey } from "@/lib";
import { FIVE_MINUTES } from "@/lib/constants.ts";
import { PageEditMode } from "@/features/user/types/user.types.ts";
@@ -199,6 +201,7 @@ function CollabPageEditor({
const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false });
const documentState = useDocumentVisibility();
const { pageSlug } = useParams();
const [searchParams] = useSearchParams();
const slugId = extractPageSlugId(pageSlug);
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
const canScroll = useCallback(
@@ -437,6 +440,14 @@ function CollabPageEditor({
const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true);
useSearchNavigationParams({
editor,
isSynced,
pageId,
searchParams,
showStatic,
});
useEffect(() => {
if (
!hasConnectedOnceRef.current &&
@@ -460,6 +471,7 @@ function CollabPageEditor({
{editor && (
<SearchAndReplaceDialog editor={editor} editable={editable} />
)}
{editor && <SearchNavigationDialog editor={editor} />}
{editor && editorIsEditable && (
<div>
+35 -1
View File
@@ -25,11 +25,37 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
return `${titleSlug}-${pageSlugId}`;
};
function appendSearchParams(
url: string,
search?: string[],
wholeWord?: boolean,
): string {
if(search?.length === 0){
return url;
}
const params = new URLSearchParams();
search
?.map((term) => term.trim())
.filter(Boolean)
.forEach((term) => params.append("q", term));
if (wholeWord) {
params.set("m", "whole");
}
const queryString = params.toString();
return queryString ? `${url}?${queryString}` : url;
}
export const buildPageUrl = (
spaceName: string,
pageSlugId: string,
pageTitle?: string,
anchorId?: string,
search?: string[],
wholeWord?: boolean,
): string => {
let url: string;
if (spaceName === undefined) {
@@ -37,6 +63,9 @@ export const buildPageUrl = (
} else {
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
}
url = appendSearchParams(url, search, wholeWord);
return anchorId ? `${url}#${anchorId}` : url;
};
@@ -45,14 +74,19 @@ export const buildSharedPageUrl = (opts: {
pageSlugId: string;
pageTitle?: string;
anchorId?: string;
search?: string[];
wholeWord?: boolean;
}): string => {
const { shareId, pageSlugId, pageTitle, anchorId } = opts;
const { shareId, pageSlugId, pageTitle, anchorId, search, wholeWord } = opts;
let url: string;
if (!shareId) {
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} else {
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
}
url = appendSearchParams(url, search, wholeWord);
return anchorId ? `${url}#${anchorId}` : url;
};
@@ -33,6 +33,10 @@ import {
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
import { treeModel } from "@/features/page/tree/model/tree-model";
import {
spaceRoots,
updateSpaceRoots,
} from "@/features/page/tree/utils/utils.ts";
import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts";
import type { SpaceTreeNode } from "@/features/page/tree/types.ts";
import classes from "@/features/page/tree/styles/tree.module.css";
@@ -77,7 +81,10 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
const duplicatedPage = await duplicatePage({ pageId: node.id });
// figure out parent + insertion index
const siblings = treeModel.siblingsOf(data, node.id);
const siblings = treeModel.siblingsOf(
spaceRoots(data, node.spaceId),
node.id,
);
const parentId = siblings?.parentId ?? null;
const currentIndex = siblings?.index ?? 0;
const newIndex = currentIndex + 1;
@@ -96,7 +103,9 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
};
setData((prev) =>
treeModel.insert(prev, parentId, treeNodeData, newIndex),
updateSpaceRoots(prev, node.spaceId, (roots) =>
treeModel.insert(roots, parentId, treeNodeData, newIndex),
),
);
setTimeout(() => {
@@ -16,6 +16,8 @@ import {
buildTree,
buildTreeWithChildren,
mergeRootTrees,
spaceRoots,
updateSpaceRoots,
} from "@/features/page/tree/utils/utils.ts";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { getPageTitle } from "@/features/page/page.utils";
@@ -66,18 +68,14 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
const allItems = pagesData.pages.flatMap((page) => page.items);
const treeData = buildTree(allItems);
setData((prev) => {
// Keep nodes belonging to other spaces — filteredData filters by spaceId
// for rendering, so accumulating is safe. Preserves lazy-loaded children
// and open-state when the user returns to a previously-visited space.
const otherSpaces = prev.filter((n) => n?.spaceId !== spaceId);
const currentSpace = prev.filter((n) => n?.spaceId === spaceId);
const refreshed =
currentSpace.length > 0
? mergeRootTrees(currentSpace, treeData)
: treeData;
return [...otherSpaces, ...refreshed];
});
// Keep nodes belonging to other spaces — filteredData filters by spaceId
// for rendering, so accumulating is safe. Preserves lazy-loaded children
// and open-state when the user returns to a previously-visited space.
setData((prev) =>
updateSpaceRoots(prev, spaceId, (roots) =>
roots.length > 0 ? mergeRootTrees(roots, treeData) : treeData,
),
);
setIsDataLoaded(true);
}, [pagesData, hasNextPage, spaceId]);
@@ -183,7 +181,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
);
const filteredData = useMemo(
() => data.filter((node) => node?.spaceId === spaceId),
() => spaceRoots(data, spaceId),
[data, spaceId],
);
@@ -7,6 +7,10 @@ import { useNavigate, useParams } from "react-router-dom";
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
import { treeModel } from "@/features/page/tree/model/tree-model";
import type { DropOp } from "@/features/page/tree/model/tree-model.types";
import {
spaceRoots,
updateSpaceRoots,
} from "@/features/page/tree/utils/utils.ts";
import { dropOpToMovePayload } from "./drop-op-to-move-payload";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { IPage } from "@/features/page/types/page.types.ts";
@@ -45,7 +49,7 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
const handleMove = useCallback(
async (sourceId: string, op: DropOp) => {
const before = store.get(treeDataAtom);
const before = spaceRoots(store.get(treeDataAtom), spaceId);
const { tree: after, result } = treeModel.move(before, sourceId, op);
if (after === before) return;
@@ -80,12 +84,12 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
} as Partial<SpaceTreeNode>);
}
setData(optimistic);
setData((prev) => updateSpaceRoots(prev, spaceId, () => optimistic));
try {
await movePageMutation.mutateAsync(payload);
} catch {
setData(before);
setData((prev) => updateSpaceRoots(prev, spaceId, () => before));
notifications.show({
message: t("Failed to move page"),
color: "red",
@@ -157,7 +161,7 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
// tree (e.g. lazy-load children on expand) immediately before calling
// handleCreate hit a stale closure and compute lastIndex against the
// pre-load tree, requiring a setTimeout-based wait at the call site.
const current = store.get(treeDataAtom);
const current = spaceRoots(store.get(treeDataAtom), spaceId);
let lastIndex: number;
if (parentId === null) {
lastIndex = current.length;
@@ -166,7 +170,11 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
lastIndex = parent?.children?.length ?? 0;
}
setData((prev) => treeModel.insert(prev, parentId, newNode, lastIndex));
setData((prev) =>
updateSpaceRoots(prev, spaceId, (roots) =>
treeModel.insert(roots, parentId, newNode, lastIndex),
),
);
setTimeout(() => {
emit({
@@ -220,3 +220,21 @@ export function mergeRootTrees(
return sortPositionKeys(merged);
}
export function spaceRoots(
tree: SpaceTreeNode[],
spaceId: string,
): SpaceTreeNode[] {
return tree.filter((node) => node?.spaceId === spaceId);
}
export function updateSpaceRoots(
tree: SpaceTreeNode[],
spaceId: string,
update: (roots: SpaceTreeNode[]) => SpaceTreeNode[],
): SpaceTreeNode[] {
const roots = spaceRoots(tree, spaceId);
const next = update(roots);
if (next === roots) return tree;
return [...tree.filter((node) => node?.spaceId !== spaceId), ...next];
}
@@ -25,6 +25,8 @@ import { SearchMobileControl } from "@/features/search/components/search-control
import styles from "./docs.module.css";
const MemoizedDocsSidebarTree = React.memo(DocsSidebarTree);
const MANTINE_COLOR_SCHEME_ATTRIBUTE = "data-mantine-color-scheme";
const DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE = "data-docs-print-color-scheme";
type DocsShellProps = {
surface: DocsSurface;
@@ -47,6 +49,45 @@ export default function DocsShell({
);
const [mobileTocOpen, setMobileTocOpen] = useAtom(docsMobileTocAtom);
React.useEffect(() => {
const root = document.documentElement;
let previousColorScheme: string | null = null;
let isPrinting = false;
const restoreColorScheme = () => {
if (!isPrinting) return;
if (previousColorScheme === null) {
root.removeAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE);
} else {
root.setAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE, previousColorScheme);
}
root.removeAttribute(DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE);
isPrinting = false;
};
const useLightPrintTheme = () => {
if (isPrinting) return;
previousColorScheme = root.getAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE);
root.setAttribute(
DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE,
previousColorScheme ?? "light",
);
root.setAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE, "light");
isPrinting = true;
};
window.addEventListener("beforeprint", useLightPrintTheme);
window.addEventListener("afterprint", restoreColorScheme);
return () => {
window.removeEventListener("beforeprint", useLightPrintTheme);
window.removeEventListener("afterprint", restoreColorScheme);
restoreColorScheme();
};
}, []);
return (
<DocsSurfaceProvider value={surface}>
<div className={clsx(styles.root, "public-typography")}>
@@ -15,8 +15,6 @@
--docs-accent: #2b7af1;
--docs-accent-soft: color-mix(in srgb, var(--docs-accent) 10%, transparent);
/* Cloudflare-style single-ink model: one foreground for headings, bold, and
* body on a just-off-white page; neither end of the scale is pure. */
--docs-bg: oklch(99% 0 0);
--docs-fg: oklch(21% 0 0);
--docs-content-fg: var(--docs-fg);
@@ -410,7 +408,7 @@
}
}
/* Expanded parents read as section headers, Cloudflare-style. */
/* Expanded parents read as section headers. */
.treeRow[data-open-parent="true"] {
color: var(--docs-fg);
font-weight: 500;
@@ -543,8 +541,6 @@
}
}
/* ---------- Sidebar branding experiments (GitBook card / ReadMe line) ---------- */
/* ---------- Footer branding ---------- */
.footer {
@@ -773,13 +769,11 @@
color: inherit;
}
/* Modest semibold heading scale (Cloudflare-style); class doubled to outrank
* the shared editor and .public-typography rules. */
.root.root :global(.ProseMirror) h1 {
font-size: 2.1875rem;
font-size: 1.75rem;
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1.25;
letter-spacing: -0.02em;
line-height: 1.3;
}
.root.root :global(.ProseMirror) h2 {
@@ -900,3 +894,59 @@
display: flex;
flex-direction: column;
}
/* ---------- Print ---------- */
@page public-doc {
background-color: #fff;
}
/* Only the article prints; the body grid collapses so it takes the page width. */
@media print {
:global(html):has(.root),
:global(body):has(.root) {
page: public-doc;
background-color: #fff !important;
}
:global(html[data-docs-print-color-scheme="dark"])
.root.root
:global(.codeBlock svg) {
filter: invert(1) hue-rotate(180deg);
}
.header,
.sidebar,
.toc,
.articleActions,
.breadcrumbs,
.pageNav,
.footer {
display: none !important;
}
.root {
--docs-bg: #fff;
--docs-fg: #1f1f1f;
--docs-content-fg: var(--docs-fg);
--docs-nav-fg: #495057;
--docs-muted: #5f6368;
--docs-hover: #f1f3f5;
--docs-faint: #868e96;
--docs-border: #dee2e6;
--docs-header-bg: #fff;
min-height: 0;
background-color: #fff;
color: var(--docs-fg);
}
.body {
display: block;
}
.article {
max-width: none;
padding: 0;
}
}
@@ -117,6 +117,9 @@ export function SearchResultItem({
pageResult.space.slug,
pageResult.slugId,
pageResult.title,
undefined,
pageResult.matchedText,
pageResult.wholeWord
)}
style={{ userSelect: "none" }}
>
@@ -14,6 +14,8 @@ export interface IPageSearch {
updatedAt: Date;
rank: string;
highlight: string;
matchedText: string[];
wholeWord: boolean;
space: Partial<ISpace>;
}
@@ -6,6 +6,7 @@ import { WebSocketEvent } from "@/features/websocket/types";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { useQueryClient } from "@tanstack/react-query";
import { treeModel } from "@/features/page/tree/model/tree-model";
import { updateSpaceRoots } from "@/features/page/tree/utils/utils.ts";
import localEmitter from "@/lib/local-emitter.ts";
export const useTreeSocket = () => {
@@ -61,65 +62,69 @@ export const useTreeSocket = () => {
setTreeData((prev) => {
if (treeModel.find(prev, event.payload.data.id)) return prev;
const newParentId = event.payload.parentId as string | null;
let next = treeModel.insert(
prev,
newParentId,
event.payload.data,
event.payload.index,
);
// Mirror the emitter: flip new parent's hasChildren to true so
// the chevron renders on the receiver.
if (newParentId) {
next = treeModel.update(next, newParentId, {
hasChildren: true,
} as Partial<SpaceTreeNode>);
}
return next;
return updateSpaceRoots(prev, event.spaceId, (roots) => {
let next = treeModel.insert(
roots,
newParentId,
event.payload.data,
event.payload.index,
);
// Mirror the emitter: flip new parent's hasChildren to true so
// the chevron renders on the receiver.
if (newParentId) {
next = treeModel.update(next, newParentId, {
hasChildren: true,
} as Partial<SpaceTreeNode>);
}
return next;
});
});
break;
case "moveTreeNode":
setTreeData((prev) => {
const sourceBefore = treeModel.find(prev, event.payload.id);
if (!sourceBefore) return prev;
const oldParentId =
(sourceBefore as SpaceTreeNode).parentPageId ?? null;
const newParentId = event.payload.parentId as string | null;
setTreeData((prev) =>
updateSpaceRoots(prev, event.spaceId, (roots) => {
const sourceBefore = treeModel.find(roots, event.payload.id);
if (!sourceBefore) return roots;
const oldParentId =
(sourceBefore as SpaceTreeNode).parentPageId ?? null;
const newParentId = event.payload.parentId as string | null;
const placed = treeModel.place(prev, event.payload.id, {
parentId: newParentId,
index: event.payload.index,
});
// `place` silently returns the same reference if the destination
// parent isn't loaded on this client. Falling back to removing the
// source keeps the UI consistent (the source will reappear when
// the user expands the new parent and lazy-load fetches it).
if (placed === prev) {
return treeModel.remove(prev, event.payload.id);
}
const placed = treeModel.place(roots, event.payload.id, {
parentId: newParentId,
index: event.payload.index,
});
// `place` silently returns the same reference if the destination
// parent isn't loaded on this client. Falling back to removing the
// source keeps the UI consistent (the source will reappear when
// the user expands the new parent and lazy-load fetches it).
if (placed === roots) {
return treeModel.remove(roots, event.payload.id);
}
let next = treeModel.update(placed, event.payload.id, {
position: event.payload.position,
parentPageId: newParentId,
} as Partial<SpaceTreeNode>);
let next = treeModel.update(placed, event.payload.id, {
position: event.payload.position,
parentPageId: newParentId,
} as Partial<SpaceTreeNode>);
// Mirror the emitter's hasChildren bookkeeping so both clients
// converge to the same chevron state.
if (oldParentId) {
const oldParent = treeModel.find(next, oldParentId);
if (!oldParent?.children?.length) {
next = treeModel.update(next, oldParentId, {
hasChildren: false,
// Mirror the emitter's hasChildren bookkeeping so both clients
// converge to the same chevron state.
if (oldParentId) {
const oldParent = treeModel.find(next, oldParentId);
if (!oldParent?.children?.length) {
next = treeModel.update(next, oldParentId, {
hasChildren: false,
} as Partial<SpaceTreeNode>);
}
}
if (newParentId) {
next = treeModel.update(next, newParentId, {
hasChildren: true,
} as Partial<SpaceTreeNode>);
}
}
if (newParentId) {
next = treeModel.update(next, newParentId, {
hasChildren: true,
} as Partial<SpaceTreeNode>);
}
return next;
});
return next;
}),
);
break;
case "deleteTreeNode":
setTreeData((prev) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.95.0",
"version": "0.96.0",
"description": "",
"author": "",
"private": true,
@@ -1,5 +1,6 @@
import {
BadRequestException,
ConflictException,
Injectable,
Logger,
NotFoundException,
@@ -20,7 +21,7 @@ import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
import { MovePageDto } from '../dto/move-page.dto';
import { generateSlugId } from '../../../common/helpers';
import { getPageTitle } from '../../../common/helpers';
import { executeTx } from '@docmost/db/utils';
import { dbOrTx, executeTx } from '@docmost/db/utils';
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
import { v7 as uuid7 } from 'uuid';
import {
@@ -174,10 +175,14 @@ export class PageService {
return page;
}
async nextPagePosition(spaceId: string, parentPageId?: string) {
async nextPagePosition(
spaceId: string,
parentPageId?: string,
trx?: KyselyTransaction,
) {
let pagePosition: string;
const lastPageQuery = this.db
const lastPageQuery = dbOrTx(this.db, trx)
.selectFrom('pages')
.select(['position'])
.where('spaceId', '=', spaceId)
@@ -391,35 +396,46 @@ export class PageService {
}
async movePageToSpace(rootPage: Page, spaceId: string, userId: string) {
let childPageIds: string[] = [];
return executeTx(this.db, async (trx) => {
await this.pageRepo.lockPageHierarchySpaces(
[rootPage.spaceId, spaceId],
trx,
);
const allPages = await this.pageRepo.getPageAndDescendants(rootPage.id, {
includeContent: false,
});
const currentRootPage = await this.pageRepo.findById(rootPage.id, {
trx,
});
if (!currentRootPage || currentRootPage.deletedAt) {
throw new NotFoundException('Page to move not found');
}
if (currentRootPage.spaceId !== rootPage.spaceId) {
throw new ConflictException('Page location changed; retry the move');
}
// Filter to only accessible pages while maintaining tree integrity
const accessiblePages = await this.filterAccessibleTreePages(
allPages,
rootPage.id,
userId,
rootPage.spaceId,
);
const accessibleIds = new Set(accessiblePages.map((p) => p.id));
const allPages = await this.pageRepo.getPageAndDescendants(
currentRootPage.id,
{ includeContent: false, trx },
);
const accessiblePages = await this.filterAccessibleTreePages(
allPages,
currentRootPage.id,
userId,
currentRootPage.spaceId,
);
const accessibleIds = new Set(accessiblePages.map((p) => p.id));
const pagesToOrphan = allPages.filter(
(p) =>
!accessibleIds.has(p.id) &&
p.parentPageId &&
accessibleIds.has(p.parentPageId),
);
// Find inaccessible pages whose parent is being moved - these need to be orphaned
const pagesToOrphan = allPages.filter(
(p) =>
!accessibleIds.has(p.id) &&
p.parentPageId &&
accessibleIds.has(p.parentPageId),
);
await executeTx(this.db, async (trx) => {
// Orphan inaccessible child pages (make them root pages in original space)
for (const page of pagesToOrphan) {
const orphanPosition = await this.nextPagePosition(
rootPage.spaceId,
currentRootPage.spaceId,
null,
trx,
);
await this.pageRepo.updatePage(
{ parentPageId: null, position: orphanPosition },
@@ -429,16 +445,18 @@ export class PageService {
}
// Update root page
const nextPosition = await this.nextPagePosition(spaceId);
const nextPosition = await this.nextPagePosition(spaceId, null, trx);
await this.pageRepo.updatePage(
{ spaceId, parentPageId: null, position: nextPosition },
rootPage.id,
currentRootPage.id,
trx,
);
const pageIdsToMove = accessiblePages.map((p) => p.id);
childPageIds = pageIdsToMove.filter((id) => id !== rootPage.id);
const childPageIds = pageIdsToMove.filter(
(id) => id !== currentRootPage.id,
);
if (pageIdsToMove.length > 1) {
// Update sub pages (all accessible pages except root)
@@ -501,7 +519,7 @@ export class PageService {
{
pageIds: pageIdsToMove,
spaceId,
workspaceId: rootPage.workspaceId,
workspaceId: currentRootPage.workspaceId,
},
{
attempts: 2,
@@ -512,9 +530,9 @@ export class PageService {
},
);
}
});
return { childPageIds };
return { childPageIds };
});
}
async duplicatePage(
@@ -825,31 +843,59 @@ export class PageService {
throw new BadRequestException('A page cannot be its own parent');
}
let parentPageId = null;
if (movedPage.parentPageId === dto.parentPageId) {
parentPageId = undefined;
} else {
// changing the page's parent
if (dto.parentPageId) {
const parentPage = await this.pageRepo.findById(dto.parentPageId);
if (
!parentPage ||
parentPage.deletedAt ||
parentPage.spaceId !== movedPage.spaceId
) {
throw new NotFoundException('Parent page not found');
}
parentPageId = parentPage.id;
}
}
await executeTx(this.db, async (trx) => {
await this.pageRepo.lockPageHierarchySpaces(
[movedPage.spaceId],
trx,
);
await this.pageRepo.updatePage(
{
position: dto.position,
parentPageId: parentPageId,
},
dto.pageId,
);
const currentPage = await this.pageRepo.findById(dto.pageId, { trx });
if (!currentPage || currentPage.deletedAt) {
throw new NotFoundException('Moved page not found');
}
if (currentPage.spaceId !== movedPage.spaceId) {
throw new ConflictException('Page location changed; retry the move');
}
let parentPageId = null;
if (currentPage.parentPageId === dto.parentPageId) {
parentPageId = undefined;
} else {
if (dto.parentPageId) {
const parentPage = await this.pageRepo.findById(dto.parentPageId, {
trx,
});
if (
!parentPage ||
parentPage.deletedAt ||
parentPage.spaceId !== currentPage.spaceId
) {
throw new NotFoundException('Parent page not found');
}
if (
await this.pageRepo.isPageDescendant(
dto.pageId,
parentPage.id,
trx,
)
) {
throw new BadRequestException(
'A page cannot be moved under its descendant',
);
}
parentPageId = parentPage.id;
}
}
await this.pageRepo.updatePage(
{
position: dto.position,
parentPageId: parentPageId,
},
dto.pageId,
trx,
);
});
}
async getPageBreadCrumbs(childPageId: string) {
@@ -8,6 +8,8 @@ export class SearchResponseDto {
creatorId: string;
rank: number;
highlight: string;
matchedText: string[];
wholeWord: boolean;
createdAt: Date;
updatedAt: Date;
space: Partial<Space>;
+18 -4
View File
@@ -191,11 +191,25 @@ export class SearchService {
//@ts-ignore
const searchResults = results.map((result: SearchResponseDto) => {
if (result.highlight) {
result.highlight = result.highlight
.replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' ');
result.wholeWord = true
if (!result.highlight) {
result.matchedText = [];
return result;
}
result.highlight = result.highlight
.replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' ');
result.matchedText = [
...new Set(
Array.from(
result.highlight.matchAll(/<b>([^<]*)<\/b>/gi),
(match) => match[1],
),
),
];
return result;
});
@@ -60,16 +60,21 @@ export class ShareSeoController {
const pageId = this.extractPageSlugId(pageSlug);
const share = await this.shareService.getShareForPage(
pageId,
workspace.id,
);
if (!share) {
let title: string;
let searchIndexing = false;
try {
const shared = await this.shareService.getSharedPage(
{ pageId },
workspace.id,
{ includeContent: false },
);
title = shared.page.title;
searchIndexing = shared.share.searchIndexing;
} catch (err) {
return this.sendIndex(indexFilePath, res);
}
const rawTitle = htmlEscape(share?.sharedPage.title ?? 'untitled');
const rawTitle = htmlEscape(title ?? 'untitled');
const metaTitle =
rawTitle.length > 80 ? `${rawTitle.slice(0, 77)}` : rawTitle;
@@ -78,7 +83,7 @@ export class ShareSeoController {
const metaTags = [
`<meta property="og:title" content="${metaTitle}" />`,
`<meta property="twitter:title" content="${metaTitle}" />`,
!share.searchIndexing ? `<meta name="robots" content="noindex" />` : '',
!searchIndexing ? `<meta name="robots" content="noindex" />` : '',
]
.filter(Boolean)
.join('\n ');
+15 -6
View File
@@ -110,7 +110,11 @@ export class ShareService {
}
}
async getSharedPage(dto: ShareInfoDto, workspaceId: string) {
async getSharedPage(
dto: ShareInfoDto,
workspaceId: string,
opts?: { includeContent?: boolean },
) {
//TODO: we should resolve the page from the share id
if (!dto.pageId) throw new NotFoundException('Shared page not found');
@@ -120,10 +124,13 @@ export class ShareService {
throw new NotFoundException('Shared page not found');
}
const page = await this.pageRepo.findById(dto.pageId, {
includeContent: true,
includeCreator: true,
});
const includeContent = opts?.includeContent !== false;
const page = includeContent
? await this.pageRepo.findById(dto.pageId, {
includeContent: true,
includeCreator: true,
})
: await this.pageRepo.findById(dto.pageId);
if (!page || page.deletedAt) {
throw new NotFoundException('Shared page not found');
@@ -137,7 +144,9 @@ export class ShareService {
throw new NotFoundException('Shared page not found');
}
page.content = await this.updatePublicAttachments(page);
if (includeContent) {
page.content = await this.updatePublicAttachments(page);
}
return { page, share };
}
@@ -10,7 +10,7 @@ import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
import { AddSpaceMembersDto } from '../dto/add-space-members.dto';
import { InjectKysely } from 'nestjs-kysely';
import { Space, SpaceMember, User } from '@docmost/db/types/entity.types';
import { Space, User } from '@docmost/db/types/entity.types';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
import { RemoveSpaceMemberDto } from '../dto/remove-space-member.dto';
import { UpdateSpaceMemberRoleDto } from '../dto/update-space-member-role.dto';
@@ -218,41 +218,18 @@ export class SpaceMemberService {
dto: RemoveSpaceMemberDto,
workspaceId: string,
): Promise<void> {
const space = await this.spaceRepo.findById(dto.spaceId, workspaceId);
if (!space) {
throw new NotFoundException('Space not found');
}
const memberTypeId = dto.userId
? { userId: dto.userId }
: dto.groupId
? { groupId: dto.groupId }
: null;
let spaceMember: SpaceMember = null;
if (dto.userId) {
spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
{
userId: dto.userId,
},
);
} else if (dto.groupId) {
spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
{
groupId: dto.groupId,
},
);
} else {
if (!memberTypeId) {
throw new BadRequestException(
'Please provide a valid userId or groupId to remove',
);
}
if (!spaceMember) {
throw new NotFoundException('Space membership not found');
}
if (spaceMember.role === SpaceRole.ADMIN) {
await this.validateLastAdmin(dto.spaceId);
}
let affectedUserIds: string[] = [];
if (dto.userId) {
affectedUserIds = [dto.userId];
@@ -262,7 +239,29 @@ export class SpaceMemberService {
);
}
await executeTx(this.db, async (trx) => {
const { space, spaceMember } = await executeTx(this.db, async (trx) => {
const space = await this.spaceRepo.findById(
dto.spaceId,
workspaceId,
{ withLock: true, trx },
);
if (!space) {
throw new NotFoundException('Space not found');
}
const spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
memberTypeId,
trx,
);
if (!spaceMember) {
throw new NotFoundException('Space membership not found');
}
if (spaceMember.role === SpaceRole.ADMIN) {
await this.validateLastAdmin(dto.spaceId, trx);
}
await this.spaceMemberRepo.removeSpaceMemberById(
spaceMember.id,
dto.spaceId,
@@ -280,6 +279,8 @@ export class SpaceMemberService {
dto.spaceId,
{ trx },
);
return { space, spaceMember };
});
this.auditService.log({
@@ -304,48 +305,40 @@ export class SpaceMemberService {
dto: UpdateSpaceMemberRoleDto,
workspaceId: string,
): Promise<void> {
const space = await this.spaceRepo.findById(dto.spaceId, workspaceId);
if (!space) {
throw new NotFoundException('Space not found');
}
const memberTypeId = dto.userId
? { userId: dto.userId }
: dto.groupId
? { groupId: dto.groupId }
: null;
let spaceMember: SpaceMember = null;
if (dto.userId) {
spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
{
userId: dto.userId,
},
);
} else if (dto.groupId) {
spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
{
groupId: dto.groupId,
},
);
} else {
if (!memberTypeId) {
throw new BadRequestException(
'Please provide a valid userId or groupId to remove',
);
}
if (!spaceMember) {
throw new NotFoundException('Space membership not found');
}
const result = await executeTx(this.db, async (trx) => {
const space = await this.spaceRepo.findById(
dto.spaceId,
workspaceId,
{ withLock: true, trx },
);
if (!space) {
throw new NotFoundException('Space not found');
}
if (spaceMember.role === dto.role) {
return;
}
const spaceMember = await this.spaceMemberRepo.getSpaceMemberByTypeId(
dto.spaceId,
memberTypeId,
trx,
);
if (!spaceMember) {
throw new NotFoundException('Space membership not found');
}
await executeTx(this.db, async (trx) => {
await trx
.selectFrom('spaces')
.select('id')
.where('id', '=', dto.spaceId)
.forUpdate()
.executeTakeFirst();
if (spaceMember.role === dto.role) {
return { changed: false, space, spaceMember };
}
if (spaceMember.role === SpaceRole.ADMIN) {
await this.validateLastAdmin(dto.spaceId, trx);
@@ -357,8 +350,16 @@ export class SpaceMemberService {
dto.spaceId,
trx,
);
return { changed: true, space, spaceMember };
});
if (!result.changed) {
return;
}
const { space, spaceMember } = result;
this.auditService.log({
event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED,
resourceType: AuditResource.SPACE_MEMBER,
@@ -387,7 +388,7 @@ export class SpaceMemberService {
spaceId,
trx,
);
if (spaceOwnerCount === 1) {
if (spaceOwnerCount <= 1) {
throw new BadRequestException(
'There must be at least one space admin with full access',
);
@@ -747,44 +747,61 @@ export class WorkspaceService {
userRoleDto: UpdateWorkspaceUserRoleDto,
workspaceId: string,
) {
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
const newRole = userRoleDto.role.toLowerCase();
const result = await executeTx(this.db, async (trx) => {
const workspace = await this.workspaceRepo.findById(workspaceId, {
withLock: true,
trx,
});
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
if (!user) {
throw new BadRequestException('Workspace member not found');
}
// prevent ADMIN from managing OWNER role
if (
isAdminActingOnOwner(authUser.role, newRole) ||
isAdminActingOnOwner(authUser.role, user.role)
) {
throw new ForbiddenException();
}
if (user.role === newRole) {
return user;
}
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
);
if (user.role === UserRole.OWNER && workspaceOwnerCount === 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
const user = await this.userRepo.findById(
userRoleDto.userId,
workspaceId,
{ trx },
);
if (!user) {
throw new BadRequestException('Workspace member not found');
}
if (
isAdminActingOnOwner(authUser.role, newRole) ||
isAdminActingOnOwner(authUser.role, user.role)
) {
throw new ForbiddenException();
}
if (user.role === newRole) {
return { changed: false, user };
}
if (
user.role === UserRole.OWNER &&
!user.deletedAt &&
!user.deactivatedAt
) {
await this.validateLastWorkspaceOwner(workspaceId, trx);
}
await this.userRepo.updateUser(
{
role: newRole,
},
user.id,
workspaceId,
trx,
);
return { changed: true, user };
});
if (!result.changed) {
return result.user;
}
await this.userRepo.updateUser(
{
role: newRole,
},
user.id,
workspaceId,
);
const { user } = result;
this.auditService.log({
event: AuditEvent.USER_ROLE_CHANGED,
@@ -848,40 +865,38 @@ export class WorkspaceService {
userId: string,
workspaceId: string,
): Promise<void> {
const user = await this.userRepo.findById(userId, workspaceId);
const user = await executeTx(this.db, async (trx) => {
const workspace = await this.workspaceRepo.findById(workspaceId, {
withLock: true,
trx,
});
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
const user = await this.userRepo.findById(userId, workspaceId, { trx });
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
if (user.deactivatedAt) {
throw new BadRequestException('User is already deactivated');
}
if (user.deactivatedAt) {
throw new BadRequestException('User is already deactivated');
}
if (authUser.id === userId) {
throw new BadRequestException('You cannot deactivate yourself');
}
if (authUser.id === userId) {
throw new BadRequestException('You cannot deactivate yourself');
}
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException(
'You cannot deactivate a user with owner role',
);
}
if (user.role === UserRole.OWNER) {
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
);
if (workspaceOwnerCount === 1) {
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException(
'There must be at least one workspace owner',
'You cannot deactivate a user with owner role',
);
}
}
await executeTx(this.db, async (trx) => {
if (user.role === UserRole.OWNER) {
await this.validateLastWorkspaceOwner(workspaceId, trx);
}
await this.userRepo.updateUser(
{ deactivatedAt: new Date() },
userId,
@@ -889,6 +904,8 @@ export class WorkspaceService {
trx,
);
await this.userSessionRepo.revokeByUserId(userId, workspaceId, trx);
return user;
});
this.auditService.log({
@@ -951,32 +968,34 @@ export class WorkspaceService {
userId: string,
workspaceId: string,
): Promise<void> {
const user = await this.userRepo.findById(userId, workspaceId);
const user = await executeTx(this.db, async (trx) => {
const workspace = await this.workspaceRepo.findById(workspaceId, {
withLock: true,
trx,
});
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
const user = await this.userRepo.findById(userId, workspaceId, { trx });
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
);
if (authUser.id === userId) {
throw new BadRequestException('You cannot delete yourself');
}
if (user.role === UserRole.OWNER && workspaceOwnerCount === 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
}
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException(
'You cannot delete a user with owner role',
);
}
if (authUser.id === userId) {
throw new BadRequestException('You cannot delete yourself');
}
if (user.role === UserRole.OWNER && !user.deactivatedAt) {
await this.validateLastWorkspaceOwner(workspaceId, trx);
}
if (isAdminActingOnOwner(authUser.role, user.role)) {
throw new BadRequestException('You cannot delete a user with owner role');
}
await executeTx(this.db, async (trx) => {
await this.userRepo.updateUser(
{
name: 'Deleted user',
@@ -1009,6 +1028,8 @@ export class WorkspaceService {
});
await this.userSessionRepo.revokeByUserId(userId, workspaceId, trx);
return user;
});
this.auditService.log({
@@ -1030,4 +1051,20 @@ export class WorkspaceService {
// empty
}
}
private async validateLastWorkspaceOwner(
workspaceId: string,
trx: KyselyTransaction,
): Promise<void> {
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
trx,
);
if (workspaceOwnerCount <= 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
}
}
}
@@ -161,6 +161,22 @@ export class PageRepo {
return result;
}
async lockPageHierarchySpaces(
spaceIds: string[],
trx: KyselyTransaction,
): Promise<void> {
const sortedSpaceIds = [...new Set(spaceIds)].sort();
for (const spaceId of sortedSpaceIds) {
await sql`
SELECT pg_advisory_xact_lock(
hashtext('page-hierarchy'),
hashtext(${spaceId})
)
`.execute(trx);
}
}
async insertPage(
insertablePage: InsertablePage,
trx?: KyselyTransaction,
@@ -489,9 +505,9 @@ export class PageRepo {
async getPageAndDescendants(
parentPageId: string,
opts: { includeContent: boolean },
opts: { includeContent: boolean; trx?: KyselyTransaction },
) {
return this.db
return dbOrTx(this.db, opts.trx)
.withRecursive('page_hierarchy', (db) =>
db
.selectFrom('pages')
@@ -535,6 +551,36 @@ export class PageRepo {
.execute();
}
async isPageDescendant(
ancestorPageId: string,
descendantPageId: string,
trx?: KyselyTransaction,
): Promise<boolean> {
const result = await dbOrTx(this.db, trx)
.withRecursive('page_ancestors', (db) =>
db
.selectFrom('pages')
.select(['id', 'parentPageId'])
.where('id', '=', descendantPageId)
.union((exp) =>
exp
.selectFrom('pages as parent')
.select(['parent.id', 'parent.parentPageId'])
.innerJoin(
'page_ancestors as ancestor',
'ancestor.parentPageId',
'parent.id',
),
),
)
.selectFrom('page_ancestors')
.select('id')
.where('id', '=', ancestorPageId)
.executeTakeFirst();
return Boolean(result);
}
/**
* Get page and all descendants, excluding restricted pages and their subtrees.
* More efficient than getPageAndDescendants + filtering because:
@@ -25,7 +25,11 @@ export class SpaceRepo {
async findById(
spaceId: string,
workspaceId: string,
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
opts?: {
includeMemberCount?: boolean;
withLock?: boolean;
trx?: KyselyTransaction;
},
): Promise<Space> {
const db = dbOrTx(this.db, opts?.trx);
@@ -41,6 +45,11 @@ export class SpaceRepo {
} else {
query = query.where(sql`LOWER(slug)`, '=', sql`LOWER(${spaceId})`);
}
if (opts?.withLock && opts?.trx) {
query = query.forUpdate();
}
return query.executeTakeFirst();
}
@@ -145,12 +145,16 @@ export class UserRepo {
async roleCountByWorkspaceId(
role: string,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<number> {
const { count } = await this.db
const db = dbOrTx(this.db, trx);
const { count } = await db
.selectFrom('users')
.select((eb) => eb.fn.count('role').as('count'))
.where('role', '=', role)
.where('workspaceId', '=', workspaceId)
.where('deletedAt', 'is', null)
.where('deactivatedAt', 'is', null)
.executeTakeFirst();
return count as number;
+15
View File
@@ -3,6 +3,8 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { Server, Socket } from 'socket.io';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { SpaceRole } from '../common/helpers/types/permission';
import {
TREE_EVENTS,
WS_SPACE_RESTRICTION_CACHE_PREFIX,
@@ -18,6 +20,7 @@ export class WsService {
constructor(
private readonly pagePermissionRepo: PagePermissionRepo,
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly spaceMemberRepo: SpaceMemberRepo,
) {}
setServer(server: Server): void {
@@ -31,6 +34,18 @@ export class WsService {
return;
}
const userSpaceRoles = await this.spaceMemberRepo.getUserSpaceRoles(
client.data.userId,
data.spaceId,
);
const canPublish = userSpaceRoles?.some(
({ role }) => role === SpaceRole.ADMIN || role === SpaceRole.WRITER,
);
if (!canPublish) {
return;
}
if (data.operation === 'refetchRootTreeNodeEvent') {
client.broadcast.to(room).emit('message', data);
return;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docmost",
"homepage": "https://docmost.com",
"version": "0.95.0",
"version": "0.96.0",
"private": true,
"scripts": {
"build": "nx run-many -t build",
@@ -39,7 +39,11 @@ declare module "@tiptap/core" {
/**
* @description Set search term in extension.
*/
setSearchTerm: (searchTerm: string) => ReturnType;
setSearchTerms: (searchTerms: string[]) => ReturnType;
/**
* @description Set whole word search in extension.
*/
setWholeWord: (wholeWord: boolean) => ReturnType;
/**
* @description Set replace term in extension.
*/
@@ -82,13 +86,26 @@ interface TextNodesWithPosition {
}
const getRegex = (
s: string,
searchTerms: string[],
disableRegex: boolean,
caseSensitive: boolean,
wholeWord: boolean,
): RegExp => {
return RegExp(
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s,
caseSensitive ? "gu" : "gui",
const terms = searchTerms.filter(Boolean).sort((a, b) => b.length - a.length);
const pattern = terms
.map((term) =>
disableRegex ? term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : term,
)
.join("|");
const finalPattern = wholeWord
? `(?<![\\p{L}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{N}_])`
: pattern;
return new RegExp(
finalPattern,
caseSensitive ? "gu" : "giu",
);
};
@@ -255,12 +272,14 @@ export interface SearchAndReplaceOptions {
}
export interface SearchAndReplaceStorage {
searchTerm: string;
searchTerms: string[];
replaceTerm: string;
results: Range[];
lastSearchTerm: string;
lastSearchTerms: string[];
caseSensitive: boolean;
lastCaseSensitive: boolean;
wholeWord: boolean;
lastWholeWord: boolean;
resultIndex: number;
lastResultIndex: number;
}
@@ -280,11 +299,13 @@ export const SearchAndReplace = Extension.create<
addStorage() {
return {
searchTerm: "",
searchTerms: [],
replaceTerm: "",
results: [],
lastSearchTerm: "",
lastSearchTerms: [],
caseSensitive: false,
wholeWord: false,
lastWholeWord: false,
lastCaseSensitive: false,
resultIndex: 0,
lastResultIndex: 0,
@@ -293,10 +314,21 @@ export const SearchAndReplace = Extension.create<
addCommands() {
return {
setSearchTerm:
(searchTerm: string) =>
setSearchTerms:
(searchTerms: string[]) =>
({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm;
editor.storage.searchAndReplace.searchTerms = searchTerms.filter(Boolean);
// clear whole word by default
// should remove if whole word toggle is added to search and replace dialog
editor.storage.searchAndReplace.wholeWord = false;
return false;
},
setWholeWord:
(wholeWord: boolean) =>
({ editor }) => {
editor.storage.searchAndReplace.wholeWord = wholeWord;
return false;
},
@@ -409,8 +441,10 @@ export const SearchAndReplace = Extension.create<
const editor = this.editor;
const { searchResultClass, disableRegex } = this.options;
const setLastSearchTerm = (t: string) =>
(editor.storage.searchAndReplace.lastSearchTerm = t);
const setLastSearchTerms = (terms: string[]) =>
(editor.storage.searchAndReplace.lastSearchTerms = [...terms]);
const setLastWholeWord = (t: boolean) =>
(editor.storage.searchAndReplace.lastWholeWord = t);
const setLastCaseSensitive = (t: boolean) =>
(editor.storage.searchAndReplace.lastCaseSensitive = t);
const setLastResultIndex = (t: number) =>
@@ -425,37 +459,47 @@ export const SearchAndReplace = Extension.create<
const storage = editor.storage.searchAndReplace;
if (!storage) return oldState;
const {
searchTerm,
lastSearchTerm,
caseSensitive,
searchTerms,
lastCaseSensitive,
lastSearchTerms,
caseSensitive,
wholeWord,
lastWholeWord,
resultIndex,
lastResultIndex,
} = storage;
if (
!docChanged &&
lastSearchTerm === searchTerm &&
searchTerms.length === lastSearchTerms.length &&
searchTerms.every((term, index) => term === lastSearchTerms[index]) &&
lastCaseSensitive === caseSensitive &&
lastWholeWord === wholeWord &&
lastResultIndex === resultIndex
)
return oldState;
setLastSearchTerm(searchTerm);
setLastSearchTerms(searchTerms);
setLastCaseSensitive(caseSensitive);
setLastWholeWord(wholeWord);
setLastResultIndex(resultIndex);
if (!searchTerm) {
if (searchTerms.length === 0) {
editor.storage.searchAndReplace.results = [];
return DecorationSet.empty;
}
const { decorationsToReturn, results } = processSearches(
doc,
getRegex(searchTerm, disableRegex, caseSensitive),
searchResultClass,
resultIndex,
);
doc,
getRegex(
searchTerms,
disableRegex,
caseSensitive,
wholeWord,
),
searchResultClass,
resultIndex,
);
editor.storage.searchAndReplace.results = results;