mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea27919079 | |||
| 2b6ac81c61 |
+2
-3
@@ -1,6 +1,5 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
dist
|
||||
/data
|
||||
.env*
|
||||
.nx
|
||||
data
|
||||
|
||||
+1
-7
@@ -46,10 +46,4 @@ DRAWIO_URL=
|
||||
DISABLE_TELEMETRY=false
|
||||
|
||||
# Enable debug logging in production (default: false)
|
||||
DEBUG_MODE=false
|
||||
|
||||
# Log database queries
|
||||
DEBUG_DB=false
|
||||
|
||||
# Log http requests
|
||||
LOG_HTTP=false
|
||||
DEBUG_MODE=false
|
||||
+5
-7
@@ -1,22 +1,19 @@
|
||||
FROM node:22-slim AS base
|
||||
FROM node:22-alpine AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
|
||||
|
||||
RUN npm install -g pnpm@10.4.0
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install -g pnpm@10.4.0
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
FROM base AS installer
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl bash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache curl bash
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -32,11 +29,12 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
|
||||
# Copy root package files
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/pnpm*.yaml /app/
|
||||
COPY --from=builder /app/.npmrc /app/.npmrc
|
||||
|
||||
# Copy patches
|
||||
COPY --from=builder /app/patches /app/patches
|
||||
|
||||
RUN npm install -g pnpm@10.4.0
|
||||
|
||||
RUN chown -R node:node /app
|
||||
|
||||
USER node
|
||||
|
||||
+29
-26
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.23.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -10,51 +10,54 @@
|
||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.2",
|
||||
"@casl/react": "^4.0.0",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@excalidraw/excalidraw": "0.18.0-c158187",
|
||||
"@mantine/core": "^8.3.12",
|
||||
"@mantine/dates": "^8.3.12",
|
||||
"@mantine/form": "^8.3.12",
|
||||
"@mantine/hooks": "^8.3.12",
|
||||
"@mantine/modals": "^8.3.12",
|
||||
"@mantine/notifications": "^8.3.12",
|
||||
"@mantine/spotlight": "^8.3.12",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@excalidraw/excalidraw": "0.18.0-864353b",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@mantine/dates": "^8.3.2",
|
||||
"@mantine/form": "^8.1.3",
|
||||
"@mantine/hooks": "^8.1.3",
|
||||
"@mantine/modals": "^8.1.3",
|
||||
"@mantine/notifications": "^8.1.3",
|
||||
"@mantine/spotlight": "^8.1.3",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"@tiptap/extension-character-count": "^2.10.3",
|
||||
"alfaaz": "^1.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlightjs-sap-abap": "^0.3.0",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-http-backend": "^2.7.3",
|
||||
"jotai": "^2.16.2",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jotai": "^2.12.5",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "0.16.27",
|
||||
"katex": "0.16.22",
|
||||
"lowlight": "^3.3.0",
|
||||
"mantine-form-zod-resolver": "^1.3.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"mermaid": "^11.11.0",
|
||||
"mitt": "^3.0.1",
|
||||
"posthog-js": "^1.255.1",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "3.4.0",
|
||||
"react-clear-modal": "^2.0.17",
|
||||
"react-clear-modal": "^2.0.15",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drawio": "^1.0.7",
|
||||
"react-drawio": "^1.0.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^7.12.0",
|
||||
"semver": "^7.7.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"semver": "^7.7.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||
"zod": "^3.25.76"
|
||||
"zod": "^3.25.56"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
@@ -62,10 +65,10 @@
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/node": "22.19.1",
|
||||
"@types/node": "22.10.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
@@ -78,6 +81,6 @@
|
||||
"prettier": "^3.4.1",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^7.2.4"
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
|
||||
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
|
||||
"Confirm": "Bestätigen",
|
||||
"Copy as Markdown": "Als Markdown kopieren",
|
||||
"Copy link": "Link kopieren",
|
||||
"Create": "Erstellen",
|
||||
"Create group": "Gruppe erstellen",
|
||||
@@ -41,9 +40,9 @@
|
||||
"Date": "Datum",
|
||||
"Delete": "Löschen",
|
||||
"Delete group": "Gruppe löschen",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Details",
|
||||
"Details": "Einzelheiten",
|
||||
"e.g ACME": "z.B. ACME",
|
||||
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||
"e.g Developers": "z.B. Entwickler",
|
||||
@@ -66,7 +65,7 @@
|
||||
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
||||
"Enter your password": "Geben Sie Ihr Passwort ein",
|
||||
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
||||
"Error loading page history.": "Fehler beim Laden des Seitenverlaufs.",
|
||||
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.",
|
||||
"Export": "Exportieren",
|
||||
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||
@@ -114,7 +113,7 @@
|
||||
"New page": "Neue Seite",
|
||||
"New password": "Neues Passwort",
|
||||
"No group found": "Keine Gruppe gefunden",
|
||||
"No page history saved yet.": "Es wurde noch kein Seitenverlauf gespeichert.",
|
||||
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.",
|
||||
"No pages yet": "Noch keine Seiten",
|
||||
"No results found...": "Keine Ergebnisse gefunden...",
|
||||
"No user found": "Kein Benutzer gefunden",
|
||||
@@ -122,9 +121,7 @@
|
||||
"Owner": "Besitzer",
|
||||
"page": "Seite",
|
||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||
"Page history": "Seitenverlauf",
|
||||
"Select version": "Version auswählen",
|
||||
"Highlight changes": "Änderungen hervorheben",
|
||||
"Page history": "Seitengeschichte",
|
||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||
"Pages": "Seiten",
|
||||
"pages": "Seiten",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Export fehlgeschlagen:",
|
||||
"export error": "Exportfehler",
|
||||
"Export page": "Seite exportieren",
|
||||
"Export successful": "Export erfolgreich",
|
||||
"Export space": "Bereich exportieren",
|
||||
"Export {{type}}": "Exportiere {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
|
||||
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
|
||||
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||
"Uploading {{name}}": "Lade {{name}} hoch",
|
||||
"Uploading file": "Datei wird hochgeladen",
|
||||
"Table": "Tabelle",
|
||||
"Insert a table.": "Tabelle einfügen.",
|
||||
"Insert collapsible block.": "Einklappbaren Block einfügen.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Freigabe erfolgreich gelöscht",
|
||||
"Share not found": "Freigabe nicht gefunden",
|
||||
"Failed to share page": "Fehler beim Teilen der Seite",
|
||||
"Disable public sharing": "Öffentliches Teilen deaktivieren",
|
||||
"Prevent members from sharing pages publicly.": "Verhindern Sie, dass Mitglieder Seiten öffentlich teilen.",
|
||||
"Toggle public sharing": "Öffentliches Teilen umschalten",
|
||||
"Toggle space public sharing": "Öffentliches Teilen im Bereich umschalten",
|
||||
"Public sharing is disabled at the workspace level": "Öffentliches Teilen ist auf der Arbeitsbereichsebene deaktiviert",
|
||||
"Prevent pages in this space from being shared publicly.": "Verhindern Sie, dass Seiten in diesem Bereich öffentlich geteilt werden.",
|
||||
"Requires an enterprise license": "Erfordert eine Unternehmenslizenz",
|
||||
"Enable public sharing": "Öffentliches Teilen aktivieren",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sind Sie sicher, dass Sie das öffentliche Teilen aktivieren möchten? Mitglieder können Seiten öffentlich teilen.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Arbeitsbereich werden gelöscht.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Sind Sie sicher, dass Sie das öffentliche Teilen für diesen Bereich aktivieren möchten?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Bereich werden gelöscht.",
|
||||
"Public sharing is disabled": "Öffentliches Teilen ist deaktiviert",
|
||||
"Public sharing has been disabled at the workspace level.": "Das öffentliche Teilen wurde auf der Arbeitsbereichsebene deaktiviert.",
|
||||
"Public sharing has been disabled for this space.": "Das öffentliche Teilen wurde für diesen Bereich deaktiviert.",
|
||||
"Copy page": "Seite kopieren",
|
||||
"Copy page to a different space.": "Seite in einen anderen Bereich kopieren.",
|
||||
"Page copied successfully": "Seite erfolgreich kopiert",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "SSO-Anbieter löschen",
|
||||
"Are you sure you want to delete this SSO provider?": "Sind Sie sicher, dass Sie diesen SSO-Anbieter löschen möchten?",
|
||||
"Action": "Aktion",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration",
|
||||
"Icon": "Icon",
|
||||
"Upload image": "Bild hochladen",
|
||||
"Remove image": "Bild entfernen",
|
||||
"Failed to remove image": "Fehler beim Entfernen des Bildes",
|
||||
"Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.",
|
||||
"Image removed successfully": "Bild erfolgreich entfernt",
|
||||
"API key": "API-Schlüssel",
|
||||
"API key created successfully": "API-Schlüssel erfolgreich erstellt",
|
||||
"API keys": "API-Schlüssel",
|
||||
"API management": "API-Verwaltung",
|
||||
"Are you sure you want to revoke this API key": "Sind Sie sicher, dass Sie diesen API-Schlüssel widerrufen möchten?",
|
||||
"Create API Key": "API-Schlüssel erstellen",
|
||||
"Custom expiration date": "Benutzerdefiniertes Ablaufdatum",
|
||||
"Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein",
|
||||
"Expiration": "Ablauf",
|
||||
"Expired": "Abgelaufen",
|
||||
"Expires": "Läuft ab",
|
||||
"I've saved my API key": "Ich habe meinen API-Schlüssel gespeichert",
|
||||
"Last use": "Zuletzt verwendet",
|
||||
"No API keys found": "Keine API-Schlüssel gefunden",
|
||||
"No expiration": "Kein Ablauf",
|
||||
"Revoke API key": "API-Schlüssel widerrufen",
|
||||
"Revoked successfully": "Erfolgreich widerrufen",
|
||||
"Select expiration date": "Ablaufdatum wählen",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.",
|
||||
"Update API key": "API-Schlüssel aktualisieren",
|
||||
"Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich",
|
||||
"AI settings": "KI-Einstellungen",
|
||||
"AI search": "KI-Suche",
|
||||
"AI Answer": "KI-Antwort",
|
||||
"Ask AI": "KI fragen",
|
||||
"AI is thinking...": "Die KI überlegt...",
|
||||
"Ask a question...": "Fragen stellen...",
|
||||
"AI-powered search (Ask AI)": "KI-gestützte Suche (KI fragen)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Die KI-Suche verwendet Vektor-Einbettungen, um semantische Suchfunktionen in Ihrem Arbeitsbereich bereitzustellen.",
|
||||
"Toggle AI search": "KI-Suche umschalten",
|
||||
"Sources": "Quellen",
|
||||
"Ask AI not available for attachments": "KI fragen nicht für Anhänge verfügbar",
|
||||
"No answer available": "Keine Antwort verfügbar",
|
||||
"Background color": "Hintergrundfarbe",
|
||||
"Highlight color": "Hervorhebungsfarbe",
|
||||
"Remove color": "Farbe entfernen"
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||
"Confirm": "Confirm",
|
||||
"Copy as Markdown": "Copy as Markdown",
|
||||
"Copy link": "Copy link",
|
||||
"Create": "Create",
|
||||
"Create group": "Create group",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page deleted successfully",
|
||||
"Page history": "Page history",
|
||||
"Select version": "Select version",
|
||||
"Highlight changes": "Highlight changes",
|
||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Export failed:",
|
||||
"export error": "export error",
|
||||
"Export page": "Export page",
|
||||
"Export successful": "Export successful",
|
||||
"Export space": "Export space",
|
||||
"Export {{type}}": "Export {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Upload any image from your device.",
|
||||
"Upload any video from your device.": "Upload any video from your device.",
|
||||
"Upload any file from your device.": "Upload any file from your device.",
|
||||
"Uploading {{name}}": "Uploading {{name}}",
|
||||
"Uploading file": "Uploading file",
|
||||
"Table": "Table",
|
||||
"Insert a table.": "Insert a table.",
|
||||
"Insert collapsible block.": "Insert collapsible block.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Share deleted successfully",
|
||||
"Share not found": "Share not found",
|
||||
"Failed to share page": "Failed to share page",
|
||||
"Disable public sharing": "Disable public sharing",
|
||||
"Prevent members from sharing pages publicly.": "Prevent members from sharing pages publicly.",
|
||||
"Toggle public sharing": "Toggle public sharing",
|
||||
"Toggle space public sharing": "Toggle space public sharing",
|
||||
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
|
||||
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
|
||||
"Requires an enterprise license": "Requires an enterprise license",
|
||||
"Enable public sharing": "Enable public sharing",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Are you sure you want to enable public sharing? Members will be able to share pages publicly.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Are you sure you want to enable public sharing for this space?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.",
|
||||
"Public sharing is disabled": "Public sharing is disabled",
|
||||
"Public sharing has been disabled at the workspace level.": "Public sharing has been disabled at the workspace level.",
|
||||
"Public sharing has been disabled for this space.": "Public sharing has been disabled for this space.",
|
||||
"Copy page": "Copy page",
|
||||
"Copy page to a different space.": "Copy page to a different space.",
|
||||
"Page copied successfully": "Page copied successfully",
|
||||
@@ -575,20 +554,5 @@
|
||||
"Select expiration date": "Select expiration date",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.",
|
||||
"Update API key": "Update API key",
|
||||
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace",
|
||||
"AI settings": "AI settings",
|
||||
"AI search": "AI search",
|
||||
"AI Answer": "AI Answer",
|
||||
"Ask AI": "Ask AI",
|
||||
"AI is thinking...": "AI is thinking...",
|
||||
"Ask a question...": "Ask a question...",
|
||||
"AI-powered search (Ask AI)": "AI-powered search (Ask AI)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.",
|
||||
"Toggle AI search": "Toggle AI search",
|
||||
"Sources": "Sources",
|
||||
"Ask AI not available for attachments": "Ask AI not available for attachments",
|
||||
"No answer available": "No answer available",
|
||||
"Background color": "Background color",
|
||||
"Highlight color": "Highlight color",
|
||||
"Remove color": "Remove color"
|
||||
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.",
|
||||
"Choose your preferred page width.": "Elige el ancho de página que prefieras.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy as Markdown": "Copiar como Markdown",
|
||||
"Copy link": "Copiar enlace",
|
||||
"Create": "Crear",
|
||||
"Create group": "Crear grupo",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página eliminada con éxito",
|
||||
"Page history": "Historial de la página",
|
||||
"Select version": "Seleccionar versión",
|
||||
"Highlight changes": "Resaltar cambios",
|
||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Exportación fallida:",
|
||||
"export error": "error de exportación",
|
||||
"Export page": "Exportar página",
|
||||
"Export successful": "Exportación exitosa",
|
||||
"Export space": "Exportar espacio",
|
||||
"Export {{type}}": "Exportar {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
|
||||
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
|
||||
"Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.",
|
||||
"Uploading {{name}}": "Subiendo {{name}}",
|
||||
"Uploading file": "Subiendo archivo",
|
||||
"Table": "Tabla",
|
||||
"Insert a table.": "Insertar una tabla.",
|
||||
"Insert collapsible block.": "Insertar bloque desplegable.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Compartición eliminada con éxito",
|
||||
"Share not found": "Compartición no encontrada",
|
||||
"Failed to share page": "Error al compartir la página",
|
||||
"Disable public sharing": "Desactivar el uso compartido público",
|
||||
"Prevent members from sharing pages publicly.": "Evitar que los miembros compartan páginas públicamente.",
|
||||
"Toggle public sharing": "Alternar el uso compartido público",
|
||||
"Toggle space public sharing": "Alternar el uso compartido público del espacio",
|
||||
"Public sharing is disabled at the workspace level": "El uso compartido público está desactivado a nivel de espacio de trabajo",
|
||||
"Prevent pages in this space from being shared publicly.": "Evitar que las páginas en este espacio se compartan públicamente.",
|
||||
"Requires an enterprise license": "Requiere una licencia empresarial",
|
||||
"Enable public sharing": "Activar el uso compartido público",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "¿Está seguro de que desea activar el uso compartido público? Los miembros podrán compartir páginas públicamente.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "¿Está seguro de que desea desactivar el uso compartido público? Todos los enlaces compartidos existentes en este espacio de trabajo se eliminarán.",
|
||||
"Are you sure you want to enable public sharing for this space?": "¿Está seguro de que desea activar el uso compartido público para este espacio?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "¿Está seguro de que desea desactivar el uso compartido público? Todos los enlaces compartidos existentes en este espacio se eliminarán.",
|
||||
"Public sharing is disabled": "El uso compartido público está desactivado",
|
||||
"Public sharing has been disabled at the workspace level.": "El uso compartido público se ha desactivado a nivel de espacio de trabajo.",
|
||||
"Public sharing has been disabled for this space.": "El uso compartido público se ha desactivado para este espacio.",
|
||||
"Copy page": "Copiar página",
|
||||
"Copy page to a different space.": "Copiar página en otro espacio",
|
||||
"Page copied successfully": "Página copiada exitosamente",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Eliminar proveedor de SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "¿Está seguro de que desea eliminar este proveedor de SSO?",
|
||||
"Action": "Acción",
|
||||
"{{ssoProviderType}} configuration": "Configuración de {{ssoProviderType}}",
|
||||
"Icon": "Icono",
|
||||
"Upload image": "Subir imagen",
|
||||
"Remove image": "Eliminar imagen",
|
||||
"Failed to remove image": "No se ha podido eliminar la imagen",
|
||||
"Image exceeds 10MB limit.": "La imagen excede del límite de 10 MB",
|
||||
"Image removed successfully": "Imagen eliminada correctamente",
|
||||
"API key": "Clave API",
|
||||
"API key created successfully": "Clave API creada correctamente",
|
||||
"API keys": "Claves API",
|
||||
"API management": "Gestión de API",
|
||||
"Are you sure you want to revoke this API key": "¿Está seguro de que desea revocar esta clave API? ",
|
||||
"Create API Key": "Crear clave API",
|
||||
"Custom expiration date": "Fecha de vencimiento personalizada",
|
||||
"Enter a descriptive token name": "Introduce un nombre descriptivo del token",
|
||||
"Expiration": "Vencimiento",
|
||||
"Expired": "Vencido",
|
||||
"Expires": "Vence",
|
||||
"I've saved my API key": "He guardado mi clave API",
|
||||
"Last use": "Último uso",
|
||||
"No API keys found": "No se han encontrado claves API",
|
||||
"No expiration": "Sin vencimiento",
|
||||
"Revoke API key": "Revocar clave API",
|
||||
"Revoked successfully": "Revocada correctamente",
|
||||
"Select expiration date": "Seleccionar fecha de vencimiento",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Esta acción no se puede deshacer. Las aplicaciones que utilicen esta clave API dejarán de funcionar.",
|
||||
"Update API key": "Actualizar clave API",
|
||||
"Manage API keys for all users in the workspace": "Gestionar claves API para todos los usuarios en el espacio de trabajo",
|
||||
"AI settings": "Configuración de IA",
|
||||
"AI search": "Búsqueda de IA",
|
||||
"AI Answer": "Respuesta de IA",
|
||||
"Ask AI": "Preguntar a IA",
|
||||
"AI is thinking...": "IA está pensando...",
|
||||
"Ask a question...": "Haz una pregunta...",
|
||||
"AI-powered search (Ask AI)": "Búsqueda impulsada por IA (Preguntar a IA)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La búsqueda de IA utiliza incrustaciones vectoriales para proporcionar capacidades de búsqueda semántica en todo el contenido de su espacio de trabajo.",
|
||||
"Toggle AI search": "Alternar búsqueda de IA",
|
||||
"Sources": "Fuentes",
|
||||
"Ask AI not available for attachments": "Preguntar a IA no está disponible para adjuntos",
|
||||
"No answer available": "No hay respuesta disponible",
|
||||
"Background color": "Color de fondo",
|
||||
"Highlight color": "Color de resaltado",
|
||||
"Remove color": "Eliminar color"
|
||||
"{{ssoProviderType}} configuration": "Configuración de {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.",
|
||||
"Choose your preferred page width.": "Choisissez votre largeur de page préférée.",
|
||||
"Confirm": "Confirmer",
|
||||
"Copy as Markdown": "Copier comme Markdown",
|
||||
"Copy link": "Copier le lien",
|
||||
"Create": "Créer",
|
||||
"Create group": "Créer groupe",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page supprimée avec succès",
|
||||
"Page history": "Historique de la page",
|
||||
"Select version": "Sélectionner la version",
|
||||
"Highlight changes": "Mettre en évidence les changements",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Échec de l'exportation :",
|
||||
"export error": "exporter l'erreur",
|
||||
"Export page": "Exporter la page",
|
||||
"Export successful": "Exportation réussie",
|
||||
"Export space": "Exporter l'espace",
|
||||
"Export {{type}}": "Exporter {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
|
||||
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
|
||||
"Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.",
|
||||
"Uploading {{name}}": "Téléchargement de {{name}}",
|
||||
"Uploading file": "Téléchargement du fichier",
|
||||
"Table": "Tableau",
|
||||
"Insert a table.": "Insérez un tableau.",
|
||||
"Insert collapsible block.": "Insérer un bloc repliable.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Partage supprimé avec succès",
|
||||
"Share not found": "Partage non trouvé",
|
||||
"Failed to share page": "Échec du partage de la page",
|
||||
"Disable public sharing": "Désactiver le partage public",
|
||||
"Prevent members from sharing pages publicly.": "Empêcher les membres de partager des pages publiquement.",
|
||||
"Toggle public sharing": "Basculer le partage public",
|
||||
"Toggle space public sharing": "Basculer le partage public de l'espace",
|
||||
"Public sharing is disabled at the workspace level": "Le partage public est désactivé au niveau de l'espace de travail",
|
||||
"Prevent pages in this space from being shared publicly.": "Empêcher les pages de cet espace d'être partagées publiquement.",
|
||||
"Requires an enterprise license": "Nécessite une licence d'entreprise",
|
||||
"Enable public sharing": "Activer le partage public",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Êtes-vous sûr de vouloir activer le partage public ? Les membres pourront partager des pages publiquement.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Êtes-vous sûr de vouloir désactiver le partage public ? Tous les liens partagés existants dans cet espace de travail seront supprimés.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Êtes-vous sûr de vouloir activer le partage public pour cet espace ?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Êtes-vous sûr de vouloir désactiver le partage public ? Tous les liens partagés existants dans cet espace seront supprimés.",
|
||||
"Public sharing is disabled": "Le partage public est désactivé",
|
||||
"Public sharing has been disabled at the workspace level.": "Le partage public a été désactivé au niveau de l'espace de travail.",
|
||||
"Public sharing has been disabled for this space.": "Le partage public a été désactivé pour cet espace.",
|
||||
"Copy page": "Copier la page",
|
||||
"Copy page to a different space.": "Copier la page dans un autre espace.",
|
||||
"Page copied successfully": "Page copiée avec succès",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Supprimer le fournisseur SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Êtes-vous sûr de vouloir supprimer ce fournisseur SSO ?",
|
||||
"Action": "Action",
|
||||
"{{ssoProviderType}} configuration": "Configuration {{ssoProviderType}}",
|
||||
"Icon": "Icône",
|
||||
"Upload image": "Téléverser une image",
|
||||
"Remove image": "Supprimer l'image",
|
||||
"Failed to remove image": "Échec de la suppression de l'image",
|
||||
"Image exceeds 10MB limit.": "L'image dépasse la limite de 10 Mo.",
|
||||
"Image removed successfully": "Image supprimée avec succès",
|
||||
"API key": "Clé API",
|
||||
"API key created successfully": "Clé API créée avec succès",
|
||||
"API keys": "Clés API",
|
||||
"API management": "Gestion des API",
|
||||
"Are you sure you want to revoke this API key": "Êtes-vous sûr de vouloir révoquer cette clé API",
|
||||
"Create API Key": "Créer une clé API",
|
||||
"Custom expiration date": "Date d'expiration personnalisée",
|
||||
"Enter a descriptive token name": "Entrez un nom descriptif pour le jeton",
|
||||
"Expiration": "Expiration",
|
||||
"Expired": "Expiré(e)",
|
||||
"Expires": "Expire",
|
||||
"I've saved my API key": "J'ai enregistré ma clé API",
|
||||
"Last use": "Dernière utilisation",
|
||||
"No API keys found": "Aucune clé API trouvée",
|
||||
"No expiration": "Pas d'expiration",
|
||||
"Revoke API key": "Révoquer la clé API",
|
||||
"Revoked successfully": "Révoqué(e) avec succès",
|
||||
"Select expiration date": "Sélectionnez la date d'expiration",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Cette action ne peut pas être annulée. Toutes les applications utilisant cette clé API cesseront de fonctionner.",
|
||||
"Update API key": "Mettre à jour la clé API",
|
||||
"Manage API keys for all users in the workspace": "Gérer les clés API pour tous les utilisateurs dans l'espace de travail",
|
||||
"AI settings": "Paramètres de l'IA",
|
||||
"AI search": "Recherche IA",
|
||||
"AI Answer": "Réponse IA",
|
||||
"Ask AI": "Demander à l'IA",
|
||||
"AI is thinking...": "L'IA réfléchit...",
|
||||
"Ask a question...": "Posez une question...",
|
||||
"AI-powered search (Ask AI)": "Recherche assistée par l'IA (Demander à l'IA)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La recherche IA utilise des incorporations vectorielles pour fournir des capacités de recherche sémantique à travers le contenu de votre espace de travail.",
|
||||
"Toggle AI search": "Basculer la recherche IA",
|
||||
"Sources": "Sources",
|
||||
"Ask AI not available for attachments": "Demande à l'IA non disponible pour les pièces jointes",
|
||||
"No answer available": "Pas de réponse disponible",
|
||||
"Background color": "Couleur de fond",
|
||||
"Highlight color": "Couleur de surbrillance",
|
||||
"Remove color": "Supprimer la couleur"
|
||||
"{{ssoProviderType}} configuration": "Configuration {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.",
|
||||
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
|
||||
"Confirm": "Conferma",
|
||||
"Copy as Markdown": "Copia come Markdown",
|
||||
"Copy link": "Copia link",
|
||||
"Create": "Crea",
|
||||
"Create group": "Crea gruppo",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina eliminata con successo",
|
||||
"Page history": "Cronologia della pagina",
|
||||
"Select version": "Seleziona versione",
|
||||
"Highlight changes": "Evidenzia modifiche",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||
"Pages": "Pagine",
|
||||
"pages": "pagine",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Esportazione fallita:",
|
||||
"export error": "errore di esportazione",
|
||||
"Export page": "Esporta pagina",
|
||||
"Export successful": "Esportazione riuscita",
|
||||
"Export space": "Esporta spazio",
|
||||
"Export {{type}}": "Esporta {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
|
||||
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
|
||||
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
|
||||
"Uploading {{name}}": "Caricamento di {{name}}",
|
||||
"Uploading file": "Caricamento file",
|
||||
"Table": "Tabella",
|
||||
"Insert a table.": "Inserisci una tabella.",
|
||||
"Insert collapsible block.": "Inserisci blocco comprimibile.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Condivisione eliminata con successo",
|
||||
"Share not found": "Condivisione non trovata",
|
||||
"Failed to share page": "Condivisione della pagina fallita",
|
||||
"Disable public sharing": "Disabilita la condivisione pubblica",
|
||||
"Prevent members from sharing pages publicly.": "Impedisci ai membri di condividere pubblicamente le pagine.",
|
||||
"Toggle public sharing": "Attiva/disattiva la condivisione pubblica",
|
||||
"Toggle space public sharing": "Attiva/disattiva la condivisione pubblica nello spazio",
|
||||
"Public sharing is disabled at the workspace level": "La condivisione pubblica è disabilitata a livello di area di lavoro",
|
||||
"Prevent pages in this space from being shared publicly.": "Impedisci che le pagine in questo spazio vengano condivise pubblicamente.",
|
||||
"Requires an enterprise license": "Richiede una licenza enterprise",
|
||||
"Enable public sharing": "Abilita la condivisione pubblica",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sei sicuro di voler abilitare la condivisione pubblica? I membri potranno condividere le pagine pubblicamente.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questa area di lavoro verranno eliminati.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Sei sicuro di voler abilitare la condivisione pubblica per questo spazio?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questo spazio verranno eliminati.",
|
||||
"Public sharing is disabled": "La condivisione pubblica è disabilitata",
|
||||
"Public sharing has been disabled at the workspace level.": "La condivisione pubblica è stata disabilitata a livello di area di lavoro.",
|
||||
"Public sharing has been disabled for this space.": "La condivisione pubblica è stata disabilitata per questo spazio.",
|
||||
"Copy page": "Copia pagina",
|
||||
"Copy page to a different space.": "Copia pagina in un altro spazio.",
|
||||
"Page copied successfully": "Pagina copiata con successo",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Elimina provider SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Sei sicuro di voler eliminare questo provider SSO?",
|
||||
"Action": "Azione",
|
||||
"{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}",
|
||||
"Icon": "Icona",
|
||||
"Upload image": "Carica immagine",
|
||||
"Remove image": "Rimuovi immagine",
|
||||
"Failed to remove image": "Rimozione immagine fallita",
|
||||
"Image exceeds 10MB limit.": "L'immagine supera il limite di 10MB.",
|
||||
"Image removed successfully": "Immagine rimossa con successo",
|
||||
"API key": "Chiave API",
|
||||
"API key created successfully": "Chiave API creata con successo",
|
||||
"API keys": "Chiavi API",
|
||||
"API management": "Gestione API",
|
||||
"Are you sure you want to revoke this API key": "Sei sicuro di voler revocare questa chiave API",
|
||||
"Create API Key": "Crea Chiave API",
|
||||
"Custom expiration date": "Data di scadenza personalizzata",
|
||||
"Enter a descriptive token name": "Inserisci un nome descrittivo del token",
|
||||
"Expiration": "Scadenza",
|
||||
"Expired": "Scaduto",
|
||||
"Expires": "Scade",
|
||||
"I've saved my API key": "Ho salvato la mia chiave API",
|
||||
"Last use": "Ultimo utilizzo",
|
||||
"No API keys found": "Nessuna chiave API trovata",
|
||||
"No expiration": "Nessuna scadenza",
|
||||
"Revoke API key": "Revoca chiave API",
|
||||
"Revoked successfully": "Revocata con successo",
|
||||
"Select expiration date": "Seleziona la data di scadenza",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Questa azione non può essere annullata. Qualsiasi applicazione che utilizza questa chiave API smetterà di funzionare.",
|
||||
"Update API key": "Aggiorna chiave API",
|
||||
"Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro",
|
||||
"AI settings": "Impostazioni AI",
|
||||
"AI search": "Ricerca AI",
|
||||
"AI Answer": "Risposta AI",
|
||||
"Ask AI": "Chiedi all'AI",
|
||||
"AI is thinking...": "L'AI sta pensando...",
|
||||
"Ask a question...": "Fai una domanda...",
|
||||
"AI-powered search (Ask AI)": "Ricerca potenziata dall'AI (Chiedi all'AI)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La ricerca AI utilizza embeddings vettoriali per fornire capacità di ricerca semantica nel contenuto della tua area di lavoro.",
|
||||
"Toggle AI search": "Attiva/disattiva ricerca AI",
|
||||
"Sources": "Fonti",
|
||||
"Ask AI not available for attachments": "Chiedere all'AI non è disponibile per gli allegati",
|
||||
"No answer available": "Nessuna risposta disponibile",
|
||||
"Background color": "Colore di sfondo",
|
||||
"Highlight color": "Colore evidenziato",
|
||||
"Remove color": "Rimuovi colore"
|
||||
"{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -13,23 +13,22 @@
|
||||
"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.": "このユーザをスペースから削除してもよろしいですか? ユーザはこのスペースへのアクセス権をすべて失います。",
|
||||
"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.": "スペース内のページを作成・編集できます",
|
||||
"Can become members of groups and spaces in workspace": "ワークスペース内のグループやスペースのメンバーになることができます",
|
||||
"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.": "スペース内のページを閲覧できますが編集はできません",
|
||||
"Can view pages in space but not edit.": "スペース内のページを閲覧できますが、編集はできません。",
|
||||
"Cancel": "キャンセル",
|
||||
"Change email": "メールアドレスの変更",
|
||||
"Change password": "パスワードの変更",
|
||||
"Change photo": "画像の変更",
|
||||
"Choose a role": "ロールを選んでください",
|
||||
"Choose your preferred color scheme.": "お好みのカラースキームを選択してください",
|
||||
"Choose your preferred interface language.": "お好みの言語を選択してください",
|
||||
"Choose your preferred page width.": "お好みのページ幅を選択してください",
|
||||
"Choose your preferred color scheme.": "お好みのカラースキームを選択してください。",
|
||||
"Choose your preferred interface language.": "お好みのインターフェース言語を選択してください。",
|
||||
"Choose your preferred page width.": "左右の余白を縮小する場合はオンにしてください。",
|
||||
"Confirm": "確認",
|
||||
"Copy as Markdown": "Markdownとしてコピー",
|
||||
"Copy link": "リンクをコピー",
|
||||
"Create": "新規作成",
|
||||
"Create group": "グループを作成",
|
||||
@@ -41,24 +40,24 @@
|
||||
"Date": "日付",
|
||||
"Delete": "削除",
|
||||
"Delete group": "グループを削除",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "このページを削除してもよろしいですか?子ページとページ履歴も削除されます。この操作は取り消せません。",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "このページを削除してもよろしいですか?この操作により、子ページおよびページ履歴が削除されます。この操作は元に戻せません。",
|
||||
"Description": "説明",
|
||||
"Details": "詳細",
|
||||
"e.g ACME": "例: 山田太郎",
|
||||
"e.g ACME Inc": "例: 株式会社サンプル",
|
||||
"e.g Developers": "例: エンジニア",
|
||||
"e.g Group for developers": "例: 開発チーム",
|
||||
"e.g Group for developers": "例: エンジニアグループ",
|
||||
"e.g product": "例: product",
|
||||
"e.g Product Team": "例: プロダクトチーム",
|
||||
"e.g Sales": "例: 営業部",
|
||||
"e.g Space for product team": "例: プロダクトチーム用スペース",
|
||||
"e.g Space for sales team to collaborate": "例: 営業チーム用スペース",
|
||||
"e.g Product Team": "例: 製品チーム",
|
||||
"e.g Sales": "例: 営業",
|
||||
"e.g Space for product team": "例: 製品チームのスペース",
|
||||
"e.g Space for sales team to collaborate": "例: 営業チーム連携用スペース",
|
||||
"Edit": "編集",
|
||||
"Read": "閲覧",
|
||||
"Read": "読む",
|
||||
"Edit group": "グループを編集",
|
||||
"Email": "メールアドレス",
|
||||
"Enter a strong password": "強力なパスワードを入力してください",
|
||||
"Enter valid email addresses separated by comma or space max_50": "メールアドレスをカンマまたはスペース区切りで入力(最大50件)",
|
||||
"Enter valid email addresses separated by comma or space max_50": "有効なメールアドレスをカンマまたはスペースで区切って入力してください(最大 50 個)",
|
||||
"enter valid emails addresses": "有効なメールアドレスを入力してください",
|
||||
"Enter your current password": "現在のパスワードを入力してください",
|
||||
"enter your full name": "氏名を入力してください",
|
||||
@@ -82,18 +81,18 @@
|
||||
"Group description": "グループ説明",
|
||||
"Group name": "グループ名",
|
||||
"Groups": "グループ",
|
||||
"Has full access to space settings and pages.": "スペース設定とページにフルアクセスできます",
|
||||
"Has full access to space settings and pages.": "スペース設定とページにフルアクセスできます。",
|
||||
"Home": "ホーム",
|
||||
"Import pages": "ページをインポート",
|
||||
"Import pages & space settings": "ページとスペース設定をインポート",
|
||||
"Importing pages": "ページをインポートしています",
|
||||
"invalid invitation link": "無効な招待リンクです",
|
||||
"invalid invitation link": "招待リンクが間違っています",
|
||||
"Invitation signup": "招待登録",
|
||||
"Invite by email": "メールアドレスで招待する",
|
||||
"Invite members": "メンバーを招待する",
|
||||
"Invite new members": "新しいメンバーを招待する",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "招待を承諾していないメンバーがここに表示されます",
|
||||
"Invited members will be granted access to spaces the groups can access": "招待されたメンバーはグループがアクセスできるスペースにアクセスできます",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "招待をまだ承諾していないメンバーはここに表示されます。",
|
||||
"Invited members will be granted access to spaces the groups can access": "招待されたメンバーは、グループがアクセスできるスペースにアクセス権が付与されます",
|
||||
"Join the workspace": "ワークスペースに参加",
|
||||
"Language": "言語",
|
||||
"Light": "ライト",
|
||||
@@ -114,22 +113,20 @@
|
||||
"New page": "新規ページ",
|
||||
"New password": "新しいパスワード",
|
||||
"No group found": "グループが見つかりません",
|
||||
"No page history saved yet.": "ページ履歴がありません",
|
||||
"No page history saved yet.": "まだページの履歴が保存されていません。",
|
||||
"No pages yet": "ページがありません",
|
||||
"No results found...": "結果が見つかりません",
|
||||
"No user found": "ユーザーが見つかりません",
|
||||
"No results found...": "結果が見つかりませんでした...",
|
||||
"No user found": "ユーザがいません",
|
||||
"Overview": "概要",
|
||||
"Owner": "所有者",
|
||||
"page": "ページ",
|
||||
"Page deleted successfully": "ページを削除しました",
|
||||
"Page history": "ページ履歴",
|
||||
"Select version": "バージョンを選択",
|
||||
"Highlight changes": "変更を強調表示",
|
||||
"Page import is in progress. Please do not close this tab.": "ページをインポート中です。このタブを閉じないでください",
|
||||
"Page deleted successfully": "ページが正常に削除されました",
|
||||
"Page history": "ページの履歴",
|
||||
"Page import is in progress. Please do not close this tab.": "ページのインポートが進行中です。このタブを閉じないでください。",
|
||||
"Pages": "ページ",
|
||||
"pages": "ページ",
|
||||
"Password": "パスワード",
|
||||
"Password changed successfully": "パスワードを変更しました",
|
||||
"Password changed successfully": "パスワードが正常に変更されました",
|
||||
"Pending": "保留中",
|
||||
"Please confirm your action": "アクションを確認してください",
|
||||
"Preferences": "設定",
|
||||
@@ -146,95 +143,95 @@
|
||||
"Search for groups": "グループを検索",
|
||||
"Search for users": "ユーザーを検索",
|
||||
"Search for users and groups": "ユーザーとグループを検索",
|
||||
"Search...": "検索",
|
||||
"Search...": "検索する語句を入力",
|
||||
"Select language": "言語を選択",
|
||||
"Select role": "ロールを選択",
|
||||
"Select role to assign to all invited members": "招待するメンバーに割り当てるロールを選択",
|
||||
"Select role to assign to all invited members": "招待されたすべてのメンバーに割り当てるロールを選択してください",
|
||||
"Select theme": "テーマを選択",
|
||||
"Send invitation": "招待を送る",
|
||||
"Invitation sent": "招待を送信しました",
|
||||
"Invitation sent": "招待が送信されました",
|
||||
"Settings": "設定",
|
||||
"Setup workspace": "ワークスペースを設定する",
|
||||
"Sign In": "サインイン",
|
||||
"Sign Up": "新規登録",
|
||||
"Slug": "スラッグ(URL識別子)",
|
||||
"Sign Up": "アカウント登録",
|
||||
"Slug": "Slug (URL用文字列)",
|
||||
"Space": "スペース",
|
||||
"Space description": "スペース説明",
|
||||
"Space menu": "スペースメニュー",
|
||||
"Space name": "スペース名",
|
||||
"Space settings": "スペース設定",
|
||||
"Space slug": "スペースのスラッグ(URL識別子)",
|
||||
"Space slug": "スペースのSlug (URL用文字列)",
|
||||
"Spaces": "スペース",
|
||||
"Spaces you belong to": "所属しているスペース",
|
||||
"No space found": "スペースが見つかりません",
|
||||
"Search for spaces": "スペースを検索",
|
||||
"Start typing to search...": "入力して検索",
|
||||
"Start typing to search...": "検索を開始するには入力してください...",
|
||||
"Status": "ステータス",
|
||||
"Successfully imported": "インポートしました",
|
||||
"Successfully restored": "復元しました",
|
||||
"Successfully imported": "インポートに成功しました",
|
||||
"Successfully restored": "正常に復元されました",
|
||||
"System settings": "システム設定",
|
||||
"Theme": "テーマ",
|
||||
"To change your email, you have to enter your password and new email.": "メールアドレスを変更するには、パスワードと新しいメールアドレスを入力してください",
|
||||
"Toggle full page width": "ページ幅を切り替え",
|
||||
"Unable to import pages. Please try again.": "ページをインポートできませんでした。もう一度お試しください",
|
||||
"To change your email, you have to enter your password and new email.": "メールアドレスを変更するには、パスワードと新しいメールアドレスを入力する必要があります。",
|
||||
"Toggle full page width": "ページ幅を切り替える",
|
||||
"Unable to import pages. Please try again.": "ページをインポートできません。もう一度お試しください。",
|
||||
"untitled": "無題",
|
||||
"Untitled": "無題",
|
||||
"Updated successfully": "更新しました",
|
||||
"Updated successfully": "正常に更新されました",
|
||||
"User": "ユーザー",
|
||||
"Workspace": "ワークスペース",
|
||||
"Workspace Name": "ワークスペース名",
|
||||
"Workspace settings": "ワークスペース設定",
|
||||
"You can change your password here.": "パスワードを変更できます",
|
||||
"You can change your password here.": "パスワードを変更できます。",
|
||||
"Your Email": "メールアドレス",
|
||||
"Your import is complete.": "インポートが完了しました",
|
||||
"Your import is complete.": "インポートが完了しました。",
|
||||
"Your name": "名前",
|
||||
"Your Name": "名前",
|
||||
"Your password": "パスワード",
|
||||
"Your password must be a minimum of 8 characters.": "パスワードは8文字以上にしてください",
|
||||
"Your password must be a minimum of 8 characters.": "パスワードは最低 8 文字必要です。",
|
||||
"Sidebar toggle": "サイドバー切り替え",
|
||||
"Comments": "コメント",
|
||||
"404 page not found": "404 ページが見つかりません",
|
||||
"Sorry, we can't find the page you are looking for.": "お探しのページが見つかりません",
|
||||
"Sorry, we can't find the page you are looking for.": "お探しのページが見つかりません。",
|
||||
"Take me back to homepage": "ホームに戻る",
|
||||
"Forgot password": "パスワードを忘れた",
|
||||
"Forgot your password?": "パスワードを忘れましたか?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "パスワードリセット用のリンクをメールに送信しました。受信トレイを確認してください",
|
||||
"Send reset link": "リセットリンクを送信",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "パスワードリセットリンクがあなたのメールアドレスに送信されました。受信箱を確認してください。",
|
||||
"Send reset link": "リセットリンクを送る",
|
||||
"Password reset": "パスワードリセット",
|
||||
"Your new password": "新しいパスワード",
|
||||
"Set password": "パスワードを設定",
|
||||
"Write a comment": "コメントを書く",
|
||||
"Reply...": "返信...",
|
||||
"Error loading comments.": "コメントの読み込みに失敗しました",
|
||||
"No comments yet.": "コメントがありません",
|
||||
"Error loading comments.": "コメントの読み込み中にエラーが発生しました。",
|
||||
"No comments yet.": "コメントがありません。",
|
||||
"Edit comment": "コメントを編集する",
|
||||
"Delete comment": "コメントを削除する",
|
||||
"Are you sure you want to delete this comment?": "このコメントを削除してもよろしいですか?",
|
||||
"Comment created successfully": "コメントを作成しました",
|
||||
"Error creating comment": "コメントの作成に失敗しました",
|
||||
"Comment updated successfully": "コメントを更新しました",
|
||||
"Comment created successfully": "コメントが作成されました",
|
||||
"Error creating comment": "コメントの作成中にエラーが発生しました",
|
||||
"Comment updated successfully": "コメントが更新されました",
|
||||
"Failed to update comment": "コメントの更新に失敗しました",
|
||||
"Comment deleted successfully": "コメントを削除しました",
|
||||
"Comment deleted successfully": "コメントが削除されました",
|
||||
"Failed to delete comment": "コメントの削除に失敗しました",
|
||||
"Comment resolved successfully": "コメントを解決しました",
|
||||
"Comment re-opened successfully": "コメントを再開しました",
|
||||
"Comment unresolved successfully": "コメントを未解決に戻しました",
|
||||
"Comment resolved successfully": "コメントが解決されました",
|
||||
"Comment re-opened successfully": "コメントが再開されました",
|
||||
"Comment unresolved successfully": "コメントが再解決されました",
|
||||
"Failed to resolve comment": "コメントの解決に失敗しました",
|
||||
"Resolve comment": "コメントを解決",
|
||||
"Unresolve comment": "コメントを未解決に戻す",
|
||||
"Unresolve comment": "コメントを再解決",
|
||||
"Resolve Comment Thread": "コメントスレッドを解決",
|
||||
"Unresolve Comment Thread": "コメントスレッドを未解決に戻す",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "このコメントスレッドを解決しますか?完了としてマークされます",
|
||||
"Are you sure you want to unresolve this comment thread?": "このコメントスレッドを未解決に戻しますか?",
|
||||
"Unresolve Comment Thread": "コメントスレッドを再解決",
|
||||
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "このコメントスレッドを解決しますか? これにより完了としてマークされます。",
|
||||
"Are you sure you want to unresolve this comment thread?": "このコメントスレッドを再解決しますか?",
|
||||
"Resolved": "解決済",
|
||||
"No active comments.": "アクティブなコメントはありません",
|
||||
"No resolved comments.": "解決済みのコメントはありません",
|
||||
"No active comments.": "アクティブなコメントはありません。",
|
||||
"No resolved comments.": "解決されたコメントはありません。",
|
||||
"Revoke invitation": "招待を取り消す",
|
||||
"Revoke": "取り消す",
|
||||
"Don't": "取り消さない",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "この招待を取り消してもよろしいですか?ユーザーはワークスペースに参加できなくなります",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "この招待を取り消してもよろしいですか? ユーザはワークスペースに参加できなくなります。",
|
||||
"Resend invitation": "招待を再度送る",
|
||||
"Anyone with this link can join this workspace.": "このリンクを知っている人は誰でもワークスペースに参加できます",
|
||||
"Anyone with this link can join this workspace.": "このリンクを持っている人は誰でもこのワークスペースに参加できます。",
|
||||
"Invite link": "招待リンク",
|
||||
"Copy": "コピー",
|
||||
"Copy to space": "スペースにコピー",
|
||||
@@ -242,13 +239,13 @@
|
||||
"Duplicate": "複製",
|
||||
"Select a user": "ユーザを選択",
|
||||
"Select a group": "グループを選択",
|
||||
"Export all pages and attachments in this 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.": "スペース内のすべてのページ、コメント、添付ファイル、権限が完全に削除されます",
|
||||
"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.": "確認のためスペース名 <b>{{spaceName}}</b> を入力してください",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "アクションを確認するためにスペース名 <b>{{spaceName}}</b> を入力してください。",
|
||||
"Format": "フォーマット",
|
||||
"Include subpages": "サブページを含める",
|
||||
"Include attachments": "添付ファイルを含める",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "エクスポートに失敗しました:",
|
||||
"export error": "エクスポートエラー",
|
||||
"Export page": "エクスポートページ",
|
||||
"Export successful": "エクスポート成功",
|
||||
"Export space": "エクスポートスペース",
|
||||
"Export {{type}}": "{{type}}をエクスポート",
|
||||
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
|
||||
@@ -277,12 +273,12 @@
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危険",
|
||||
"Mermaid diagram error:": "Mermaid ダイアグラムエラー:",
|
||||
"Invalid Mermaid diagram": "無効な Mermaid ダイアグラムです",
|
||||
"Double-click to edit Draw.io diagram": "ダブルクリックして Draw.io 図を編集",
|
||||
"Mermaid diagram error:": "Mermaid コードエラー",
|
||||
"Invalid Mermaid diagram": "無効な Mermaid コードです",
|
||||
"Double-click to edit Draw.io diagram": "ダブルクリックしてDraw.ioの図を編集",
|
||||
"Exit": "終了",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Double-click to edit Excalidraw diagram": "ダブルクリックして Excalidraw 図を編集",
|
||||
"Double-click to edit Excalidraw diagram": "ダブルクリックしてExcalidraw図を編集",
|
||||
"Paste link": "リンクを貼り付け",
|
||||
"Edit link": "リンクを編集",
|
||||
"Remove link": "リンクを削除",
|
||||
@@ -319,24 +315,22 @@
|
||||
"Bullet List": "箇条書きリスト",
|
||||
"Numbered List": "番号付きリスト",
|
||||
"Blockquote": "引用",
|
||||
"Just start typing with plain text.": "プレーンテキストを入力します",
|
||||
"Track tasks with a to-do list.": "Todo リストでタスクを管理します",
|
||||
"Big section heading.": "大見出し",
|
||||
"Medium section heading.": "中見出し",
|
||||
"Small section heading.": "小見出し",
|
||||
"Create a simple bullet list.": "箇条書きリストを作成します",
|
||||
"Create a list with numbering.": "番号付きリストを作成します",
|
||||
"Create block quote.": "引用ブロックを作成します",
|
||||
"Insert code snippet.": "コードスニペットを挿入します",
|
||||
"Insert horizontal rule divider": "区切り線を挿入します",
|
||||
"Upload any image from your device.": "デバイスから画像をアップロードします",
|
||||
"Upload any video from your device.": "デバイスから動画をアップロードします",
|
||||
"Upload any file from your device.": "デバイスからファイルをアップロードします",
|
||||
"Uploading {{name}}": "{{name}} をアップロード中",
|
||||
"Uploading file": "ファイルをアップロード中",
|
||||
"Just start typing with plain text.": "すぐに文章を書き始められます。",
|
||||
"Track tasks with a to-do list.": "Todoリストでタスクを追跡します。",
|
||||
"Big section heading.": "大きいフォントのセクション見出しです。",
|
||||
"Medium section heading.": "中くらいのフォントのセクション見出しです。",
|
||||
"Small section heading.": "小さいフォントのセクション見出しです。",
|
||||
"Create a simple bullet list.": "シンプルな箇条書きのリストを作成します。",
|
||||
"Create a list with numbering.": "番号付きのリストを作成します。",
|
||||
"Create block quote.": "引用文を作成します。",
|
||||
"Insert code snippet.": "コードスニペットを入力します。",
|
||||
"Insert horizontal rule divider": "水平線を挿入します。",
|
||||
"Upload any image from your device.": "画像をアップロードします。",
|
||||
"Upload any video from your device.": "動画をアップロードします。",
|
||||
"Upload any file from your device.": "ファイルをアップロードします。",
|
||||
"Table": "テーブル",
|
||||
"Insert a table.": "テーブルを挿入します",
|
||||
"Insert collapsible block.": "折りたたみブロックを挿入します",
|
||||
"Insert a table.": "表を挿入します。",
|
||||
"Insert collapsible block.": "折りたたみ可能なブロックを挿入します。",
|
||||
"Video": "動画",
|
||||
"Divider": "区切り線",
|
||||
"Quote": "引用",
|
||||
@@ -344,16 +338,16 @@
|
||||
"File attachment": "ファイル添付",
|
||||
"Toggle block": "ブロックを切り替える",
|
||||
"Callout": "コールアウト",
|
||||
"Insert callout notice.": "コールアウトを挿入します",
|
||||
"Insert callout notice.": "コールアウトブロックを挿入します。",
|
||||
"Math inline": "インライン数式",
|
||||
"Insert inline math equation.": "インライン数式を挿入します",
|
||||
"Insert inline math equation.": "インライン数式を挿入します。",
|
||||
"Math block": "数式ブロック",
|
||||
"Insert math equation": "数式を挿入します",
|
||||
"Mermaid diagram": "Mermaid ダイアグラム",
|
||||
"Insert mermaid diagram": "Mermaid ダイアグラムを挿入します",
|
||||
"Insert and design Drawio diagrams": "Draw.io 図を挿入・編集します",
|
||||
"Insert current date": "現在の日付を挿入します",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw 図を挿入します",
|
||||
"Mermaid diagram": "Mermaidコード",
|
||||
"Insert mermaid diagram": "Mermaidコードを記述して図を挿入します",
|
||||
"Insert and design Drawio diagrams": "Drawioの図を挿入してデザインします",
|
||||
"Insert current date": "今日の日付を挿入します",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidrawの図を埋め込みます",
|
||||
"Multiple": "複数",
|
||||
"Heading {{level}}": "見出し {{level}}",
|
||||
"Toggle title": "タイトルの表示/非表示を切り替える",
|
||||
@@ -363,29 +357,29 @@
|
||||
"Yesterday, {{time}}": "昨日、{{time}}",
|
||||
"Space created successfully": "スペースを作成しました",
|
||||
"Space updated successfully": "スペースを更新しました",
|
||||
"Space deleted successfully": "スペースを削除しました",
|
||||
"Space deleted successfully": "スペースが削除されました",
|
||||
"Members added successfully": "メンバーを追加しました",
|
||||
"Member removed successfully": "メンバーを削除しました",
|
||||
"Member removed successfully": "メンバーが削除されました",
|
||||
"Member role updated successfully": "メンバーのロールを更新しました",
|
||||
"Created by: <b>{{creatorName}}</b>": "作成者: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "作成日: {{time}}",
|
||||
"Created at: {{time}}": "が作成しました:{{time}}",
|
||||
"Edited by {{name}} {{time}}": "最終編集: {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "単語数: {{wordCount}}",
|
||||
"Word count: {{wordCount}}": "ワード数: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "文字数: {{characterCount}}",
|
||||
"New update": "新規更新",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} が利用可能です",
|
||||
"{{latestVersion}} is available": "{{latestVersion}}は利用可能です",
|
||||
"Default page edit mode": "デフォルトのページ編集モード",
|
||||
"Choose your preferred page edit mode. Avoid accidental edits.": "お好みのページ編集モードを選択してください(誤編集を防止します)",
|
||||
"Choose your preferred page edit mode. Avoid accidental edits.": "希望のページ編集モードを選択してください。誤って編集を防ぎます。",
|
||||
"Reading": "読み取り",
|
||||
"Delete member": "メンバーを削除する",
|
||||
"Member deleted successfully": "メンバーを削除しました",
|
||||
"Are you sure you want to delete this workspace member? This action is irreversible.": "このメンバーを削除してもよろしいですか?この操作は取り消せません",
|
||||
"Member deleted successfully": "メンバーが削除されました",
|
||||
"Are you sure you want to delete this workspace member? This action is irreversible.": "ワークスペースメンバーを削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"Move": "移動",
|
||||
"Move page": "ページを移動",
|
||||
"Move page to a different space.": "ページを別のスペースに移動します",
|
||||
"Real-time editor connection lost. Retrying...": "リアルタイム編集の接続が切断されました。再接続中...",
|
||||
"Move page to a different space.": "ページを別のスペースに移動します。",
|
||||
"Real-time editor connection lost. Retrying...": "リアルタイムエディターの接続が失われました。再試行しています…",
|
||||
"Table of contents": "目次",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "見出し(H1、H2、H3)を追加すると目次が生成されます",
|
||||
"Add headings (H1, H2, H3) to generate a table of contents.": "見出し(H1、H2、H3)を追加して目次を生成します。",
|
||||
"Share": "共有",
|
||||
"Public sharing": "公開共有",
|
||||
"Shared by": "共有者",
|
||||
@@ -404,28 +398,13 @@
|
||||
"Delete share": "共有を削除",
|
||||
"Are you sure you want to delete this shared link?": "この共有リンクを削除してもよろしいですか?",
|
||||
"Publicly shared pages from spaces you are a member of will appear here": "メンバーであるスペースからの公開ページがここに表示されます",
|
||||
"Share deleted successfully": "共有を削除しました",
|
||||
"Share deleted successfully": "共有が正常に削除されました",
|
||||
"Share not found": "共有が見つかりません",
|
||||
"Failed to share page": "ページの共有に失敗しました",
|
||||
"Disable public sharing": "公開共有を無効にする",
|
||||
"Prevent members from sharing pages publicly.": "メンバーがページを公開で共有するのを防ぐ。",
|
||||
"Toggle public sharing": "公開共有を切り替える",
|
||||
"Toggle space public sharing": "スペースの公開共有を切り替える",
|
||||
"Public sharing is disabled at the workspace level": "ワークスペースレベルで公開共有が無効になっています",
|
||||
"Prevent pages in this space from being shared publicly.": "このスペース内のページが公開で共有されるのを防ぐ。",
|
||||
"Requires an enterprise license": "エンタープライズライセンスが必要です",
|
||||
"Enable public sharing": "公開共有を有効にする",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "本当に公開共有を有効にしますか?メンバーはページを公開で共有できるようになります。",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "本当に公開共有を無効にしますか?このワークスペース内のすべての既存の共有リンクが削除されます。",
|
||||
"Are you sure you want to enable public sharing for this space?": "本当にこのスペースの公開共有を有効にしますか?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "本当に公開共有を無効にしますか?このスペースのすべての既存の共有リンクが削除されます。",
|
||||
"Public sharing is disabled": "公開共有が無効になっています",
|
||||
"Public sharing has been disabled at the workspace level.": "ワークスペースレベルで公開共有が無効になりました。",
|
||||
"Public sharing has been disabled for this space.": "このスペースで公開共有が無効になりました。",
|
||||
"Copy page": "ページをコピー",
|
||||
"Copy page to a different space.": "ページを別のスペースにコピーします",
|
||||
"Page copied successfully": "ページをコピーしました",
|
||||
"Page duplicated successfully": "ページを複製しました",
|
||||
"Copy page to a different space.": "ページを別のスペースにコピーします。",
|
||||
"Page copied successfully": "ページのコピーに成功しました",
|
||||
"Page duplicated successfully": "ページが正常に複製されました",
|
||||
"Find": "検索",
|
||||
"Not found": "見つかりません",
|
||||
"Previous Match (Shift+Enter)": "前の一致 (Shift+Enter)",
|
||||
@@ -440,26 +419,26 @@
|
||||
"Error": "エラー",
|
||||
"Failed to disable MFA": "MFAの無効化に失敗しました",
|
||||
"Disable two-factor authentication": "二要素認証を無効化",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "二要素認証を無効にすると、アカウントのセキュリティが低下します。サインインにはパスワードのみが必要になります",
|
||||
"Please enter your password to disable two-factor authentication:": "二要素認証を無効にするにはパスワードを入力してください",
|
||||
"Two-factor authentication has been enabled": "二要素認証を有効にしました",
|
||||
"Two-factor authentication has been disabled": "二要素認証を無効にしました",
|
||||
"2-step verification": "2段階認証",
|
||||
"Protect your account with an additional verification layer when signing in.": "サインイン時に追加の認証でアカウントを保護します",
|
||||
"Two-factor authentication is active on your account.": "二要素認証が有効です",
|
||||
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "二要素認証を無効化すると、アカウントのセキュリティが低下します。サインインにはパスワードのみが必要になります。",
|
||||
"Please enter your password to disable two-factor authentication:": "二要素認証を無効化するにはパスワードを入力してください:",
|
||||
"Two-factor authentication has been enabled": "二要素認証が有効になりました",
|
||||
"Two-factor authentication has been disabled": "二要素認証が無効になりました",
|
||||
"2-step verification": "2段階確認",
|
||||
"Protect your account with an additional verification layer when signing in.": "サインイン時に追加の認証レイヤーでアカウントを保護します。",
|
||||
"Two-factor authentication is active on your account.": "二要素認証がアカウントで有効です。",
|
||||
"Add 2FA method": "2FAメソッドを追加",
|
||||
"Backup codes": "バックアップコード",
|
||||
"Disable": "無効にする",
|
||||
"Invalid verification code": "無効な認証コード",
|
||||
"New backup codes have been generated": "新しいバックアップコードを生成しました",
|
||||
"New backup codes have been generated": "新しいバックアップコードが生成されました",
|
||||
"Failed to regenerate backup codes": "バックアップコードの再生成に失敗しました",
|
||||
"About backup codes": "バックアップコードについて",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "認証アプリにアクセスできない場合、バックアップコードでアカウントにアクセスできます。各コードは1回のみ使用可能です",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "新しいバックアップコードはいつでも再生成できます。既存のコードはすべて無効になります",
|
||||
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "バックアップコードは、認証アプリへのアクセスを失った場合にアカウントにアクセスするために使用できます。各コードは一度しか使用できません。",
|
||||
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "いつでも新しいバックアップコードを再生成できます。これにより、既存のすべてのコードが無効になります。",
|
||||
"Confirm password": "パスワードを確認",
|
||||
"Generate new backup codes": "新しいバックアップコードを生成",
|
||||
"Save your new backup codes": "新しいバックアップコードを保存",
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "これらのコードを安全な場所に保存してください。古いバックアップコードは無効になりました",
|
||||
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "これらのコードを安全な場所に保存してください。古いバックアップコードは無効です。",
|
||||
"Your new backup codes": "新しいバックアップコード",
|
||||
"I've saved my backup codes": "バックアップコードを保存しました",
|
||||
"Failed to setup MFA": "MFAの設定に失敗しました",
|
||||
@@ -470,51 +449,51 @@
|
||||
"Enter this code manually in your authenticator app:": "このコードを認証アプリに手動で入力してください:",
|
||||
"2. Enter the 6-digit code from your authenticator": "2. 認証アプリからの6桁のコードを入力してください",
|
||||
"Verify and enable": "確認と有効化",
|
||||
"Failed to generate QR code. Please try again.": "QRコードの生成に失敗しました。もう一度お試しください",
|
||||
"Failed to generate QR code. Please try again.": "QRコードの生成に失敗しました。再試行してください。",
|
||||
"Backup": "バックアップ",
|
||||
"Save codes": "コードを保存",
|
||||
"Save your backup codes": "バックアップコードを保存",
|
||||
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "認証アプリにアクセスできない場合、これらのコードでアカウントにアクセスできます。各コードは1回のみ使用可能です",
|
||||
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "これらのコードは、認証アプリへのアクセスを失った場合にアカウントにアクセスするために使用できます。各コードは一度しか使用できません。",
|
||||
"Print": "印刷",
|
||||
"Two-factor authentication has been set up. Please log in again.": "二要素認証を設定しました。再度ログインしてください",
|
||||
"Two-factor authentication has been set up. Please log in again.": "二要素認証が設定されました。再度ログインしてください。",
|
||||
"Two-Factor authentication required": "二要素認証が必要です",
|
||||
"Your workspace requires two-factor authentication for all users": "このワークスペースではすべてのユーザーに二要素認証が必要です",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "ワークスペースにアクセスするには二要素認証を設定してください。アカウントのセキュリティが強化されます",
|
||||
"Your workspace requires two-factor authentication for all users": "ワークスペースでは、すべてのユーザーに二要素認証が必要です",
|
||||
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "ワークスペースへのアクセスを続けるには、二要素認証を設定する必要があります。これにより、アカウントに追加のセキュリティ層が追加されます。",
|
||||
"Set up two-factor authentication": "二要素認証を設定",
|
||||
"Cancel and logout": "キャンセルしてログアウト",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "このワークスペースでは二要素認証が必要です。続行するには設定してください",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "認証アプリからの確認コードでアカウントのセキュリティが強化されます",
|
||||
"Your workspace requires two-factor authentication. Please set it up to continue.": "ワークスペースでは二要素認証が必要です。続行するには設定してください。",
|
||||
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "これにより、認証アプリからの確認コードが必要となり、アカウントに追加のセキュリティ層が追加されます。",
|
||||
"Password is required": "パスワードが必要です",
|
||||
"Password must be at least 8 characters": "パスワードは8文字以上必要です",
|
||||
"Please enter a 6-digit code": "6桁のコードを入力してください",
|
||||
"Code must be exactly 6 digits": "コードは6桁で入力してください",
|
||||
"Code must be exactly 6 digits": "コードは正確に6桁である必要があります",
|
||||
"Enter the 6-digit code found in your authenticator app": "認証アプリに表示された6桁のコードを入力してください",
|
||||
"Need help authenticating?": "認証に関するヘルプが必要ですか?",
|
||||
"MFA QR Code": "MFA QRコード",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "アカウントを作成しました。二要素認証を設定するためにログインしてください",
|
||||
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "パスワードをリセットしました。新しいパスワードでログインして二要素認証を完了してください",
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "パスワードをリセットしました。新しいパスワードでログインして二要素認証を設定してください",
|
||||
"Password reset was successful. Please log in with your new password.": "パスワードをリセットしました。新しいパスワードでログインしてください",
|
||||
"Account created successfully. Please log in to set up two-factor authentication.": "アカウントが正常に作成されました。二要素認証を設定するためにログインしてください。",
|
||||
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "パスワードのリセットが成功しました。新しいパスワードでログインし、二要素認証を完了してください。",
|
||||
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "パスワードのリセットが成功しました。二要素認証を設定するために新しいパスワードでログインしてください。",
|
||||
"Password reset was successful. Please log in with your new password.": "パスワードのリセットが成功しました。新しいパスワードでログインしてください。",
|
||||
"Two-factor authentication": "二要素認証",
|
||||
"Use authenticator app instead": "代わりに認証アプリを使用",
|
||||
"Verify backup code": "バックアップコードを確認",
|
||||
"Use backup code": "バックアップコードを使用",
|
||||
"Enter one of your backup codes": "バックアップコードのいずれかを入力してください",
|
||||
"Backup code": "バックアップコード",
|
||||
"Enter one of your backup codes. Each backup code can only be used once.": "バックアップコードを入力してください。各コードは1回のみ使用可能です",
|
||||
"Enter one of your backup codes. Each backup code can only be used once.": "バックアップコードのいずれかを入力してください。各バックアップコードは一度しか使用できません。",
|
||||
"Verify": "確認",
|
||||
"Trash": "ごみ箱",
|
||||
"Pages in trash will be permanently deleted after 30 days.": "ごみ箱内のページは30日後に完全に削除されます",
|
||||
"Pages in trash will be permanently deleted after 30 days.": "ごみ箱内のページは30日後に完全に削除されます。",
|
||||
"Deleted": "削除",
|
||||
"No pages in trash": "ごみ箱にページがありません",
|
||||
"Permanently delete page?": "ページを完全に削除しますか?",
|
||||
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "「{{title}}」を完全に削除しますか?この操作は取り消せません",
|
||||
"Restore '{{title}}' and its sub-pages?": "「{{title}}」とそのサブページを復元しますか?",
|
||||
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "{{title}}』を完全に削除しますか? この操作は元に戻せません。",
|
||||
"Restore '{{title}}' and its sub-pages?": "{{title}}』とそのサブページを復元しますか?",
|
||||
"Move to trash": "ごみ箱に移動",
|
||||
"Move this page to trash?": "このページをごみ箱に移動しますか?",
|
||||
"Restore page": "ページを復元",
|
||||
"Page moved to trash": "ページをごみ箱に移動しました",
|
||||
"Page restored successfully": "ページを復元しました",
|
||||
"Page moved to trash": "ページがごみ箱に移動されました",
|
||||
"Page restored successfully": "ページが正常に復元されました",
|
||||
"Deleted by": "削除者",
|
||||
"Deleted at": "削除日時",
|
||||
"Preview": "プレビュー",
|
||||
@@ -532,10 +511,10 @@
|
||||
"Enterprise": "エンタープライズ",
|
||||
"Download attachment": "添付ファイルをダウンロード",
|
||||
"Allowed email domains": "許可されたメールドメイン",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "これらのドメインのメールアドレスを持つユーザーのみSSO経由で登録できます",
|
||||
"Only users with email addresses from these domains can signup via SSO.": "これらのドメインからのメールアドレスを持つユーザーのみがSSOで登録できます。",
|
||||
"Enter valid domain names separated by comma or space": "コンマまたはスペースで区切って有効なドメイン名を入力してください",
|
||||
"Enforce two-factor authentication": "二要素認証を強制する",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "有効にすると、すべてのメンバーが二要素認証を設定しないとワークスペースにアクセスできなくなります",
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.": "一度強制されると、すべてのメンバーはワークスペースにアクセスするために二要素認証を有効にする必要があります。",
|
||||
"Toggle MFA enforcement": "MFAの強制を切り替える",
|
||||
"Display name": "表示名",
|
||||
"Allow signup": "登録を許可する",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "SSOプロバイダーを削除する",
|
||||
"Are you sure you want to delete this SSO provider?": "このSSOプロバイダーを削除してもよろしいですか?",
|
||||
"Action": "アクション",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}}の構成",
|
||||
"Icon": "アイコン",
|
||||
"Upload image": "画像をアップロード",
|
||||
"Remove image": "画像を削除",
|
||||
"Failed to remove image": "画像の削除に失敗しました",
|
||||
"Image exceeds 10MB limit.": "画像が10MBの制限を超えています",
|
||||
"Image removed successfully": "画像を削除しました",
|
||||
"API key": "APIキー",
|
||||
"API key created successfully": "APIキーを作成しました",
|
||||
"API keys": "APIキー",
|
||||
"API management": "API管理",
|
||||
"Are you sure you want to revoke this API key": "このAPIキーを無効にしてもよろしいですか",
|
||||
"Create API Key": "APIキーを作成",
|
||||
"Custom expiration date": "カスタム有効期限",
|
||||
"Enter a descriptive token name": "説明的なトークン名を入力してください",
|
||||
"Expiration": "有効期限",
|
||||
"Expired": "期限切れ",
|
||||
"Expires": "期限が切れます",
|
||||
"I've saved my API key": "APIキーを保存しました",
|
||||
"Last use": "最終使用",
|
||||
"No API keys found": "APIキーが見つかりません",
|
||||
"No expiration": "期限なし",
|
||||
"Revoke API key": "APIキーを無効にする",
|
||||
"Revoked successfully": "無効にしました",
|
||||
"Select expiration date": "有効期限を選択してください",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "この操作は取り消せません。このAPIキーを使用しているアプリケーションは動作しなくなります",
|
||||
"Update API key": "APIキーを更新",
|
||||
"Manage API keys for all users in the workspace": "ワークスペース内のすべてのユーザーのAPIキーを管理",
|
||||
"AI settings": "AI設定",
|
||||
"AI search": "AI検索",
|
||||
"AI Answer": "AI回答",
|
||||
"Ask AI": "AIに質問する",
|
||||
"AI is thinking...": "AIが考え中...",
|
||||
"Ask a question...": "質問を入力...",
|
||||
"AI-powered search (Ask AI)": "AIによる検索(AIに質問)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI検索はベクター埋め込みを使用してワークスペース全体の意味検索を実現します",
|
||||
"Toggle AI search": "AI検索を切り替え",
|
||||
"Sources": "ソース",
|
||||
"Ask AI not available for attachments": "添付ファイルにはAI質問は利用できません",
|
||||
"No answer available": "回答がありません",
|
||||
"Background color": "背景色",
|
||||
"Highlight color": "ハイライト色",
|
||||
"Remove color": "色を削除"
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}}の構成"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.",
|
||||
"Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.",
|
||||
"Confirm": "확인",
|
||||
"Copy as Markdown": "Markdown으로 복사",
|
||||
"Copy link": "링크 복사",
|
||||
"Create": "생성",
|
||||
"Create group": "팀 생성",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "페이지",
|
||||
"Page deleted successfully": "페이지 삭제 완료",
|
||||
"Page history": "페이지 기록",
|
||||
"Select version": "버전 선택",
|
||||
"Highlight changes": "변경 사항 강조",
|
||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||
"Pages": "페이지",
|
||||
"pages": "페이지",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "내보내기 실패:",
|
||||
"export error": "내보내기 오류",
|
||||
"Export page": "페이지 내보내기",
|
||||
"Export successful": "내보내기 성공",
|
||||
"Export space": "Space 내보내기",
|
||||
"Export {{type}}": "{{type}} 내보내기",
|
||||
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
|
||||
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
|
||||
"Upload any file from your device.": "기기에서 파일을 업로드하세요.",
|
||||
"Uploading {{name}}": "{{name}} 업로드 중",
|
||||
"Uploading file": "파일 업로드 중",
|
||||
"Table": "테이블",
|
||||
"Insert a table.": "테이블 삽입.",
|
||||
"Insert collapsible block.": "접을 수 있는 블록 삽입.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "공유가 성공적으로 삭제되었습니다",
|
||||
"Share not found": "공유를 찾을 수 없습니다",
|
||||
"Failed to share page": "페이지 공유에 실패했습니다",
|
||||
"Disable public sharing": "공유 비활성화",
|
||||
"Prevent members from sharing pages publicly.": "멤버들이 페이지를 공개적으로 공유하지 못하도록 방지하십시오.",
|
||||
"Toggle public sharing": "공유 전환",
|
||||
"Toggle space public sharing": "공간 공유 전환",
|
||||
"Public sharing is disabled at the workspace level": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
|
||||
"Prevent pages in this space from being shared publicly.": "이 공간의 페이지가 공개적으로 공유되지 않도록 방지하십시오.",
|
||||
"Requires an enterprise license": "기업 라이센스가 필요합니다.",
|
||||
"Enable public sharing": "공유 활성화",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공유를 활성화하시겠습니까? 멤버들이 페이지를 공개적으로 공유할 수 있게 됩니다.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 워크스페이스의 모든 기존 공유 링크가 삭제됩니다.",
|
||||
"Are you sure you want to enable public sharing for this space?": "이 공간의 공유를 활성화하시겠습니까?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 공간의 모든 기존 공유 링크가 삭제됩니다.",
|
||||
"Public sharing is disabled": "공유가 비활성화되었습니다.",
|
||||
"Public sharing has been disabled at the workspace level.": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
|
||||
"Public sharing has been disabled for this space.": "이 공간의 공유가 비활성화되었습니다.",
|
||||
"Copy page": "페이지 복사하기",
|
||||
"Copy page to a different space.": "다른 공간으로 페이지 복사하기.",
|
||||
"Page copied successfully": "페이지가 성공적으로 복사되었습니다",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "SSO 제공자 삭제",
|
||||
"Are you sure you want to delete this SSO provider?": "이 SSO 제공자를 삭제하시겠습니까?",
|
||||
"Action": "작업",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} 구성",
|
||||
"Icon": "아이콘",
|
||||
"Upload image": "이미지 업로드",
|
||||
"Remove image": "이미지 제거",
|
||||
"Failed to remove image": "이미지 제거 실패",
|
||||
"Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.",
|
||||
"Image removed successfully": "이미지가 성공적으로 제거되었습니다",
|
||||
"API key": "API 키",
|
||||
"API key created successfully": "API 키 생성 완료",
|
||||
"API keys": "API 키",
|
||||
"API management": "API 관리",
|
||||
"Are you sure you want to revoke this API key": "이 API 키를 취소하시겠습니까?",
|
||||
"Create API Key": "API 키 생성",
|
||||
"Custom expiration date": "사용자 정의 만료일",
|
||||
"Enter a descriptive token name": "토큰 이름을 입력하세요",
|
||||
"Expiration": "만료",
|
||||
"Expired": "만료됨",
|
||||
"Expires": "만료일",
|
||||
"I've saved my API key": "API 키를 저장했습니다",
|
||||
"Last use": "최근 사용",
|
||||
"No API keys found": "API 키를 찾을 수 없습니다",
|
||||
"No expiration": "유효기간 없음",
|
||||
"Revoke API key": "API 키 취소",
|
||||
"Revoked successfully": "성공적으로 취소되었습니다",
|
||||
"Select expiration date": "만료일 선택",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.",
|
||||
"Update API key": "API 키 갱신",
|
||||
"Manage API keys for all users in the workspace": "워크스페이스 내 모든 사용자의 API 키 관리",
|
||||
"AI settings": "AI 설정",
|
||||
"AI search": "AI 검색",
|
||||
"AI Answer": "AI 답변",
|
||||
"Ask AI": "AI에게 묻기",
|
||||
"AI is thinking...": "AI가 생각 중입니다...",
|
||||
"Ask a question...": "질문하세요...",
|
||||
"AI-powered search (Ask AI)": "AI 지원 검색 (AI에게 묻기)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI 검색은 벡터 임베딩을 사용하여 작업공간 콘텐츠에 대한 의미 검색 기능을 제공합니다.",
|
||||
"Toggle AI search": "AI 검색 전환",
|
||||
"Sources": "출처",
|
||||
"Ask AI not available for attachments": "AI에게 묻기 기능은 첨부 파일에 대해 사용할 수 없습니다",
|
||||
"No answer available": "답변을 제공할 수 없습니다",
|
||||
"Background color": "배경 색",
|
||||
"Highlight color": "강조 색",
|
||||
"Remove color": "색 제거"
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} 구성"
|
||||
}
|
||||
|
||||
@@ -29,13 +29,12 @@
|
||||
"Choose your preferred interface language.": "Kies uw gewenste interfacetaal.",
|
||||
"Choose your preferred page width.": "Kies uw gewenste paginabreedte.",
|
||||
"Confirm": "Bevestig",
|
||||
"Copy as Markdown": "Kopiëren als Markdown",
|
||||
"Copy link": "Link kopiëren",
|
||||
"Create": "Aanmaken",
|
||||
"Create group": "Groep aanmaken",
|
||||
"Create page": "Pagina aanmaken",
|
||||
"Create space": "Ruimte aanmaken",
|
||||
"Create workspace": "Werkruimte aanmaken",
|
||||
"Create workspace": "Wwerkruimte aanmaken",
|
||||
"Current password": "Huidig wachtwoord",
|
||||
"Dark": "Donker",
|
||||
"Date": "Datum",
|
||||
@@ -92,7 +91,7 @@
|
||||
"Invite by email": "Uitnodigen via e-mail",
|
||||
"Invite members": "Leden uitnodigen",
|
||||
"Invite new members": "Nieuwe leden uitnodigen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Uitgenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Uigenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Uitgenodigde leden wordt toegang gegeven tot ruimtes de groepen toegang toe heeft",
|
||||
"Join the workspace": "Word lid van de werkruimte",
|
||||
"Language": "Taal",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina succesvol verwijderd",
|
||||
"Page history": "Pagina geschiedenis",
|
||||
"Select version": "Selecteer versie",
|
||||
"Highlight changes": "Wijzigingen markeren",
|
||||
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
||||
"Pages": "Pagina's",
|
||||
"pages": "pagina's",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Exporteren mislukt:",
|
||||
"export error": "Exporteer fout",
|
||||
"Export page": "Exporteer pagina",
|
||||
"Export successful": "Export succesvol",
|
||||
"Export space": "Exporteer ruimte",
|
||||
"Export {{type}}": "Exporteer {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.",
|
||||
"Upload any video from your device.": "Upload een video vanaf uw apparaat.",
|
||||
"Upload any file from your device.": "Upload een bestand vanaf uw apparaat.",
|
||||
"Uploading {{name}}": "Uploaden {{name}}",
|
||||
"Uploading file": "Bestand uploaden",
|
||||
"Table": "Tabel",
|
||||
"Insert a table.": "Voeg een tabel in.",
|
||||
"Insert collapsible block.": "Inklapbaar blok invoegen.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Delen succesvol verwijderd",
|
||||
"Share not found": "Delen niet gevonden",
|
||||
"Failed to share page": "Pagina delen mislukt",
|
||||
"Disable public sharing": "Openbaar delen uitschakelen",
|
||||
"Prevent members from sharing pages publicly.": "Voorkom dat leden pagina's openbaar delen.",
|
||||
"Toggle public sharing": "Wissel openbaar delen",
|
||||
"Toggle space public sharing": "Wissel openbaar delen van ruimte",
|
||||
"Public sharing is disabled at the workspace level": "Openbaar delen is uitgeschakeld op werkruimteniveau",
|
||||
"Prevent pages in this space from being shared publicly.": "Voorkom dat pagina's in deze ruimte openbaar worden gedeeld.",
|
||||
"Requires an enterprise license": "Vereist een bedrijfslicentie",
|
||||
"Enable public sharing": "Openbaar delen inschakelen",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Weet je zeker dat je openbaar delen wilt inschakelen? Leden kunnen pagina's openbaar delen.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Weet je zeker dat je openbaar delen wilt uitschakelen? Alle bestaande gedeelde links in deze werkruimte zullen worden verwijderd.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Weet je zeker dat je openbaar delen voor deze ruimte wilt inschakelen?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Weet je zeker dat je openbaar delen wilt uitschakelen? Alle bestaande gedeelde links in deze ruimte zullen worden verwijderd.",
|
||||
"Public sharing is disabled": "Openbaar delen is uitgeschakeld",
|
||||
"Public sharing has been disabled at the workspace level.": "Openbaar delen is uitgeschakeld op werkruimteniveau.",
|
||||
"Public sharing has been disabled for this space.": "Openbaar delen is uitgeschakeld voor deze ruimte.",
|
||||
"Copy page": "Pagina kopiëren",
|
||||
"Copy page to a different space.": "Kopieer pagina naar een andere ruimte.",
|
||||
"Page copied successfully": "Pagina succesvol gekopieerd",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Verwijder SSO-provider",
|
||||
"Are you sure you want to delete this SSO provider?": "Weet u zeker dat u deze SSO-provider wilt verwijderen?",
|
||||
"Action": "Actie",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} configuratie",
|
||||
"Icon": "Icoon",
|
||||
"Upload image": "Afbeelding uploaden",
|
||||
"Remove image": "Afbeelding verwijderen",
|
||||
"Failed to remove image": "Afbeelding verwijderen mislukt",
|
||||
"Image exceeds 10MB limit.": "Afbeelding overschrijdt de limiet van 10MB.",
|
||||
"Image removed successfully": "Afbeelding succesvol verwijderd",
|
||||
"API key": "API-sleutel",
|
||||
"API key created successfully": "API-sleutel succesvol aangemaakt",
|
||||
"API keys": "API-sleutels",
|
||||
"API management": "API-beheer",
|
||||
"Are you sure you want to revoke this API key": "Weet u zeker dat u deze API-sleutel wilt intrekken",
|
||||
"Create API Key": "API-sleutel aanmaken",
|
||||
"Custom expiration date": "Aangepaste vervaldatum",
|
||||
"Enter a descriptive token name": "Voer een beschrijvende tokennaam in",
|
||||
"Expiration": "Vervaldatum",
|
||||
"Expired": "Verlopen",
|
||||
"Expires": "Verloopt",
|
||||
"I've saved my API key": "Ik heb mijn API-sleutel opgeslagen",
|
||||
"Last use": "Laatst gebruikt",
|
||||
"No API keys found": "Geen API-sleutels gevonden",
|
||||
"No expiration": "Geen vervaldatum",
|
||||
"Revoke API key": "API-sleutel intrekken",
|
||||
"Revoked successfully": "Succesvol ingetrokken",
|
||||
"Select expiration date": "Selecteer vervaldatum",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Deze actie kan niet ongedaan worden gemaakt. Alle toepassingen die deze API-sleutel gebruiken, zullen niet meer werken.",
|
||||
"Update API key": "API-sleutel bijwerken",
|
||||
"Manage API keys for all users in the workspace": "Beheer API-sleutels voor alle gebruikers in de werkruimte",
|
||||
"AI settings": "AI-instellingen",
|
||||
"AI search": "AI-zoekopdracht",
|
||||
"AI Answer": "AI Antwoord",
|
||||
"Ask AI": "Vraag AI",
|
||||
"AI is thinking...": "AI is aan het nadenken...",
|
||||
"Ask a question...": "Stel een vraag...",
|
||||
"AI-powered search (Ask AI)": "AI-ondersteunde zoekopdracht (Vraag AI)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI-zoekopdracht maakt gebruik van vectorembeddings om semantische zoekmogelijkheden te bieden in uw werkruimte-inhoud.",
|
||||
"Toggle AI search": "Schakel AI-zoekopdracht in/uit",
|
||||
"Sources": "Bronnen",
|
||||
"Ask AI not available for attachments": "Vraag AI is niet beschikbaar voor bijlages",
|
||||
"No answer available": "Geen antwoord beschikbaar",
|
||||
"Background color": "Achtergrondkleur",
|
||||
"Highlight color": "Markeerkleur",
|
||||
"Remove color": "Kleur verwijderen"
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} configuratie"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Escolha o idioma da interface.",
|
||||
"Choose your preferred page width.": "Escolha a largura preferida da página.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy as Markdown": "Copiar como Markdown",
|
||||
"Copy link": "Copiar link",
|
||||
"Create": "Criar",
|
||||
"Create group": "Criar grupo",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página excluída com sucesso",
|
||||
"Page history": "Histórico da página",
|
||||
"Select version": "Selecionar versão",
|
||||
"Highlight changes": "Destacar alterações",
|
||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Falha ao exportar:",
|
||||
"export error": "erro de exportação",
|
||||
"Export page": "Exportar página",
|
||||
"Export successful": "Exportação bem-sucedida",
|
||||
"Export space": "Exportar espaço",
|
||||
"Export {{type}}": "Exportar para {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
|
||||
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
|
||||
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
|
||||
"Uploading {{name}}": "Enviando {{name}}",
|
||||
"Uploading file": "Enviando arquivo",
|
||||
"Table": "Tabela",
|
||||
"Insert a table.": "Insira uma tabela.",
|
||||
"Insert collapsible block.": "Insira um bloco colapsável.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Compartilhamento excluído com sucesso",
|
||||
"Share not found": "Compartilhamento não encontrado",
|
||||
"Failed to share page": "Falha ao compartilhar página",
|
||||
"Disable public sharing": "Desativar compartilhamento público",
|
||||
"Prevent members from sharing pages publicly.": "Impedir que os membros compartilhem páginas publicamente.",
|
||||
"Toggle public sharing": "Alternar compartilhamento público",
|
||||
"Toggle space public sharing": "Alternar compartilhamento público do espaço",
|
||||
"Public sharing is disabled at the workspace level": "O compartilhamento público está desativado no nível do espaço de trabalho",
|
||||
"Prevent pages in this space from being shared publicly.": "Impedir que as páginas neste espaço sejam compartilhadas publicamente.",
|
||||
"Requires an enterprise license": "Requer uma licença empresarial",
|
||||
"Enable public sharing": "Ativar compartilhamento público",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Tem certeza de que deseja ativar o compartilhamento público? Os membros poderão compartilhar páginas publicamente.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço de trabalho serão excluídos.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Tem certeza de que deseja ativar o compartilhamento público para este espaço?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço serão excluídos.",
|
||||
"Public sharing is disabled": "Compartilhamento público está desativado",
|
||||
"Public sharing has been disabled at the workspace level.": "O compartilhamento público foi desativado no nível do espaço de trabalho.",
|
||||
"Public sharing has been disabled for this space.": "O compartilhamento público foi desativado para este espaço.",
|
||||
"Copy page": "Copiar página",
|
||||
"Copy page to a different space.": "Copiar página para um espaço diferente.",
|
||||
"Page copied successfully": "Página copiada com sucesso",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Excluir provedor de SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Tem certeza de que deseja excluir este provedor de SSO?",
|
||||
"Action": "Ação",
|
||||
"{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}",
|
||||
"Icon": "Ícone",
|
||||
"Upload image": "Fazer upload da imagem",
|
||||
"Remove image": "Remover imagem",
|
||||
"Failed to remove image": "Falha ao remover imagem",
|
||||
"Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.",
|
||||
"Image removed successfully": "Imagem removida com sucesso",
|
||||
"API key": "Chave API",
|
||||
"API key created successfully": "Chave API criada com sucesso",
|
||||
"API keys": "Chaves API",
|
||||
"API management": "Gestão de API",
|
||||
"Are you sure you want to revoke this API key": "Tem certeza de que deseja revogar esta chave API",
|
||||
"Create API Key": "Criar Chave API",
|
||||
"Custom expiration date": "Data de expiração personalizada",
|
||||
"Enter a descriptive token name": "Insira um nome descritivo para o token",
|
||||
"Expiration": "Expiração",
|
||||
"Expired": "Expirado",
|
||||
"Expires": "Expira",
|
||||
"I've saved my API key": "Salvei minha chave API",
|
||||
"Last use": "Último uso",
|
||||
"No API keys found": "Nenhuma chave API encontrada",
|
||||
"No expiration": "Sem expiração",
|
||||
"Revoke API key": "Revogar chave API",
|
||||
"Revoked successfully": "Revogada com sucesso",
|
||||
"Select expiration date": "Selecionar data de expiração",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.",
|
||||
"Update API key": "Atualizar chave API",
|
||||
"Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho",
|
||||
"AI settings": "Configurações de IA",
|
||||
"AI search": "Pesquisa IA",
|
||||
"AI Answer": "Resposta de IA",
|
||||
"Ask AI": "Pergunte à IA",
|
||||
"AI is thinking...": "IA está pensando...",
|
||||
"Ask a question...": "Faça uma pergunta...",
|
||||
"AI-powered search (Ask AI)": "Pesquisa com IA (Pergunte à IA)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "A pesquisa IA usa vetores de incorporação para fornecer capacidades de pesquisa semântica em todo o conteúdo do seu espaço de trabalho.",
|
||||
"Toggle AI search": "Alternar pesquisa de IA",
|
||||
"Sources": "Fontes",
|
||||
"Ask AI not available for attachments": "Perguntar à IA não está disponível para anexos",
|
||||
"No answer available": "Nenhuma resposta disponível",
|
||||
"Background color": "Cor de fundo",
|
||||
"Highlight color": "Cor de destaque",
|
||||
"Remove color": "Remover cor"
|
||||
"{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.",
|
||||
"Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.",
|
||||
"Confirm": "Подтвердить",
|
||||
"Copy as Markdown": "Копировать как Markdown",
|
||||
"Copy link": "Копировать ссылку",
|
||||
"Create": "Создать",
|
||||
"Create group": "Создать группу",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "страница",
|
||||
"Page deleted successfully": "Страница успешно удалена",
|
||||
"Page history": "История страницы",
|
||||
"Select version": "Выбрать версию",
|
||||
"Highlight changes": "Выделить изменения",
|
||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||
"Pages": "Страницы",
|
||||
"pages": "страницы",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Экспортирование не удалось:",
|
||||
"export error": "ошибка экспорта",
|
||||
"Export page": "Экспорт страницы",
|
||||
"Export successful": "Экспорт выполнен успешно",
|
||||
"Export space": "Экспорт пространства",
|
||||
"Export {{type}}": "Экспорт {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
|
||||
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
|
||||
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
|
||||
"Uploading {{name}}": "Загрузка {{name}}",
|
||||
"Uploading file": "Загрузка файла",
|
||||
"Table": "Таблица",
|
||||
"Insert a table.": "Вставить таблицу.",
|
||||
"Insert collapsible block.": "Вставить сворачиваемый блок.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Общий доступ успешно удален",
|
||||
"Share not found": "Общий доступ не найден",
|
||||
"Failed to share page": "Не удалось поделиться страницей",
|
||||
"Disable public sharing": "Отключить общий доступ",
|
||||
"Prevent members from sharing pages publicly.": "Запретить участникам делиться страницами публично.",
|
||||
"Toggle public sharing": "Переключить общий доступ",
|
||||
"Toggle space public sharing": "Переключить общий доступ для пространства",
|
||||
"Public sharing is disabled at the workspace level": "Общий доступ отключен на уровне рабочего пространства",
|
||||
"Prevent pages in this space from being shared publicly.": "Запретить делиться страницами в этом пространстве публично.",
|
||||
"Requires an enterprise license": "Требуется корпоративная лицензия",
|
||||
"Enable public sharing": "Включить общий доступ",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Вы уверены, что хотите включить общий доступ? Участники смогут делиться страницами публично.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Вы уверены, что хотите отключить общий доступ? Все существующие ссылки в этом рабочем пространстве будут удалены.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Вы уверены, что хотите включить общий доступ для этого пространства?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Вы уверены, что хотите отключить общий доступ? Все существующие ссылки в этом пространстве будут удалены.",
|
||||
"Public sharing is disabled": "Общий доступ отключен",
|
||||
"Public sharing has been disabled at the workspace level.": "Общий доступ был отключен на уровне рабочего пространства.",
|
||||
"Public sharing has been disabled for this space.": "Общий доступ был отключен для этого пространства.",
|
||||
"Copy page": "Копировать страницу",
|
||||
"Copy page to a different space.": "Копировать страницу в другое пространство.",
|
||||
"Page copied successfully": "Страница успешно скопирована",
|
||||
@@ -519,10 +498,10 @@
|
||||
"Deleted at": "Удалено в",
|
||||
"Preview": "Предпросмотр",
|
||||
"Subpages": "Подстраницы",
|
||||
"Failed to load subpages": "Не удалось загрузить под страницы",
|
||||
"Failed to load subpages": "Не удалось загрузить подстраницы",
|
||||
"No subpages": "Нет подстраниц",
|
||||
"Subpages (Child pages)": "Подстраницы (вложенные страницы)",
|
||||
"List all subpages of the current page": "Показать все под страницы",
|
||||
"List all subpages of the current page": "Показать все подстраницы текущей страницы",
|
||||
"Attachments": "Вложения",
|
||||
"All spaces": "Все пространства",
|
||||
"Unknown": "Неизвестно",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Удалить поставщика SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Вы уверены, что хотите удалить этого поставщика SSO?",
|
||||
"Action": "Действие",
|
||||
"{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}",
|
||||
"Icon": "Иконка",
|
||||
"Upload image": "Загрузить изображение",
|
||||
"Remove image": "Удалить изображение",
|
||||
"Failed to remove image": "Не удалось удалить изображение",
|
||||
"Image exceeds 10MB limit.": "Изображение превышает предел 10MB.",
|
||||
"Image removed successfully": "Изображение успешно удалено",
|
||||
"API key": "API ключ",
|
||||
"API key created successfully": "API ключ успешно создан",
|
||||
"API keys": "API ключи",
|
||||
"API management": "Управление API",
|
||||
"Are you sure you want to revoke this API key": "Вы уверены, что хотите отозвать этот API ключ",
|
||||
"Create API Key": "Создать API ключ",
|
||||
"Custom expiration date": "Пользовательская дата срока действия",
|
||||
"Enter a descriptive token name": "Введите понятное имя токена",
|
||||
"Expiration": "Срок действия",
|
||||
"Expired": "Истек",
|
||||
"Expires": "Истекает",
|
||||
"I've saved my API key": "Я сохранил мой API ключ",
|
||||
"Last use": "Последнее использование",
|
||||
"No API keys found": "API ключи не найдены",
|
||||
"No expiration": "Не истекает",
|
||||
"Revoke API key": "Отозвать API ключ",
|
||||
"Revoked successfully": "Отозван успешно",
|
||||
"Select expiration date": "Выберете срок действия",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Это действие необратимо. Любые приложения, использующие этот API ключ, перестанут работать.",
|
||||
"Update API key": "Обновить API ключ",
|
||||
"Manage API keys for all users in the workspace": "Управлять API ключами для всех пользователей в рабочей области",
|
||||
"AI settings": "Настройки ИИ",
|
||||
"AI search": "Поиск ИИ",
|
||||
"AI Answer": "Ответ ИИ",
|
||||
"Ask AI": "Спросить ИИ",
|
||||
"AI is thinking...": "ИИ обрабатывает запрос...",
|
||||
"Ask a question...": "Задайте вопрос...",
|
||||
"AI-powered search (Ask AI)": "Поиск на базе ИИ (Спросить ИИ)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Поиск ИИ использует векторные встраивания для обеспечения семантического поиска по содержимому вашего рабочего пространства.",
|
||||
"Toggle AI search": "Переключить поиск ИИ",
|
||||
"Sources": "Источники",
|
||||
"Ask AI not available for attachments": "Функция \"Спросить ИИ\" недоступна для вложений",
|
||||
"No answer available": "Ответ недоступен",
|
||||
"Background color": "Цвет фона",
|
||||
"Highlight color": "Цвет выделения",
|
||||
"Remove color": "Удалить цвет"
|
||||
"{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "Оберіть бажану мову інтерфейсу.",
|
||||
"Choose your preferred page width.": "Оберіть бажану ширину сторінки.",
|
||||
"Confirm": "Підтвердити",
|
||||
"Copy as Markdown": "Скопіювати як Markdown",
|
||||
"Copy link": "Копіювати посилання",
|
||||
"Create": "Створити",
|
||||
"Create group": "Створити групу",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "сторінка",
|
||||
"Page deleted successfully": "Сторінку успішно видалено",
|
||||
"Page history": "Історія сторінки",
|
||||
"Select version": "Вибрати версію",
|
||||
"Highlight changes": "Підсвітити зміни",
|
||||
"Page import is in progress. Please do not close this tab.": "Імпорт сторінки в процесі. Будь ласка, не закривайте цю вкладку.",
|
||||
"Pages": "Сторінки",
|
||||
"pages": "сторінки",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "Експортування не вдалося:",
|
||||
"export error": "помилка експорту",
|
||||
"Export page": "Експорт сторінки",
|
||||
"Export successful": "Експорт виконано успішно",
|
||||
"Export space": "Експорт простору",
|
||||
"Export {{type}}": "Експорт {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.",
|
||||
"Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.",
|
||||
"Upload any file from your device.": "Завантажити будь-який файл з вашого пристрою.",
|
||||
"Uploading {{name}}": "Завантаження {{name}}",
|
||||
"Uploading file": "Завантаження файлу",
|
||||
"Table": "Таблиця",
|
||||
"Insert a table.": "Вставити таблицю.",
|
||||
"Insert collapsible block.": "Вставити блок, що згортається.",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "Спільний доступ успішно видалено",
|
||||
"Share not found": "Спільний доступ не знайдено",
|
||||
"Failed to share page": "Не вдалося поділитися сторінкою",
|
||||
"Disable public sharing": "Вимкнути публічний доступ",
|
||||
"Prevent members from sharing pages publicly.": "Перешкодити учасникам публічно ділитися сторінками.",
|
||||
"Toggle public sharing": "Перемикання публічного доступу",
|
||||
"Toggle space public sharing": "Перемикання публічного доступу до просторів",
|
||||
"Public sharing is disabled at the workspace level": "Публічний доступ вимкнуто на рівні робочого простору",
|
||||
"Prevent pages in this space from being shared publicly.": "Перешкодити публічному поширенню сторінок у цьому просторі.",
|
||||
"Requires an enterprise license": "Потребує корпоративної ліцензії",
|
||||
"Enable public sharing": "Увімкнути публічний доступ",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Ви впевнені, що хочете увімкнути публічний доступ? Учасники зможуть публічно ділитися сторінками.",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Ви впевнені, що хочете вимкнути публічний доступ? Усі існуючі посилання для спільного доступу в цьому робочому просторі будуть видалені.",
|
||||
"Are you sure you want to enable public sharing for this space?": "Ви впевнені, що хочете увімкнути публічний доступ для цього простору?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Ви впевнені, що хочете вимкнути публічний доступ? Усі існуючі посилання для спільного доступу в цьому просторі будуть видалені.",
|
||||
"Public sharing is disabled": "Публічний доступ вимкнуто",
|
||||
"Public sharing has been disabled at the workspace level.": "Публічний доступ було вимкнено на рівні робочого простору.",
|
||||
"Public sharing has been disabled for this space.": "Публічний доступ було вимкнено для цього простору.",
|
||||
"Copy page": "Копіювати сторінки",
|
||||
"Copy page to a different space.": "Скопіювати сторінку в інший простір.",
|
||||
"Page copied successfully": "Сторінку успішно скопійовано",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "Видалити постачальника SSO",
|
||||
"Are you sure you want to delete this SSO provider?": "Ви впевнені, що хочете видалити цього постачальника SSO?",
|
||||
"Action": "Дія",
|
||||
"{{ssoProviderType}} configuration": "Конфігурація {{ssoProviderType}}",
|
||||
"Icon": "Іконка",
|
||||
"Upload image": "Завантажити зображення",
|
||||
"Remove image": "Видалити зображення",
|
||||
"Failed to remove image": "Не вдалося видалити зображення",
|
||||
"Image exceeds 10MB limit.": "Зображення має займати менше, ніж 10 МБ.",
|
||||
"Image removed successfully": "Зображення видалено",
|
||||
"API key": "Ключ API",
|
||||
"API key created successfully": "Ключ API успішно створено",
|
||||
"API keys": "Ключі API",
|
||||
"API management": "Управління API",
|
||||
"Are you sure you want to revoke this API key": "Ви впевнені, що хочете відкликати цей ключ API",
|
||||
"Create API Key": "Створити ключ API",
|
||||
"Custom expiration date": "Користувацька дата закінчення",
|
||||
"Enter a descriptive token name": "Введіть описову назву токена",
|
||||
"Expiration": "Термін дії",
|
||||
"Expired": "Закінчився",
|
||||
"Expires": "Закінчується",
|
||||
"I've saved my API key": "Я зберіг свій ключ API",
|
||||
"Last use": "Останнє використання",
|
||||
"No API keys found": "Ключі API не знайдено",
|
||||
"No expiration": "Без терміну дії",
|
||||
"Revoke API key": "Відкликати ключ API",
|
||||
"Revoked successfully": "Успішно відкликано",
|
||||
"Select expiration date": "Виберіть дату закінчення",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "Цю дію не можна скасувати. Будь-які додатки, що використовують цей ключ API, перестануть працювати.",
|
||||
"Update API key": "Оновити ключ API",
|
||||
"Manage API keys for all users in the workspace": "Керувати ключами API для всіх користувачів у робочій області",
|
||||
"AI settings": "Налаштування ШІ",
|
||||
"AI search": "Пошук з ШІ",
|
||||
"AI Answer": "Відповідь ШІ",
|
||||
"Ask AI": "Запитати ШІ",
|
||||
"AI is thinking...": "ШІ думає...",
|
||||
"Ask a question...": "Задайте питання...",
|
||||
"AI-powered search (Ask AI)": "Пошук на базі ШІ (Запитати ШІ)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Пошук з ШІ використовує векторні вбудовування для надання можливостей семантичного пошуку у вашому робочому вмісті.",
|
||||
"Toggle AI search": "Переключити пошук з ШІ",
|
||||
"Sources": "Джерела",
|
||||
"Ask AI not available for attachments": "Запитати ШІ недоступно для вкладень",
|
||||
"No answer available": "Відповідь недоступна",
|
||||
"Background color": "Колір фону",
|
||||
"Highlight color": "Колір підсвічування",
|
||||
"Remove color": "Видалити колір"
|
||||
"{{ssoProviderType}} configuration": "Конфігурація {{ssoProviderType}}"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"Choose your preferred interface language.": "选择您喜欢的界面语言。",
|
||||
"Choose your preferred page width.": "选择您喜欢的页面宽度。",
|
||||
"Confirm": "确认",
|
||||
"Copy as Markdown": "复制为Markdown",
|
||||
"Copy link": "复制链接",
|
||||
"Create": "创建",
|
||||
"Create group": "创建群组",
|
||||
@@ -123,8 +122,6 @@
|
||||
"page": "个页面",
|
||||
"Page deleted successfully": "页面已成功删除",
|
||||
"Page history": "页面历史",
|
||||
"Select version": "选择版本",
|
||||
"Highlight changes": "突出显示更改",
|
||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||
"Pages": "页面",
|
||||
"pages": "个页面",
|
||||
@@ -256,7 +253,6 @@
|
||||
"Export failed:": "导出失败:",
|
||||
"export error": "导出出错",
|
||||
"Export page": "导出页面",
|
||||
"Export successful": "导出成功",
|
||||
"Export space": "导出空间",
|
||||
"Export {{type}}": "导出为 {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
|
||||
@@ -332,8 +328,6 @@
|
||||
"Upload any image from your device.": "从设备上传任何图像",
|
||||
"Upload any video from your device.": "从设备上传任何视频",
|
||||
"Upload any file from your device.": "从设备上传任何文件",
|
||||
"Uploading {{name}}": "正在上传{{name}}",
|
||||
"Uploading file": "正在上传文件",
|
||||
"Table": "表格",
|
||||
"Insert a table.": "插入一个表格",
|
||||
"Insert collapsible block.": "插入一个折叠块",
|
||||
@@ -407,21 +401,6 @@
|
||||
"Share deleted successfully": "分享已成功删除",
|
||||
"Share not found": "未找到分享",
|
||||
"Failed to share page": "页面分享失败",
|
||||
"Disable public sharing": "禁用公开分享",
|
||||
"Prevent members from sharing pages publicly.": "阻止成员公开分享页面。",
|
||||
"Toggle public sharing": "切换公开分享",
|
||||
"Toggle space public sharing": "切换空间公开分享",
|
||||
"Public sharing is disabled at the workspace level": "公开分享在工作区级别被禁用",
|
||||
"Prevent pages in this space from being shared publicly.": "阻止此空间中的页面被公开分享。",
|
||||
"Requires an enterprise license": "需要企业许可证",
|
||||
"Enable public sharing": "启用公开分享",
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "您确定要启用公开分享吗?成员将能够公开分享页面。",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "您确定要禁用公开分享吗?此工作区中的所有现有共享链接都将被删除。",
|
||||
"Are you sure you want to enable public sharing for this space?": "您确定要为此空间启用公开分享吗?",
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "您确定要禁用公开分享吗?此空间中的所有现有共享链接都将被删除。",
|
||||
"Public sharing is disabled": "公开分享已被禁用",
|
||||
"Public sharing has been disabled at the workspace level.": "公开分享已在工作区级别被禁用。",
|
||||
"Public sharing has been disabled for this space.": "此空间的公开分享已被禁用。",
|
||||
"Copy page": "复制页面",
|
||||
"Copy page to a different space.": "将页面复制到不同的空间。",
|
||||
"Page copied successfully": "页面复制成功",
|
||||
@@ -548,47 +527,5 @@
|
||||
"Delete SSO provider": "删除SSO提供商",
|
||||
"Are you sure you want to delete this SSO provider?": "您确定要删除此SSO提供商吗?",
|
||||
"Action": "操作",
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置",
|
||||
"Icon": "图标",
|
||||
"Upload image": "上传图片",
|
||||
"Remove image": "删除图片",
|
||||
"Failed to remove image": "无法删除图片",
|
||||
"Image exceeds 10MB limit.": "图片超过10MB限制。",
|
||||
"Image removed successfully": "图片删除成功",
|
||||
"API key": "API密钥",
|
||||
"API key created successfully": "API密钥创建成功",
|
||||
"API keys": "API密钥",
|
||||
"API management": "API管理",
|
||||
"Are you sure you want to revoke this API key": "确定要撤销此API密钥吗",
|
||||
"Create API Key": "创建API密钥",
|
||||
"Custom expiration date": "自定义到期日期",
|
||||
"Enter a descriptive token name": "输入描述性令牌名称",
|
||||
"Expiration": "到期",
|
||||
"Expired": "已过期",
|
||||
"Expires": "到期",
|
||||
"I've saved my API key": "我已保存我的API密钥",
|
||||
"Last use": "上次使用",
|
||||
"No API keys found": "找不到API密钥",
|
||||
"No expiration": "无到期",
|
||||
"Revoke API key": "撤销API密钥",
|
||||
"Revoked successfully": "撤销成功",
|
||||
"Select expiration date": "选择到期日期",
|
||||
"This action cannot be undone. Any applications using this API key will stop working.": "此操作无法撤销。使用此API密钥的任何应用程序将停止工作。",
|
||||
"Update API key": "更新API密钥",
|
||||
"Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥",
|
||||
"AI settings": "AI设置",
|
||||
"AI search": "AI搜索",
|
||||
"AI Answer": "AI回答",
|
||||
"Ask AI": "询问AI",
|
||||
"AI is thinking...": "AI正在思考...",
|
||||
"Ask a question...": "提问...",
|
||||
"AI-powered search (Ask AI)": "AI驱动的搜索(询问AI)",
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI搜索使用向量嵌入提供跨工作空间内容的语义搜索功能。",
|
||||
"Toggle AI search": "切换AI搜索",
|
||||
"Sources": "来源",
|
||||
"Ask AI not available for attachments": "附件不支持询问AI",
|
||||
"No answer available": "无可用答案",
|
||||
"Background color": "背景颜色",
|
||||
"Highlight color": "突出显示颜色",
|
||||
"Remove color": "移除颜色"
|
||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
||||
import SpaceTrash from "@/pages/space/space-trash.tsx";
|
||||
import UserApiKeys from "@/ee/api-key/pages/user-api-keys";
|
||||
import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys";
|
||||
import AiSettings from "@/ee/ai/pages/ai-settings.tsx";
|
||||
|
||||
export default function App() {
|
||||
const { t } = useTranslation();
|
||||
@@ -108,7 +107,6 @@ export default function App() {
|
||||
<Route path={"spaces"} element={<Spaces />} />
|
||||
<Route path={"sharing"} element={<Shares />} />
|
||||
<Route path={"security"} element={<Security />} />
|
||||
<Route path={"ai"} element={<AiSettings />} />
|
||||
{!isCloud() && <Route path={"license"} element={<License />} />}
|
||||
{isCloud() && <Route path={"billing"} element={<Billing />} />}
|
||||
</Route>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/core/src/components/CopyButton/CopyButton.tsx - MIT
|
||||
// modified to use the polyfilled clipboard api
|
||||
import React from "react";
|
||||
import { useClipboard } from "@/hooks/use-clipboard";
|
||||
import { useProps } from "@mantine/core";
|
||||
|
||||
interface CopyButtonProps {
|
||||
/** Children callback, provides current status and copy function as an argument */
|
||||
children: (payload: { copied: boolean; copy: () => void }) => React.ReactNode;
|
||||
|
||||
/** Value that is copied to the clipboard when the button is clicked */
|
||||
value: string;
|
||||
|
||||
/** Copied status timeout in ms @default `1000` */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
timeout: 1000,
|
||||
} satisfies Partial<CopyButtonProps>;
|
||||
|
||||
export function CopyButton(props: CopyButtonProps) {
|
||||
const { children, timeout, value, ...others } = useProps(
|
||||
"CopyButton",
|
||||
defaultProps,
|
||||
props,
|
||||
);
|
||||
const clipboard = useClipboard({ timeout });
|
||||
const copy = () => clipboard.copy(value);
|
||||
return <>{children({ copy, copied: clipboard.copied, ...others })}</>;
|
||||
}
|
||||
|
||||
CopyButton.displayName = "@mantine/core/CopyButton";
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -30,11 +30,9 @@ export default function ExportModal({
|
||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
|
||||
const [isExporting, setIsExporting] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (type === "page") {
|
||||
await exportPage({
|
||||
@@ -47,9 +45,6 @@ export default function ExportModal({
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
}
|
||||
notifications.show({
|
||||
message: t("Export successful"),
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
@@ -57,8 +52,6 @@ export default function ExportModal({
|
||||
color: "red",
|
||||
});
|
||||
console.error("export error", err);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,7 +136,7 @@ export default function ExportModal({
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button>
|
||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface PagePaginationProps {
|
||||
currentPage: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onPageChange: (newPage: number) => void;
|
||||
}
|
||||
|
||||
export default function Paginate({
|
||||
currentPage,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPrev,
|
||||
onNext,
|
||||
onPageChange,
|
||||
}: PagePaginationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Paginate({
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={onPrev}
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
{t("Prev")}
|
||||
@@ -34,7 +34,7 @@ export default function Paginate({
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={onNext}
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
{t("Next")}
|
||||
|
||||
@@ -5,27 +5,26 @@ import {
|
||||
Badge,
|
||||
Table,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { formattedDate } from "@/lib/time.ts";
|
||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
} from '@mantine/core';
|
||||
import {Link} from 'react-router-dom';
|
||||
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
|
||||
import { buildPageUrl } from '@/features/page/page.utils.ts';
|
||||
import { formattedDate } from '@/lib/time.ts';
|
||||
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
||||
import { IconFileDescription } from '@tabler/icons-react';
|
||||
import { getSpaceUrl } from '@/lib/config.ts';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getInitialsColor } from "@/lib/get-initials-color.ts";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
export default function RecentChanges({ spaceId }: Props) {
|
||||
export default function RecentChanges({spaceId}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
|
||||
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
return <PageListSkeleton/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -45,8 +44,8 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
{page.icon || (
|
||||
<ActionIcon variant="transparent" color="gray" size={18}>
|
||||
<IconFileDescription size={18} />
|
||||
<ActionIcon variant='transparent' color='gray' size={18}>
|
||||
<IconFileDescription size={18}/>
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
@@ -59,23 +58,18 @@ export default function RecentChanges({ spaceId }: Props) {
|
||||
{!spaceId && (
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={getInitialsColor(page?.space.name)}
|
||||
color="blue"
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Text
|
||||
c="dimmed"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
size="xs"
|
||||
fw={500}
|
||||
>
|
||||
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getShares } from "@/features/share/services/share-service.ts";
|
||||
import { getApiKeys } from "@/ee/api-key";
|
||||
|
||||
export const prefetchWorkspaceMembers = () => {
|
||||
const params: QueryParams = { limit: 100, query: "" };
|
||||
const params = { limit: 100, page: 1, query: "" } as QueryParams;
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["workspaceMembers", params],
|
||||
queryFn: () => getWorkspaceMembers(params),
|
||||
@@ -22,15 +22,15 @@ export const prefetchWorkspaceMembers = () => {
|
||||
|
||||
export const prefetchSpaces = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["spaces", {}],
|
||||
queryFn: () => getSpaces({}),
|
||||
queryKey: ["spaces", { page: 1 }],
|
||||
queryFn: () => getSpaces({ page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchGroups = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["groups", {}],
|
||||
queryFn: () => getGroups({}),
|
||||
queryKey: ["groups", { page: 1 }],
|
||||
queryFn: () => getGroups({ page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -62,21 +62,21 @@ export const prefetchSsoProviders = () => {
|
||||
|
||||
export const prefetchShares = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["share-list", {}],
|
||||
queryFn: () => getShares({}),
|
||||
queryKey: ["share-list", { page: 1 }],
|
||||
queryFn: () => getShares({ page: 1, limit: 100 }),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchApiKeys = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["api-key-list", {}],
|
||||
queryFn: () => getApiKeys({}),
|
||||
queryKey: ["api-key-list", { page: 1 }],
|
||||
queryFn: () => getApiKeys({ page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchApiKeyManagement = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["api-key-list", { adminView: true }],
|
||||
queryFn: () => getApiKeys({ adminView: true }),
|
||||
queryKey: ["api-key-list", { page: 1 }],
|
||||
queryFn: () => getApiKeys({ page: 1, adminView: true }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,14 +12,13 @@ import {
|
||||
IconLock,
|
||||
IconKey,
|
||||
IconWorld,
|
||||
IconSparkles,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import {
|
||||
prefetchApiKeyManagement,
|
||||
@@ -110,13 +109,6 @@ const groupedData: DataGroup[] = [
|
||||
isAdmin: true,
|
||||
showDisabledInNonEE: true,
|
||||
},
|
||||
{
|
||||
label: "AI settings",
|
||||
icon: IconSparkles,
|
||||
path: "/settings/ai",
|
||||
isAdmin: true,
|
||||
isSelfhosted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useRef, useState, ReactNode } from "react";
|
||||
import { Text, TextProps, Tooltip } from "@mantine/core";
|
||||
|
||||
type AutoTooltipTextProps = TextProps & {
|
||||
children: ReactNode;
|
||||
tooltipLabel?: string;
|
||||
tooltipProps?: Omit<
|
||||
React.ComponentProps<typeof Tooltip>,
|
||||
"children" | "label"
|
||||
>;
|
||||
};
|
||||
|
||||
export function AutoTooltipText({
|
||||
children,
|
||||
tooltipLabel,
|
||||
tooltipProps,
|
||||
...textProps
|
||||
}: AutoTooltipTextProps) {
|
||||
const textRef = useRef<HTMLParagraphElement>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const element = textRef.current;
|
||||
if (element) {
|
||||
setIsTruncated(element.scrollWidth > element.clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const label = tooltipLabel ?? (typeof children === "string" ? children : "");
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={label}
|
||||
disabled={!isTruncated || !label}
|
||||
multiline
|
||||
withArrow
|
||||
{...tooltipProps}
|
||||
>
|
||||
<Text
|
||||
ref={textRef}
|
||||
truncate
|
||||
onMouseEnter={handleMouseEnter}
|
||||
{...textProps}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Paper, Text, Group, Stack, Loader, Box } from "@mantine/core";
|
||||
import { IconSparkles, IconFileText } from "@tabler/icons-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IAiSearchResponse } from "../services/ai-search-service.ts";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { markdownToHtml } from "@docmost/editor-ext";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AiSearchResultProps {
|
||||
result?: IAiSearchResponse;
|
||||
isLoading?: boolean;
|
||||
streamingAnswer?: string;
|
||||
streamingSources?: any[];
|
||||
}
|
||||
|
||||
export function AiSearchResult({
|
||||
result,
|
||||
isLoading,
|
||||
streamingAnswer = "",
|
||||
streamingSources = [],
|
||||
}: AiSearchResultProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Use streaming data if available, otherwise fall back to result
|
||||
const answer = streamingAnswer || result?.answer || "";
|
||||
const sources =
|
||||
streamingSources.length > 0 ? streamingSources : result?.sources || [];
|
||||
|
||||
// Deduplicate sources by pageId, keeping the one with highest similarity
|
||||
const deduplicatedSources = useMemo(() => {
|
||||
if (!sources || sources.length === 0) return [];
|
||||
|
||||
const pageMap = new Map();
|
||||
sources.forEach((source) => {
|
||||
const existing = pageMap.get(source.pageId);
|
||||
if (!existing || source.similarity > existing.similarity) {
|
||||
pageMap.set(source.pageId, source);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pageMap.values());
|
||||
}, [sources]);
|
||||
|
||||
if (isLoading && !answer) {
|
||||
return (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Group>
|
||||
<Loader size="sm" />
|
||||
<Text size="sm">{t("AI is thinking...")}</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!answer && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md" p="md">
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Group gap="xs" mb="sm">
|
||||
<IconSparkles size={20} color="var(--mantine-color-blue-6)" />
|
||||
<Text fw={600} size="sm">
|
||||
{t("AI Answer")}
|
||||
</Text>
|
||||
{isLoading && <Loader size="xs" />}
|
||||
</Group>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(markdownToHtml(answer) as string),
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{deduplicatedSources.length > 0 && (
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={600} c="dimmed">
|
||||
{t("Sources")}
|
||||
</Text>
|
||||
{deduplicatedSources.map((source) => (
|
||||
<Box
|
||||
key={source.pageId}
|
||||
component={Link}
|
||||
to={buildPageUrl(source.spaceSlug, source.slugId, source.title)}
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
p="xs"
|
||||
radius="sm"
|
||||
withBorder
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Group gap="xs">
|
||||
<IconFileText size={16} />
|
||||
<Text size="sm" truncate>
|
||||
{source.title}
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Group, Text, Switch, MantineSize, Title } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
|
||||
export default function EnableAiSearch() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("AI-powered search (Ask AI)")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<AiSearchToggle />
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface AiSearchToggleProps {
|
||||
size?: MantineSize;
|
||||
label?: string;
|
||||
}
|
||||
export function AiSearchToggle({ size, label }: AiSearchToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.settings?.ai?.search);
|
||||
const { hasLicenseKey } = useLicense();
|
||||
|
||||
const hasAccess = isCloud() || (!isCloud() && hasLicenseKey);
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({ aiSearch: value });
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle AI search")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { useState, useCallback } from "react";
|
||||
import { askAi, IAiSearchResponse } from "@/ee/ai/services/ai-search-service.ts";
|
||||
import { IPageSearchParams } from "@/features/search/types/search.types.ts";
|
||||
|
||||
// @ts-ignore
|
||||
interface UseAiSearchResult extends UseMutationResult<IAiSearchResponse, Error, IPageSearchParams> {
|
||||
streamingAnswer: string;
|
||||
streamingSources: any[];
|
||||
clearStreaming: () => void;
|
||||
}
|
||||
|
||||
export function useAiSearch(): UseAiSearchResult {
|
||||
const [streamingAnswer, setStreamingAnswer] = useState("");
|
||||
const [streamingSources, setStreamingSources] = useState<any[]>([]);
|
||||
|
||||
const clearStreaming = useCallback(() => {
|
||||
setStreamingAnswer("");
|
||||
setStreamingSources([]);
|
||||
}, []);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (params: IPageSearchParams & { contentType?: string }) => {
|
||||
setStreamingAnswer("");
|
||||
setStreamingSources([]);
|
||||
|
||||
const { contentType, ...apiParams } = params;
|
||||
|
||||
return await askAi(apiParams, (chunk) => {
|
||||
if (chunk.content) {
|
||||
setStreamingAnswer((prev) => prev + chunk.content);
|
||||
}
|
||||
if (chunk.sources) {
|
||||
setStreamingSources(chunk.sources);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
streamingAnswer,
|
||||
streamingSources,
|
||||
clearStreaming,
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts";
|
||||
import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts";
|
||||
|
||||
export function useAiStream() {
|
||||
const [content, setContent] = useState("");
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const mutation = useAiGenerateStreamMutation();
|
||||
|
||||
const startStream = useCallback(
|
||||
async (data: AiGenerateDto) => {
|
||||
setContent("");
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
const controller = await mutation.mutateAsync({
|
||||
...data,
|
||||
onChunk: (chunk) => {
|
||||
setContent((prev) => prev + chunk.content);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("AI stream error:", error);
|
||||
setIsStreaming(false);
|
||||
},
|
||||
onComplete: () => {
|
||||
setIsStreaming(false);
|
||||
},
|
||||
});
|
||||
|
||||
abortControllerRef.current = controller;
|
||||
} catch (error) {
|
||||
console.error("Failed to start stream:", error);
|
||||
setIsStreaming(false);
|
||||
}
|
||||
},
|
||||
[mutation]
|
||||
);
|
||||
|
||||
const stopStream = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsStreaming(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetContent = useCallback(() => {
|
||||
setContent("");
|
||||
}, []);
|
||||
|
||||
return {
|
||||
content,
|
||||
isStreaming,
|
||||
startStream,
|
||||
stopStream,
|
||||
resetContent,
|
||||
isLoading: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getAppName, isCloud } from "@/lib/config.ts";
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import React from "react";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
|
||||
import { Alert } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
export default function AiSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const { hasLicenseKey } = useLicense();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAccess = isCloud() || (!isCloud() && hasLicenseKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>AI - {getAppName()}</title>
|
||||
</Helmet>
|
||||
<SettingsTitle title={t("AI settings")} />
|
||||
|
||||
{!hasAccess && (
|
||||
<Alert
|
||||
icon={<IconInfoCircle />}
|
||||
title={t("Enterprise feature")}
|
||||
color="blue"
|
||||
mb="lg"
|
||||
>
|
||||
{t(
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<EnableAiSearch />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import {
|
||||
useMutation,
|
||||
UseMutationResult,
|
||||
useQuery,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
generateAiContent,
|
||||
generateAiContentStream,
|
||||
} from "@/ee/ai/services/ai-service.ts";
|
||||
import {
|
||||
AiConfigResponse,
|
||||
AiContentResponse,
|
||||
AiGenerateDto,
|
||||
AiStreamChunk,
|
||||
AiStreamError,
|
||||
} from "@/ee/ai/types/ai.types.ts";
|
||||
|
||||
export function useAiGenerateMutation(): UseMutationResult<
|
||||
AiContentResponse,
|
||||
Error,
|
||||
AiGenerateDto
|
||||
> {
|
||||
return useMutation({
|
||||
mutationFn: (data: AiGenerateDto) => generateAiContent(data),
|
||||
});
|
||||
}
|
||||
|
||||
interface StreamCallbacks {
|
||||
onChunk: (chunk: AiStreamChunk) => void;
|
||||
onError?: (error: AiStreamError) => void;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function useAiGenerateStreamMutation(): UseMutationResult<
|
||||
AbortController,
|
||||
Error,
|
||||
AiGenerateDto & StreamCallbacks
|
||||
> {
|
||||
return useMutation({
|
||||
mutationFn: ({ onChunk, onError, onComplete, ...data }) =>
|
||||
generateAiContentStream(data, onChunk, onError, onComplete),
|
||||
});
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import { IPageSearchParams } from "@/features/search/types/search.types.ts";
|
||||
|
||||
export interface IAiSearchResponse {
|
||||
answer: string;
|
||||
sources?: Array<{
|
||||
pageId: string;
|
||||
title: string;
|
||||
slugId: string;
|
||||
spaceSlug: string;
|
||||
similarity: number;
|
||||
distance: number;
|
||||
chunkIndex: number;
|
||||
excerpt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function askAi(
|
||||
params: IPageSearchParams,
|
||||
onChunk?: (chunk: { content?: string; sources?: any[] }) => void,
|
||||
): Promise<IAiSearchResponse> {
|
||||
const response = await fetch("/api/ai/ask", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let answer = "";
|
||||
let sources: any[] = [];
|
||||
let buffer = "";
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
// Keep the last incomplete line in the buffer
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6);
|
||||
if (data === "[DONE]") break;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.error) {
|
||||
throw new Error(parsed.error);
|
||||
}
|
||||
if (parsed.content) {
|
||||
answer += parsed.content;
|
||||
onChunk?.({ content: parsed.content });
|
||||
}
|
||||
if (parsed.sources) {
|
||||
sources = parsed.sources;
|
||||
onChunk?.({ sources: parsed.sources });
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw e;
|
||||
}
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { answer, sources };
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import {
|
||||
AiGenerateDto,
|
||||
AiContentResponse,
|
||||
AiStreamChunk,
|
||||
AiStreamError,
|
||||
} from "@/ee/ai/types/ai.types.ts";
|
||||
|
||||
export async function generateAiContent(
|
||||
data: AiGenerateDto,
|
||||
): Promise<AiContentResponse> {
|
||||
const req = await api.post<AiContentResponse>("/ai/generate", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function generateAiContentStream(
|
||||
data: AiGenerateDto,
|
||||
onChunk: (chunk: AiStreamChunk) => void,
|
||||
onError?: (error: AiStreamError) => void,
|
||||
onComplete?: () => void,
|
||||
): Promise<AbortController> {
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
const response = await fetch("/api/ai/generate/stream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: abortController.signal,
|
||||
credentials: "include", // This ensures cookies are sent, matching axios withCredentials
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const processStream = async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6);
|
||||
if (data === "[DONE]") {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.error) {
|
||||
onError?.(parsed);
|
||||
} else {
|
||||
onChunk(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors for incomplete chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== "AbortError") {
|
||||
onError?.({ error: error.message });
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
};
|
||||
|
||||
processStream();
|
||||
} catch (error) {
|
||||
onError?.({ error: error.message });
|
||||
}
|
||||
|
||||
return abortController;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
export enum AiAction {
|
||||
IMPROVE_WRITING = "improve_writing",
|
||||
FIX_SPELLING_GRAMMAR = "fix_spelling_grammar",
|
||||
MAKE_SHORTER = "make_shorter",
|
||||
MAKE_LONGER = "make_longer",
|
||||
SIMPLIFY = "simplify",
|
||||
CHANGE_TONE = "change_tone",
|
||||
SUMMARIZE = "summarize",
|
||||
CONTINUE_WRITING = "continue_writing",
|
||||
TRANSLATE = "translate",
|
||||
CUSTOM = "custom",
|
||||
}
|
||||
|
||||
export interface AiGenerateDto {
|
||||
action?: AiAction;
|
||||
content: string;
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
export interface AiContentResponse {
|
||||
content: string;
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AiConfigResponse {
|
||||
configured: boolean;
|
||||
availableActions: AiAction[];
|
||||
}
|
||||
|
||||
export interface AiStreamChunk {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AiStreamError {
|
||||
error: string;
|
||||
}
|
||||
@@ -10,19 +10,19 @@ import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-moda
|
||||
import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
|
||||
import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
|
||||
import Paginate from "@/components/common/paginate";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
|
||||
import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
|
||||
import { IApiKey } from "@/ee/api-key";
|
||||
|
||||
export default function UserApiKeys() {
|
||||
const { t } = useTranslation();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { page, setPage } = usePaginateAndSearch();
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [revokeModalOpened, setRevokeModalOpened] = useState(false);
|
||||
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
|
||||
const { data, isLoading } = useGetApiKeysQuery({ cursor });
|
||||
const { data, isLoading } = useGetApiKeysQuery({ page });
|
||||
|
||||
const handleCreateSuccess = (response: IApiKey) => {
|
||||
setCreatedApiKey(response);
|
||||
@@ -65,10 +65,10 @@ export default function UserApiKeys() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,20 +10,20 @@ import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-moda
|
||||
import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
|
||||
import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
|
||||
import Paginate from "@/components/common/paginate";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
|
||||
import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
|
||||
import { IApiKey } from "@/ee/api-key";
|
||||
import useUserRole from '@/hooks/use-user-role.tsx';
|
||||
|
||||
export default function WorkspaceApiKeys() {
|
||||
const { t } = useTranslation();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { page, setPage } = usePaginateAndSearch();
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [revokeModalOpened, setRevokeModalOpened] = useState(false);
|
||||
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
|
||||
const { data, isLoading } = useGetApiKeysQuery({ cursor, adminView: true });
|
||||
const { data, isLoading } = useGetApiKeysQuery({ page, adminView: true });
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
if (!isAdmin) {
|
||||
@@ -76,10 +76,10 @@ export default function WorkspaceApiKeys() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { isCloud } from "@/lib/config";
|
||||
import useLicense from "@/ee/hooks/use-license";
|
||||
import usePlan from "@/ee/hooks/use-plan";
|
||||
|
||||
const useEnterpriseAccess = () => {
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const { isBusiness } = usePlan();
|
||||
|
||||
return (isCloud() && isBusiness) || (!isCloud() && hasLicenseKey);
|
||||
};
|
||||
|
||||
export default useEnterpriseAccess;
|
||||
@@ -11,7 +11,7 @@ export default function OssDetails() {
|
||||
withTableBorder
|
||||
>
|
||||
<Table.Caption>
|
||||
To unlock enterprise features like AI, SSO, MFA, Resolve comments, contact sales@docmost.com.
|
||||
To unlock enterprise features like SSO, MFA, Resolve comments, contact sales@docmost.com.
|
||||
</Table.Caption>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
Group,
|
||||
List,
|
||||
Code,
|
||||
CopyButton,
|
||||
Alert,
|
||||
PasswordInput,
|
||||
} from "@mantine/core";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import {
|
||||
IconRefresh,
|
||||
IconCopy,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PinInput,
|
||||
Alert,
|
||||
List,
|
||||
CopyButton,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
Paper,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
Collapse,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import {
|
||||
IconQrcode,
|
||||
IconShieldCheck,
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
|
||||
export default function DisablePublicSharing() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Disable public sharing")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Prevent members from sharing pages publicly.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<DisablePublicSharingToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function DisablePublicSharingToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.sharing?.disabled === true,
|
||||
);
|
||||
const hasAccess = useEnterpriseAccess();
|
||||
|
||||
const applyChange = async (value: boolean) => {
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
disablePublicSharing: value,
|
||||
});
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
|
||||
modals.openConfirmModal({
|
||||
title: value ? t("Disable public sharing") : t("Enable public sharing"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
{value
|
||||
? t(
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.",
|
||||
)
|
||||
: t(
|
||||
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||
confirmProps: value ? { color: "red" } : {},
|
||||
onConfirm: () => applyChange(value),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={t("Requires an enterprise license")}
|
||||
disabled={hasAccess}
|
||||
refProp="rootRef"
|
||||
>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle public sharing")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -10,18 +10,23 @@ export default function EnforceMfa() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Enforce two-factor authentication")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<>
|
||||
<Title order={4} my="sm">
|
||||
MFA
|
||||
</Title>
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Enforce two-factor authentication")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Once enforced, all members must enable two-factor authentication to access the workspace.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<EnforceMfaToggle />
|
||||
</Group>
|
||||
<EnforceMfaToggle />
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
|
||||
type SpacePublicSharingToggleProps = {
|
||||
space: ISpace;
|
||||
};
|
||||
|
||||
export default function SpacePublicSharingToggle({
|
||||
space,
|
||||
}: SpacePublicSharingToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const workspaceDisabled = workspace?.settings?.sharing?.disabled === true;
|
||||
const [checked, setChecked] = useState(
|
||||
space.settings?.sharing?.disabled === true,
|
||||
);
|
||||
const updateSpaceMutation = useUpdateSpaceMutation();
|
||||
|
||||
const applyChange = async (value: boolean) => {
|
||||
try {
|
||||
await updateSpaceMutation.mutateAsync({
|
||||
spaceId: space.id,
|
||||
disablePublicSharing: value,
|
||||
});
|
||||
setChecked(value);
|
||||
} catch {
|
||||
// error handled by mutation
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
|
||||
modals.openConfirmModal({
|
||||
title: value ? t("Disable public sharing") : t("Enable public sharing"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
{value
|
||||
? t(
|
||||
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.",
|
||||
)
|
||||
: t(
|
||||
"Are you sure you want to enable public sharing for this space?",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||
confirmProps: value ? { color: "red" } : {},
|
||||
onConfirm: () => applyChange(value),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Disable public sharing")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{workspaceDisabled
|
||||
? t("Public sharing is disabled at the workspace level")
|
||||
: t("Prevent pages in this space from being shared publicly.")}
|
||||
</Text>
|
||||
</div>
|
||||
<Tooltip
|
||||
label={t("Public sharing is disabled at the workspace level")}
|
||||
disabled={!workspaceDisabled}
|
||||
refProp="rootRef"
|
||||
>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={workspaceDisabled}
|
||||
aria-label={t("Toggle space public sharing")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export default function SsoProviderList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data?.items.length === 0) {
|
||||
if (data?.length === 0) {
|
||||
return <Text c="dimmed">{t("No SSO providers found.")}</Text>;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function SsoProviderList() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data?.items
|
||||
{data
|
||||
.sort((a, b) => {
|
||||
const enabledDiff = Number(b.isEnabled) - Number(a.isEnabled);
|
||||
if (enabledDiff !== 0) return enabledDiff;
|
||||
@@ -104,11 +104,7 @@ export default function SsoProviderList() {
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={"gray"}
|
||||
variant="light"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Badge color={"gray"} variant="light" style={{ whiteSpace: "nowrap" }}>
|
||||
{provider.type.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
@@ -138,41 +134,41 @@ export default function SsoProviderList() {
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => handleEdit(provider)}
|
||||
>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop" }}
|
||||
withArrow
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleEdit(provider)}
|
||||
leftSection={<IconPencil size={16} />}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDeleteModal(provider.id)}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
disabled={provider.type === SSO_PROVIDER.GOOGLE}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => handleEdit(provider)}
|
||||
>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop" }}
|
||||
withArrow
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleEdit(provider)}
|
||||
leftSection={<IconPencil size={16} />}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDeleteModal(provider.id)}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
disabled={provider.type === SSO_PROVIDER.GOOGLE}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -9,16 +9,15 @@ import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx"
|
||||
import EnforceSso from "@/ee/security/components/enforce-sso.tsx";
|
||||
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
import usePlan from "@/ee/hooks/use-plan.tsx";
|
||||
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
||||
import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
|
||||
export default function Security() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const hasEnterpriseAccess = useEnterpriseAccess();
|
||||
const isCloudEE = useIsCloudEE();
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const { isBusiness } = usePlan();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
@@ -31,41 +30,26 @@ export default function Security() {
|
||||
</Helmet>
|
||||
<SettingsTitle title={t("Security")} />
|
||||
|
||||
<EnforceMfa />
|
||||
<AllowedDomains />
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{(!isCloud() || hasEnterpriseAccess) && (
|
||||
<>
|
||||
<DisablePublicSharing />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
<EnforceMfa />
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Title order={4} my="lg">
|
||||
Single sign-on (SSO)
|
||||
</Title>
|
||||
|
||||
{hasEnterpriseAccess && (
|
||||
{(isCloud() && isBusiness) || (!isCloud() && hasLicenseKey) ? (
|
||||
<>
|
||||
<EnforceSso />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isCloudEE && (
|
||||
<>
|
||||
<AllowedDomains />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEnterpriseAccess && (
|
||||
<>
|
||||
<CreateSsoProvider />
|
||||
<Divider size={0} my="lg" />
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<SsoProviderList />
|
||||
</>
|
||||
|
||||
@@ -13,9 +13,8 @@ import {
|
||||
} from "@/ee/security/services/security-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
|
||||
export function useGetSsoProviders(): UseQueryResult<IPagination<IAuthProvider>, Error> {
|
||||
export function useGetSsoProviders(): UseQueryResult<IAuthProvider[], Error> {
|
||||
return useQuery({
|
||||
queryKey: ["sso-providers"],
|
||||
queryFn: () => getSsoProviders(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
|
||||
export async function getSsoProviderById(data: {
|
||||
providerId: string;
|
||||
@@ -9,8 +8,8 @@ export async function getSsoProviderById(data: {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getSsoProviders(): Promise<IPagination<IAuthProvider>> {
|
||||
const req = await api.post<IPagination<IAuthProvider>>("/sso/providers");
|
||||
export async function getSsoProviders(): Promise<IAuthProvider[]> {
|
||||
const req = await api.post<IAuthProvider[]>("/sso/providers");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,14 +84,9 @@ const CommentEditor = forwardRef(
|
||||
autofocus: (autofocus && "end") || false,
|
||||
});
|
||||
|
||||
// Sync content from props for read-only editors (e.g. when updated via
|
||||
// websocket on another browser). Skip for editable editors to avoid
|
||||
// resetting the cursor position on every keystroke.
|
||||
useEffect(() => {
|
||||
if (!editable && commentEditor && defaultContent) {
|
||||
commentEditor.commands.setContent(defaultContent);
|
||||
}
|
||||
}, [defaultContent, editable, commentEditor]);
|
||||
commentEditor.commands.setContent(defaultContent);
|
||||
}, [defaultContent]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Group, Text, Box, Badge } from "@mantine/core";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import classes from "./comment.module.css";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { timeAgo } from "@/lib/time";
|
||||
@@ -40,7 +40,6 @@ function CommentListItem({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const editor = useAtomValue(pageEditorAtom);
|
||||
const [content, setContent] = useState<string>(comment.content);
|
||||
const editContentRef = useRef<any>(null);
|
||||
const updateCommentMutation = useUpdateCommentMutation();
|
||||
const deleteCommentMutation = useDeleteCommentMutation(comment.pageId);
|
||||
const resolveCommentMutation = useResolveCommentMutation();
|
||||
@@ -57,13 +56,9 @@ function CommentListItem({
|
||||
setIsLoading(true);
|
||||
const commentToUpdate = {
|
||||
commentId: comment.id,
|
||||
content: JSON.stringify(editContentRef.current ?? content),
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
await updateCommentMutation.mutateAsync(commentToUpdate);
|
||||
if (editContentRef.current) {
|
||||
setContent(editContentRef.current);
|
||||
editContentRef.current = null;
|
||||
}
|
||||
setIsEditing(false);
|
||||
|
||||
emit({
|
||||
@@ -133,7 +128,6 @@ function CommentListItem({
|
||||
setIsEditing(true);
|
||||
}
|
||||
function cancelEdit() {
|
||||
editContentRef.current = null;
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
@@ -200,7 +194,7 @@ function CommentListItem({
|
||||
<CommentEditor
|
||||
defaultContent={content}
|
||||
editable={true}
|
||||
onUpdate={(newContent: any) => { editContentRef.current = newContent; }}
|
||||
onUpdate={(newContent: any) => setContent(newContent)}
|
||||
onSave={handleUpdateComment}
|
||||
autofocus={true}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Group, Text, Paper, ActionIcon, Loader } from "@mantine/core";
|
||||
import { Group, Text, Paper, ActionIcon } from "@mantine/core";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import { IconDownload, IconPaperclip } from "@tabler/icons-react";
|
||||
import { useHover } from "@mantine/hooks";
|
||||
import { formatBytes } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AttachmentView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, selected } = props;
|
||||
const { url, name, size } = node.attrs;
|
||||
const { hovered, ref } = useHover();
|
||||
@@ -22,28 +20,26 @@ export default function AttachmentView(props: NodeViewProps) {
|
||||
wrap="nowrap"
|
||||
h={25}
|
||||
>
|
||||
<Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}>
|
||||
{url ? (
|
||||
<IconPaperclip size={20} style={{ flexShrink: 0 }} />
|
||||
) : (
|
||||
<Loader size={20} style={{ flexShrink: 0 }} />
|
||||
)}
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<IconPaperclip size={20} />
|
||||
|
||||
<Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}>
|
||||
{url ? name : t("Uploading {{name}}", { name })}
|
||||
<Text component="span" size="md" truncate="end">
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
<Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}>
|
||||
<Text component="span" size="sm" c="dimmed" inline>
|
||||
{formatBytes(size)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{url && (selected || hovered) && (
|
||||
{selected || hovered ? (
|
||||
<a href={getFileUrl(url)} target="_blank">
|
||||
<ActionIcon variant="default" aria-label="download file">
|
||||
<IconDownload size={18} />
|
||||
</ActionIcon>
|
||||
</a>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus";
|
||||
import { isNodeSelection, useEditorState } from "@tiptap/react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu,
|
||||
BubbleMenuProps,
|
||||
isNodeSelection,
|
||||
useEditor,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
IconBold,
|
||||
@@ -34,7 +38,7 @@ export interface BubbleMenuItem {
|
||||
}
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
editor: Editor | null;
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
};
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
@@ -129,25 +133,30 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
}
|
||||
return isTextSelected(editor);
|
||||
},
|
||||
options: {
|
||||
placement: "top",
|
||||
offset: 8,
|
||||
tippyOptions: {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
onCreate: (instance) => {
|
||||
instance.popper.firstChild?.addEventListener("blur", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
},
|
||||
onHide: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<BubbleMenu {...bubbleMenuProps} style={{ zIndex: 200, position: "relative"}}>
|
||||
<BubbleMenu {...bubbleMenuProps}>
|
||||
<div className={classes.bubbleMenu}>
|
||||
<NodeSelector
|
||||
editor={props.editor}
|
||||
@@ -155,8 +164,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -166,8 +175,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
setIsOpen={() => {
|
||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from "react";
|
||||
import { IconCheck, IconChevronDown, IconPalette } from "@tabler/icons-react";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { IconCheck, IconPalette } from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
ScrollArea,
|
||||
Text,
|
||||
Tooltip,
|
||||
SimpleGrid,
|
||||
Box,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import { useEditorState } from "@tiptap/react";
|
||||
@@ -64,12 +61,9 @@ const TEXT_COLORS: BubbleColorMenuItem[] = [
|
||||
name: "Gray",
|
||||
color: "#A8A29E",
|
||||
},
|
||||
{
|
||||
name: "Brown",
|
||||
color: "#92400E",
|
||||
},
|
||||
];
|
||||
|
||||
// TODO: handle dark mode
|
||||
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
|
||||
{
|
||||
name: "Default",
|
||||
@@ -77,39 +71,35 @@ const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
color: "#98d8f2",
|
||||
color: "#c1ecf9",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
color: "#7edb6c",
|
||||
color: "#acf79f",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
color: "#e0d6ed",
|
||||
color: "#f6f3f8",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
color: "#ffc6c2",
|
||||
color: "#fdebeb",
|
||||
},
|
||||
{
|
||||
name: "Yellow",
|
||||
color: "#faf594",
|
||||
color: "#fbf4a2",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
color: "#f5c8a9",
|
||||
color: "#faebdd",
|
||||
},
|
||||
{
|
||||
name: "Pink",
|
||||
color: "#f5cfe0",
|
||||
color: "#faf1f5",
|
||||
},
|
||||
{
|
||||
name: "Gray",
|
||||
color: "#dfdfd7",
|
||||
},
|
||||
{
|
||||
name: "Brown",
|
||||
color: "#d7c4b7",
|
||||
color: "#f1f1ef",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,21 +112,17 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: (ctx) => {
|
||||
selector: ctx => {
|
||||
if (!ctx.editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeColors: Record<string, boolean> = {};
|
||||
TEXT_COLORS.forEach(({ color }) => {
|
||||
activeColors[`text_${color}`] = ctx.editor.isActive("textStyle", {
|
||||
color,
|
||||
});
|
||||
activeColors[`text_${color}`] = ctx.editor.isActive("textStyle", { color });
|
||||
});
|
||||
HIGHLIGHT_COLORS.forEach(({ color }) => {
|
||||
activeColors[`highlight_${color}`] = ctx.editor.isActive("highlight", {
|
||||
color,
|
||||
});
|
||||
activeColors[`highlight_${color}`] = ctx.editor.isActive("highlight", { color });
|
||||
});
|
||||
|
||||
return activeColors;
|
||||
@@ -147,152 +133,67 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeColorItem = TEXT_COLORS.find(
|
||||
({ color }) => editorState[`text_${color}`],
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editorState[`text_${color}`]
|
||||
);
|
||||
|
||||
const activeHighlightItem = HIGHLIGHT_COLORS.find(
|
||||
({ color }) => editorState[`highlight_${color}`],
|
||||
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
|
||||
editorState[`highlight_${color}`]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover width={220} opened={isOpen} withArrow>
|
||||
<Popover width={200} opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Tooltip label={t("Text color")} withArrow>
|
||||
<Button
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
data-text-color={activeColorItem?.color || ""}
|
||||
data-highlight-color={activeHighlightItem?.color || ""}
|
||||
className="color-selector-trigger"
|
||||
style={{
|
||||
height: "34px",
|
||||
border: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: rem(16),
|
||||
paddingLeft: rem(8),
|
||||
paddingRight: rem(4),
|
||||
color: activeColorItem?.color,
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
A
|
||||
</Button>
|
||||
<IconPalette size={16} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
{/* make mah responsive */}
|
||||
<ScrollArea.Autosize type="scroll" mah="400">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text size="sm" fw={600} mb="xs">
|
||||
{t("Text color")}
|
||||
</Text>
|
||||
<SimpleGrid cols={5} spacing="xs">
|
||||
{TEXT_COLORS.map(({ name, color }, index) => (
|
||||
<Tooltip key={index} label={t(name)} withArrow>
|
||||
<Box
|
||||
onClick={() => {
|
||||
if (name === "Default") {
|
||||
editor.commands.unsetColor();
|
||||
} else {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setColor(color || "")
|
||||
.run();
|
||||
}
|
||||
setIsOpen(false);
|
||||
}}
|
||||
style={{
|
||||
width: rem(28),
|
||||
height: rem(28),
|
||||
borderRadius: rem(6),
|
||||
border: editorState[`text_${color}`]
|
||||
? "2px solid var(--mantine-color-gray-8)"
|
||||
: "1px solid var(--mantine-color-gray-4)",
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: rem(16),
|
||||
fontWeight: 600,
|
||||
color: color || "var(--mantine-color-gray-8)",
|
||||
}}
|
||||
>
|
||||
A
|
||||
</Box>
|
||||
</Tooltip>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
<Text span c="dimmed" tt="uppercase" inherit>
|
||||
{t("Color")}
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Text size="sm" fw={600} mb="xs">
|
||||
{t("Highlight color")}
|
||||
</Text>
|
||||
<SimpleGrid cols={5} spacing="xs">
|
||||
{HIGHLIGHT_COLORS.map(({ name, color }, index) => (
|
||||
<Tooltip key={index} label={t(name)} withArrow>
|
||||
<Box
|
||||
onClick={() => {
|
||||
if (name === "Default") {
|
||||
editor.commands.unsetHighlight();
|
||||
} else {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleMark("highlight", {
|
||||
color: color || "",
|
||||
colorName: name.toLowerCase() || "",
|
||||
})
|
||||
.run();
|
||||
}
|
||||
setIsOpen(false);
|
||||
}}
|
||||
style={{
|
||||
width: rem(28),
|
||||
height: rem(28),
|
||||
borderRadius: rem(4),
|
||||
backgroundColor: color || "var(--mantine-color-gray-2)",
|
||||
border: "1px solid var(--mantine-color-gray-4)",
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: rem(16),
|
||||
fontWeight: 600,
|
||||
color: "var(--mantine-color-gray-8)",
|
||||
}}
|
||||
>
|
||||
{editorState[`highlight_${color}`] ? (
|
||||
<IconCheck
|
||||
size={16}
|
||||
color="var(--mantine-color-green-7)"
|
||||
/>
|
||||
) : (
|
||||
"A"
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
editor.commands.unsetColor();
|
||||
editor.commands.unsetHighlight();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("Remove color")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Button.Group orientation="vertical">
|
||||
{TEXT_COLORS.map(({ name, color }, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="default"
|
||||
leftSection={<span style={{ color }}>A</span>}
|
||||
justify="left"
|
||||
fullWidth
|
||||
rightSection={
|
||||
editorState[`text_${color}`] && (
|
||||
<IconCheck style={{ width: rem(16) }} />
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (name === "Default") {
|
||||
editor.commands.unsetColor();
|
||||
} else {
|
||||
editor.chain().focus().setColor(color || "").run();
|
||||
}
|
||||
setIsOpen(false);
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{t(name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
</ScrollArea.Autosize>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import React, { useCallback } from "react";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import {
|
||||
@@ -49,26 +53,17 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "callout";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const setCalloutType = useCallback(
|
||||
@@ -117,12 +112,14 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
editor={editor}
|
||||
pluginKey={`callout-menu`}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 10],
|
||||
placement: "bottom",
|
||||
// offset: 233, // // offset: [0, 10],
|
||||
// zIndex: 99,
|
||||
flip: false,
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ActionIcon, Group, Select, Tooltip } from "@mantine/core";
|
||||
import { CopyButton } from "@/components/common/copy-button";
|
||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import classes from "./code-block.module.css";
|
||||
@@ -91,7 +90,6 @@ export default function CodeBlockView(props: NodeViewProps) {
|
||||
node.textContent.length > 0
|
||||
}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<NodeViewContent as="code" className={`language-${language}`} />
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import classes from "./code-block.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useComputedColorScheme } from "@mantine/core";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
interface MermaidViewProps {
|
||||
props: NodeViewProps;
|
||||
@@ -38,7 +37,7 @@ export default function MermaidView({ props }: MermaidViewProps) {
|
||||
.catch((err) => {
|
||||
if (props.editor.isEditable) {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${DOMPurify.sanitize(err)}</div>`,
|
||||
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${err}</div>`,
|
||||
);
|
||||
} else {
|
||||
setPreview(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { EditorView } from "@tiptap/pm/view";
|
||||
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
||||
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
||||
import { uploadAttachmentAction } from "../attachment/upload-attachment-action";
|
||||
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
|
||||
import { Slice } from "@tiptap/pm/model";
|
||||
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
|
||||
import { Editor } from "@tiptap/core";
|
||||
|
||||
export const handlePaste = (
|
||||
editor: Editor,
|
||||
view: EditorView,
|
||||
event: ClipboardEvent,
|
||||
pageId: string,
|
||||
creatorId?: string,
|
||||
@@ -17,7 +18,7 @@ export const handlePaste = (
|
||||
// we have to do this validation here to allow the default link extension to takeover if needs be
|
||||
event.preventDefault();
|
||||
const url = clipboardData.trim();
|
||||
const { from: pos, empty } = editor.state.selection;
|
||||
const { from: pos, empty } = view.state.selection;
|
||||
const match = INTERNAL_LINK_REGEX.exec(url);
|
||||
const currentPageMatch = INTERNAL_LINK_REGEX.exec(window.location.href);
|
||||
|
||||
@@ -33,27 +34,17 @@ export const handlePaste = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const anchorId = match[6] ? match[6].split("#")[0] : undefined;
|
||||
const urlWithoutAnchor = anchorId
|
||||
? url.substring(0, url.indexOf("#"))
|
||||
: url;
|
||||
createMentionAction(
|
||||
urlWithoutAnchor,
|
||||
editor.view,
|
||||
pos,
|
||||
creatorId,
|
||||
anchorId,
|
||||
);
|
||||
createMentionAction(url, view, pos, creatorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.clipboardData?.files.length) {
|
||||
event.preventDefault();
|
||||
for (const file of event.clipboardData.files) {
|
||||
const pos = editor.state.selection.from;
|
||||
uploadImageAction(file, editor, pos, pageId);
|
||||
uploadVideoAction(file, editor, pos, pageId);
|
||||
uploadAttachmentAction(file, editor, pos, pageId);
|
||||
const pos = view.state.selection.from;
|
||||
uploadImageAction(file, view, pos, pageId);
|
||||
uploadVideoAction(file, view, pos, pageId);
|
||||
uploadAttachmentAction(file, view, pos, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -61,7 +52,7 @@ export const handlePaste = (
|
||||
};
|
||||
|
||||
export const handleFileDrop = (
|
||||
editor: Editor,
|
||||
view: EditorView,
|
||||
event: DragEvent,
|
||||
moved: boolean,
|
||||
pageId: string,
|
||||
@@ -70,14 +61,14 @@ export const handleFileDrop = (
|
||||
event.preventDefault();
|
||||
|
||||
for (const file of event.dataTransfer.files) {
|
||||
const coordinates = editor.view.posAtCoords({
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
uploadImageAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadVideoAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadAttachmentAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadAttachmentAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import { useCallback } from "react";
|
||||
import { sticky } from "tippy.js";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
@@ -35,26 +40,17 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "drawio";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const onWidthChange = useCallback(
|
||||
@@ -69,11 +65,15 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
||||
editor={editor}
|
||||
pluginKey={`drawio-menu`}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
placement: "top",
|
||||
offset: 8,
|
||||
flip: false,
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: "popper",
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -66,7 +66,6 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
const fileName = "diagram.drawio.svg";
|
||||
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
//@ts-ignore
|
||||
const pageId = editor.storage?.pageId;
|
||||
|
||||
let attachment: IAttachment = null;
|
||||
@@ -88,7 +87,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function EmbedView(props: NodeViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
{embedUrl ? (
|
||||
<ResizableWrapper
|
||||
initialHeight={nodeHeight || 480}
|
||||
|
||||
@@ -1,41 +1,16 @@
|
||||
import { ReactRenderer, useEditor } from "@tiptap/react";
|
||||
import EmojiList from "./emoji-list";
|
||||
import tippy from "tippy.js";
|
||||
import { init } from "emoji-mart";
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
const renderEmojiItems = () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let popup: HTMLDivElement | null = null;
|
||||
let cleanup: (() => void) | null = null;
|
||||
let getReferenceClientRect: (() => DOMRect) | null = null;
|
||||
|
||||
const destroy = () => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
cleanup = null;
|
||||
}
|
||||
|
||||
if (popup) {
|
||||
popup.remove();
|
||||
popup = null;
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component.destroy();
|
||||
component = null;
|
||||
}
|
||||
};
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onBeforeStart: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
clientRect: () => DOMRect;
|
||||
clientRect: DOMRect;
|
||||
}) => {
|
||||
init({
|
||||
data: async () => (await import("@emoji-mart/data")).default,
|
||||
@@ -50,61 +25,51 @@ const renderEmojiItems = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
getReferenceClientRect = props.clientRect;
|
||||
popup = document.createElement("div");
|
||||
popup.style.zIndex = "9999";
|
||||
popup.style.position = "absolute";
|
||||
popup.style.top = "0";
|
||||
popup.style.left = "0";
|
||||
popup.appendChild(component.element);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () => {
|
||||
return getReferenceClientRect
|
||||
? getReferenceClientRect()
|
||||
: new DOMRect(0, 0, 0, 0);
|
||||
},
|
||||
};
|
||||
|
||||
cleanup = autoUpdate(virtualElement, popup, () => {
|
||||
if (!popup) return;
|
||||
|
||||
computePosition(virtualElement, popup, {
|
||||
placement: "bottom-start",
|
||||
middleware: [offset(10), flip(), shift()],
|
||||
}).then(({ x, y }) => {
|
||||
if (!popup) return;
|
||||
|
||||
Object.assign(popup.style, {
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
});
|
||||
});
|
||||
// @ts-ignore
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom",
|
||||
});
|
||||
},
|
||||
onStart: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
clientRect: () => DOMRect;
|
||||
clientRect: DOMRect;
|
||||
}) => {
|
||||
component?.updateProps({ ...props, isLoading: false });
|
||||
component?.updateProps({...props, isLoading: false});
|
||||
|
||||
if (props.clientRect) {
|
||||
getReferenceClientRect = props.clientRect;
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup &&
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
onUpdate: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
clientRect: () => DOMRect;
|
||||
clientRect: DOMRect;
|
||||
}) => {
|
||||
component?.updateProps(props);
|
||||
|
||||
if (props.clientRect) {
|
||||
getReferenceClientRect = props.clientRect;
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup &&
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => {
|
||||
if (props.event.key === "Escape") {
|
||||
destroy();
|
||||
popup?.[0].hide();
|
||||
component?.destroy()
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -113,7 +78,13 @@ const renderEmojiItems = () => {
|
||||
return component?.ref?.onKeyDown(props);
|
||||
},
|
||||
onExit: () => {
|
||||
destroy();
|
||||
if (popup && !popup[0]?.state.isDestroyed) {
|
||||
popup[0]?.destroy();
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component?.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import { useCallback } from "react";
|
||||
import { sticky } from "tippy.js";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
@@ -37,26 +42,17 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "excalidraw";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const onWidthChange = useCallback(
|
||||
@@ -69,13 +65,17 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`excalidraw-menu`}
|
||||
pluginKey={`excalidraw-menu}`}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
placement: "top",
|
||||
offset: 8,
|
||||
flip: false,
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: "popper",
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -98,7 +98,6 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
const fileName = "diagram.excalidraw.svg";
|
||||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
// @ts-ignore
|
||||
const pageId = editor.storage?.pageId;
|
||||
|
||||
let attachment: IAttachment = null;
|
||||
@@ -119,7 +118,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
<ReactClearModal
|
||||
style={{
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import React, { useCallback } from "react";
|
||||
import { sticky } from "tippy.js";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
@@ -17,6 +22,16 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive("image");
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
@@ -37,37 +52,17 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive("image") && editor.getAttributes("image").src;
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "image";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const alignImageLeft = useCallback(() => {
|
||||
@@ -110,11 +105,15 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
editor={editor}
|
||||
pluginKey={`image-menu`}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
placement: "top",
|
||||
offset: 8,
|
||||
flip: false,
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: "popper",
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.imageWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
|
||||
@mixin light {
|
||||
background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%);
|
||||
background-size: 400% 400%;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%);
|
||||
background-size: 400% 400%;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
100% {
|
||||
background-position: -135% 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,30 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Group, Image, Loader, Text } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { Image } from "@mantine/core";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import clsx from "clsx";
|
||||
import classes from "./image-view.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ImageView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { editor, node, selected } = props;
|
||||
const { src, width, align, title, aspectRatio, placeholder } = node.attrs;
|
||||
const { node, selected } = props;
|
||||
const { src, width, align, title } = node.attrs;
|
||||
|
||||
const alignClass = useMemo(() => {
|
||||
if (align === "left") return "alignLeft";
|
||||
if (align === "right") return "alignRight";
|
||||
if (align === "center") return "alignCenter";
|
||||
return "alignCenter";
|
||||
}, [align]);
|
||||
const previewSrc = useMemo(() => {
|
||||
editor.storage.shared.imagePreviews =
|
||||
editor.storage.shared.imagePreviews || {};
|
||||
|
||||
if (placeholder?.id) {
|
||||
return editor.storage.shared.imagePreviews[placeholder.id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [placeholder, editor]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<div
|
||||
className={clsx(
|
||||
selected && "ProseMirror-selectednode",
|
||||
classes.imageWrapper,
|
||||
alignClass,
|
||||
)}
|
||||
style={{
|
||||
aspectRatio: aspectRatio ? aspectRatio : src ? undefined : "16 / 9",
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{src && (
|
||||
<Image radius="md" fit="contain" src={getFileUrl(src)} alt={title} />
|
||||
)}
|
||||
{!src && previewSrc && (
|
||||
<Group pos="relative" h="100%" w="100%">
|
||||
<Image
|
||||
radius="md"
|
||||
fit="contain"
|
||||
src={previewSrc}
|
||||
alt={placeholder?.name}
|
||||
/>
|
||||
<Loader size={20} pos="absolute" bottom={6} right={6} />
|
||||
</Group>
|
||||
)}
|
||||
{!src && !previewSrc && (
|
||||
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
|
||||
<Loader size={20} style={{ flexShrink: 0 }} />
|
||||
<Text component="span" size="sm" truncate="end">
|
||||
{placeholder?.name
|
||||
? t("Uploading {{name}}", { name: placeholder.name })
|
||||
: t("Uploading file")}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
<NodeViewWrapper>
|
||||
<Image
|
||||
radius="md"
|
||||
fit="contain"
|
||||
w={width}
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
|
||||
/>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export type LinkFn = (
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
creatorId: string,
|
||||
anchorId?: string,
|
||||
) => void;
|
||||
|
||||
export interface InternalLinkOptions {
|
||||
@@ -19,7 +18,7 @@ export interface InternalLinkOptions {
|
||||
|
||||
export const handleInternalLink =
|
||||
({ validateFn, onResolveLink }: InternalLinkOptions): LinkFn =>
|
||||
async (url: string, view, pos, creatorId, anchorId) => {
|
||||
async (url: string, view, pos, creatorId) => {
|
||||
const validated = validateFn(url, view);
|
||||
if (!validated) return;
|
||||
|
||||
@@ -36,7 +35,6 @@ export const handleInternalLink =
|
||||
entityId: page.id,
|
||||
slugId: page.slugId,
|
||||
creatorId: creatorId,
|
||||
anchorId: anchorId,
|
||||
});
|
||||
|
||||
if (!node) return;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { BubbleMenu as BaseBubbleMenu, useEditorState } from "@tiptap/react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { LinkPreviewPanel } from "@/features/editor/components/link/link-preview.tsx";
|
||||
import { Card } from "@mantine/core";
|
||||
import { useEditorState } from "@tiptap/react";
|
||||
|
||||
export function LinkMenu({ editor, appendTo }: EditorMenuProps) {
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
@@ -60,15 +59,18 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) {
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`link-menu`}
|
||||
pluginKey={`link-menu}`}
|
||||
updateDelay={0}
|
||||
options={{
|
||||
onHide: () => {
|
||||
tippyOptions={{
|
||||
appendTo: () => {
|
||||
return appendTo?.current;
|
||||
},
|
||||
onHidden: () => {
|
||||
setShowEdit(false);
|
||||
},
|
||||
placement: "bottom",
|
||||
offset: 5,
|
||||
// zIndex: 101,
|
||||
offset: [0, 5],
|
||||
zIndex: 101,
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -106,7 +106,6 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||
|
||||
setRenderItems(items);
|
||||
// update editor storage
|
||||
//@ts-ignore
|
||||
props.editor.storage.mentionItems = items;
|
||||
}
|
||||
}, [suggestion, isLoading]);
|
||||
@@ -164,7 +163,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||
|
||||
const enterHandler = () => {
|
||||
if (!renderItems.length) return;
|
||||
if (renderItems[selectedIndex]?.entityType !== "header") {
|
||||
if (renderItems[selectedIndex].entityType !== "header") {
|
||||
selectItem(selectedIndex);
|
||||
}
|
||||
};
|
||||
@@ -204,7 +203,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||
parentPageId: page.id || null,
|
||||
title: title
|
||||
};
|
||||
|
||||
|
||||
let createdPage: IPage;
|
||||
try {
|
||||
createdPage = await createPageMutation.mutateAsync(payload);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ReactRenderer, useEditor } from "@tiptap/react";
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
import tippy from "tippy.js";
|
||||
import MentionList from "@/features/editor/components/mention/mention-list.tsx";
|
||||
|
||||
function getWhitespaceCount(query: string) {
|
||||
@@ -15,27 +9,16 @@ function getWhitespaceCount(query: string) {
|
||||
|
||||
const mentionRenderItems = () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let activeClientRect: (() => DOMRect) | null = null;
|
||||
let updatePositionCleanup: (() => void) | null = null;
|
||||
|
||||
const destroy = () => {
|
||||
updatePositionCleanup?.();
|
||||
updatePositionCleanup = null;
|
||||
component?.destroy();
|
||||
if (component?.element?.parentNode) {
|
||||
component.element.parentNode.removeChild(component.element);
|
||||
}
|
||||
component = null;
|
||||
};
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
clientRect: () => DOMRect;
|
||||
clientRect: DOMRect;
|
||||
query: string;
|
||||
}) => {
|
||||
// query must not start with a whitespace
|
||||
if (props.query.charAt(0) === " ") {
|
||||
if (props.query.charAt(0) === ' '){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,95 +37,75 @@ const mentionRenderItems = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
activeClientRect = props.clientRect;
|
||||
|
||||
const { element } = component;
|
||||
document.body.appendChild(element);
|
||||
|
||||
updatePositionCleanup = autoUpdate(
|
||||
{
|
||||
getBoundingClientRect: () =>
|
||||
activeClientRect ? activeClientRect() : new DOMRect(),
|
||||
},
|
||||
element,
|
||||
() => {
|
||||
if (!component?.element) return;
|
||||
computePosition(
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
return activeClientRect ? activeClientRect() : new DOMRect();
|
||||
},
|
||||
},
|
||||
element,
|
||||
{
|
||||
placement: "bottom-start",
|
||||
middleware: [offset(0), flip(), shift()],
|
||||
},
|
||||
).then(({ x, y }) => {
|
||||
Object.assign(element.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
position: "absolute",
|
||||
zIndex: "9999",
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
// @ts-ignore
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
});
|
||||
},
|
||||
onUpdate: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
clientRect: () => DOMRect;
|
||||
clientRect: DOMRect;
|
||||
query: string;
|
||||
}) => {
|
||||
// query must not start with a whitespace
|
||||
if (props.query.charAt(0) === " ") {
|
||||
destroy();
|
||||
if (props.query.charAt(0) === ' '){
|
||||
component?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// only update component if popup is not destroyed
|
||||
if (component) {
|
||||
component.updateProps(props);
|
||||
if (!popup?.[0].state.isDestroyed) {
|
||||
component?.updateProps(props);
|
||||
}
|
||||
|
||||
if (!props || !props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeClientRect = props.clientRect;
|
||||
|
||||
const whitespaceCount = getWhitespaceCount(props.query);
|
||||
|
||||
// destroy component if space is greater 3 without a match
|
||||
if (
|
||||
whitespaceCount > 4 &&
|
||||
//@ts-ignore
|
||||
props.editor.storage.mentionItems.length === 1
|
||||
whitespaceCount > 3 &&
|
||||
props.editor.storage.mentionItems.length === 0
|
||||
) {
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
// fallback exit
|
||||
if (whitespaceCount > 7) {
|
||||
destroy();
|
||||
popup?.[0]?.destroy();
|
||||
component?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
popup &&
|
||||
!popup?.[0].state.isDestroyed &&
|
||||
popup?.[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => {
|
||||
if (props.event.key === "Escape") {
|
||||
destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (props.event.key === "Enter" && !component) {
|
||||
destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (props.event.key)
|
||||
if (
|
||||
props.event.key === "Escape" ||
|
||||
(props.event.key === "Enter" && !popup?.[0].state.isShown)
|
||||
) {
|
||||
popup?.[0].destroy();
|
||||
component?.destroy();
|
||||
return false;
|
||||
}
|
||||
return (component?.ref as any)?.onKeyDown(props);
|
||||
},
|
||||
onExit: () => {
|
||||
destroy();
|
||||
if (popup && !popup?.[0].state.isDestroyed) {
|
||||
popup[0].destroy();
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ActionIcon, Anchor, Text } from "@mantine/core";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import {
|
||||
buildPageUrl,
|
||||
buildSharedPageUrl,
|
||||
} from "@/features/page/page.utils.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import classes from "./mention.module.css";
|
||||
|
||||
export default function MentionView(props: NodeViewProps) {
|
||||
const { node } = props;
|
||||
const { label, entityType, entityId, slugId, anchorId } = node.attrs;
|
||||
const { spaceSlug, pageSlug } = useParams();
|
||||
const { label, entityType, entityId, slugId } = node.attrs;
|
||||
const { spaceSlug } = useParams();
|
||||
const { shareId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data: page,
|
||||
isLoading,
|
||||
@@ -25,29 +23,14 @@ export default function MentionView(props: NodeViewProps) {
|
||||
const location = useLocation();
|
||||
const isShareRoute = location.pathname.startsWith("/share");
|
||||
|
||||
const currentPageSlugId = extractPageSlugId(pageSlug);
|
||||
const isSamePage = currentPageSlugId === slugId;
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isSamePage && anchorId) {
|
||||
e.preventDefault();
|
||||
const element = document.querySelector(`[id="${anchorId}"]`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
navigate(`#${anchorId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const shareSlugUrl = buildSharedPageUrl({
|
||||
shareId,
|
||||
pageSlugId: slugId,
|
||||
pageTitle: label,
|
||||
anchorId,
|
||||
});
|
||||
|
||||
return (
|
||||
<NodeViewWrapper style={{ display: "inline" }} data-drag-handle>
|
||||
<NodeViewWrapper style={{ display: "inline" }}>
|
||||
{entityType === "user" && (
|
||||
<Text className={classes.userMention} component="span">
|
||||
@{label}
|
||||
@@ -59,9 +42,8 @@ export default function MentionView(props: NodeViewProps) {
|
||||
component={Link}
|
||||
fw={500}
|
||||
to={
|
||||
isShareRoute ? shareSlugUrl : buildPageUrl(spaceSlug, slugId, label, anchorId)
|
||||
isShareRoute ? shareSlugUrl : buildPageUrl(spaceSlug, slugId, label)
|
||||
}
|
||||
onClick={handleClick}
|
||||
underline="never"
|
||||
className={classes.pageMentionLink}
|
||||
>
|
||||
|
||||
-2
@@ -73,8 +73,6 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
if (!editor) return;
|
||||
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
//TODO: check type error
|
||||
//@ts-ignore
|
||||
const position: Range = results[resultIndex];
|
||||
|
||||
if (!position) return;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
IconCalendar,
|
||||
IconAppWindow,
|
||||
IconSitemap,
|
||||
IconAlignLeft2,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
CommandProps,
|
||||
@@ -153,6 +154,14 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
title: "Table of contents",
|
||||
description: "Insert table of contents",
|
||||
searchTerms: ["toc"],
|
||||
icon: IconAlignLeft2,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).insertTableOfContents().run(),
|
||||
},
|
||||
{
|
||||
title: "Image",
|
||||
description: "Upload any image from your device.",
|
||||
@@ -161,7 +170,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
|
||||
// @ts-ignore
|
||||
const pageId = editor.storage?.pageId;
|
||||
if (!pageId) return;
|
||||
|
||||
@@ -170,18 +178,13 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.multiple = true;
|
||||
input.style.display = "none";
|
||||
document.body.appendChild(input);
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
for (const file of input.files) {
|
||||
const pos = editor.view.state.selection.from;
|
||||
|
||||
uploadImageAction(file, editor, pos, pageId);
|
||||
uploadImageAction(file, editor.view, pos, pageId);
|
||||
}
|
||||
}
|
||||
|
||||
input.remove();
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
@@ -194,7 +197,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
|
||||
// @ts-ignore
|
||||
const pageId = editor.storage?.pageId;
|
||||
if (!pageId) return;
|
||||
|
||||
@@ -202,19 +204,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "video/*";
|
||||
input.multiple = true;
|
||||
input.style.display = "none";
|
||||
document.body.appendChild(input);
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
for (const file of input.files) {
|
||||
const pos = editor.view.state.selection.from;
|
||||
|
||||
uploadVideoAction(file, editor, pos, pageId);
|
||||
}
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
uploadVideoAction(file, editor.view, pos, pageId);
|
||||
}
|
||||
|
||||
input.remove();
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
@@ -227,7 +222,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
|
||||
// @ts-ignore
|
||||
const pageId = editor.storage?.pageId;
|
||||
if (!pageId) return;
|
||||
|
||||
@@ -235,19 +229,12 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "";
|
||||
input.multiple = true;
|
||||
input.style.display = "none";
|
||||
document.body.appendChild(input);
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
for (const file of input.files) {
|
||||
const pos = editor.view.state.selection.from;
|
||||
|
||||
uploadAttachmentAction(file, editor, pos, pageId, true);
|
||||
}
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
uploadAttachmentAction(file, editor.view, pos, pageId, true);
|
||||
}
|
||||
|
||||
input.remove();
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
import { ReactRenderer, useEditor } from "@tiptap/react";
|
||||
import CommandList from "@/features/editor/components/slash-menu/command-list";
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
const renderItems = () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let popup: HTMLElement | null = null;
|
||||
let cleanup: (() => void) | null = null;
|
||||
let getReferenceClientRect: (() => DOMRect) | null = null;
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!popup || !getReferenceClientRect) return;
|
||||
|
||||
// @ts-ignore
|
||||
const rect = getReferenceClientRect();
|
||||
|
||||
computePosition({ getBoundingClientRect: () => rect }, popup, {
|
||||
placement: "bottom-start",
|
||||
middleware: [offset(0), flip(), shift()],
|
||||
}).then(({ x, y }) => {
|
||||
if (popup) {
|
||||
popup.style.left = `${x}px`;
|
||||
popup.style.top = `${y}px`;
|
||||
}
|
||||
});
|
||||
};
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: {
|
||||
@@ -46,29 +21,15 @@ const renderItems = () => {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
getReferenceClientRect = props.clientRect;
|
||||
|
||||
popup = document.createElement("div");
|
||||
popup.style.zIndex = "9999";
|
||||
popup.style.position = "absolute";
|
||||
popup.style.top = "0";
|
||||
popup.style.left = "0";
|
||||
|
||||
document.body.appendChild(popup);
|
||||
popup.appendChild(component.element);
|
||||
|
||||
cleanup = autoUpdate(
|
||||
// @ts-ignore
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
return getReferenceClientRect
|
||||
? getReferenceClientRect()
|
||||
: new DOMRect();
|
||||
},
|
||||
},
|
||||
popup,
|
||||
updatePosition
|
||||
);
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
});
|
||||
},
|
||||
onUpdate: (props: {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@@ -80,15 +41,14 @@ const renderItems = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
getReferenceClientRect = props.clientRect;
|
||||
updatePosition();
|
||||
popup &&
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => {
|
||||
if (props.event.key === "Escape") {
|
||||
if (popup) {
|
||||
popup.style.display = "none";
|
||||
}
|
||||
popup?.[0].hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -97,19 +57,12 @@ const renderItems = () => {
|
||||
return component?.ref?.onKeyDown(props);
|
||||
},
|
||||
onExit: () => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
cleanup = null;
|
||||
}
|
||||
|
||||
if (popup) {
|
||||
popup.remove();
|
||||
popup = null;
|
||||
if (popup && !popup[0].state.isDestroyed) {
|
||||
popup[0].destroy();
|
||||
}
|
||||
|
||||
if (component) {
|
||||
component.destroy();
|
||||
component = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
posToDOMRect,
|
||||
findParentNode,
|
||||
} from "@tiptap/react";
|
||||
import { Node as PMNode } from "@tiptap/pm/model";
|
||||
import React, { useCallback } from "react";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { sticky } from "tippy.js";
|
||||
|
||||
interface SubpagesMenuProps {
|
||||
editor: Editor;
|
||||
@@ -29,7 +33,7 @@ export const SubpagesMenu = React.memo(
|
||||
|
||||
return editor.isActive("subpages");
|
||||
},
|
||||
[editor]
|
||||
[editor],
|
||||
);
|
||||
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
@@ -58,8 +62,18 @@ export const SubpagesMenu = React.memo(
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`subpages-menu`}
|
||||
pluginKey={`subpages-menu}`}
|
||||
updateDelay={0}
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: "popper",
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<Tooltip position="top" label={t("Delete")}>
|
||||
@@ -75,7 +89,7 @@ export const SubpagesMenu = React.memo(
|
||||
</Tooltip>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default SubpagesMenu;
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
const { spaceSlug, shareId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
//@ts-ignore
|
||||
const currentPageId = editor.storage.pageId;
|
||||
|
||||
// Get subpages from shared tree if we're in a shared context
|
||||
@@ -53,7 +52,7 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
|
||||
if (error && !shareId) {
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
<Text c="dimmed" size="md" py="md">
|
||||
{t("Failed to load subpages")}
|
||||
</Text>
|
||||
@@ -63,7 +62,7 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
|
||||
if (subpages.length === 0) {
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
<div className={classes.container}>
|
||||
<Text c="dimmed" size="md" py="md">
|
||||
{t("No subpages")}
|
||||
@@ -74,7 +73,7 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<NodeViewWrapper>
|
||||
<div className={classes.container}>
|
||||
<Stack gap={5}>
|
||||
{subpages.map((page) => (
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
.header {
|
||||
}
|
||||
|
||||
.container {
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
}
|
||||
|
||||
.link {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
word-wrap: break-word;
|
||||
background-color: transparent;
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
line-height: var(--mantine-line-height-sm);
|
||||
padding: 6px;
|
||||
border-top-right-radius: var(--mantine-radius-sm);
|
||||
border-bottom-right-radius: var(--mantine-radius-sm);
|
||||
border: none;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-2),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
& {
|
||||
border: none !important;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkActive {
|
||||
font-weight: 500;
|
||||
border-left-color: light-dark(
|
||||
var(--mantine-color-grey-5),
|
||||
var(--mantine-color-grey-3)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
|
||||
&,
|
||||
&:hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-3),
|
||||
var(--mantine-color-dark-5)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.leftBorder {
|
||||
border-left: 1px solid
|
||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Editor as CoreEditor } from "@tiptap/core";
|
||||
import { TableOfContentsStorage } from "@tiptap/extension-table-of-contents";
|
||||
import { NodeViewWrapper, useEditorState } from "@tiptap/react";
|
||||
import { memo } from "react";
|
||||
import { clsx } from "clsx";
|
||||
import classes from "./table-of-contents-nodeview.module.css";
|
||||
|
||||
export type TableOfContentsProps = {
|
||||
editor: CoreEditor;
|
||||
onItemClick?: () => void;
|
||||
};
|
||||
|
||||
export const TableOfContentsNodeview = memo(
|
||||
({ editor, onItemClick }: TableOfContentsProps) => {
|
||||
const content = useEditorState({
|
||||
editor,
|
||||
selector: (ctx) =>
|
||||
(ctx.editor.storage.tableOfContents as TableOfContentsStorage)?.content,
|
||||
});
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<div contentEditable={false}>
|
||||
<div className={classes.header}>Table of contents</div>
|
||||
{content.length > 0 ? (
|
||||
<div className={classes.container}>
|
||||
{content
|
||||
.filter((item) => item.level <= 3)
|
||||
.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`#${item.id}`}
|
||||
style={{ marginLeft: `${1 * item.level - 1}rem` }}
|
||||
onClick={onItemClick}
|
||||
className={clsx(
|
||||
classes.link,
|
||||
item.isActive && classes.linkActive,
|
||||
)}
|
||||
>
|
||||
{item.itemIndex}. {item.textContent}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.emptyState}>
|
||||
Start adding headlines to your document …
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TableOfContentsNodeview.displayName = "TableOfContentsNodeview";
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
@@ -15,7 +17,6 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableBackgroundColor } from "./table-background-color";
|
||||
import { TableTextAlignment } from "./table-text-alignment";
|
||||
import { BubbleMenu } from "@tiptap/react/menus";
|
||||
|
||||
export const TableCellMenu = React.memo(
|
||||
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
|
||||
@@ -28,7 +29,7 @@ export const TableCellMenu = React.memo(
|
||||
|
||||
return isCellSelection(state.selection);
|
||||
},
|
||||
[editor]
|
||||
[editor],
|
||||
);
|
||||
|
||||
const mergeCells = useCallback(() => {
|
||||
@@ -52,27 +53,23 @@ export const TableCellMenu = React.memo(
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey="table-cell-menu"
|
||||
updateDelay={0}
|
||||
appendTo={() => {
|
||||
return appendTo?.current;
|
||||
}}
|
||||
ref={(element) => {
|
||||
element.style.zIndex = "99";
|
||||
}}
|
||||
options={{
|
||||
offset: {
|
||||
mainAxis: 15,
|
||||
tippyOptions={{
|
||||
appendTo: () => {
|
||||
return appendTo?.current;
|
||||
},
|
||||
offset: [0, 15],
|
||||
zIndex: 99,
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group>
|
||||
<TableBackgroundColor editor={editor} />
|
||||
<TableTextAlignment editor={editor} />
|
||||
|
||||
|
||||
<Tooltip position="top" label={t("Merge cells")}>
|
||||
<ActionIcon
|
||||
onClick={mergeCells}
|
||||
@@ -128,9 +125,9 @@ export const TableCellMenu = React.memo(
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ActionIcon.Group>
|
||||
</BubbleMenu>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default TableCellMenu;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
posToDOMRect,
|
||||
findParentNode,
|
||||
} from "@tiptap/react";
|
||||
import { Node as PMNode } from "@tiptap/pm/model";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
@@ -12,12 +17,9 @@ import {
|
||||
IconColumnRemove,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconRowRemove,
|
||||
IconTableColumn,
|
||||
IconTableRow,
|
||||
IconRowRemove, IconTableColumn, IconTableRow,
|
||||
IconTrashX,
|
||||
} from "@tabler/icons-react";
|
||||
import { BubbleMenu } from "@tiptap/react/menus";
|
||||
} from '@tabler/icons-react';
|
||||
import { isCellSelection } from "@docmost/editor-ext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -32,28 +34,20 @@ export const TableMenu = React.memo(
|
||||
|
||||
return editor.isActive("table") && !isCellSelection(state.selection);
|
||||
},
|
||||
[editor]
|
||||
[editor],
|
||||
);
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "table";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const rect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => rect,
|
||||
getClientRects: () => [rect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const rect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => rect,
|
||||
getClientRects: () => [rect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const toggleHeaderColumn = useCallback(() => {
|
||||
@@ -93,33 +87,42 @@ export const TableMenu = React.memo(
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey="table-menu"
|
||||
resizeDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
ref={(element) => {
|
||||
element.style.zIndex = "99";
|
||||
}}
|
||||
options={{
|
||||
placement: "top",
|
||||
offset: {
|
||||
mainAxis: 15,
|
||||
},
|
||||
flip: {
|
||||
fallbackPlacements: ["top", "bottom"],
|
||||
padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity },
|
||||
boundary: editor.options.element as HTMLElement,
|
||||
},
|
||||
shift: {
|
||||
padding: 8 + 15,
|
||||
crossAxis: true,
|
||||
updateDelay={0}
|
||||
tippyOptions={{
|
||||
getReferenceClientRect: getReferenceClientRect,
|
||||
offset: [0, 15],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
enabled: true,
|
||||
options: {
|
||||
altAxis: true,
|
||||
boundary: "clippingParents",
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
enabled: true,
|
||||
options: {
|
||||
boundary: editor.options.element,
|
||||
fallbackPlacements: ["top", "bottom"],
|
||||
padding: { top: 35, left: 8, right: 8, bottom: -Infinity },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group>
|
||||
<Tooltip position="top" label={t("Add left column")}>
|
||||
<Tooltip position="top" label={t("Add left column")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={addColumnLeft}
|
||||
variant="default"
|
||||
@@ -185,7 +188,8 @@ export const TableMenu = React.memo(
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label={t("Toggle header row")}>
|
||||
<Tooltip position="top" label={t("Toggle header row")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={toggleHeaderRow}
|
||||
variant="default"
|
||||
@@ -196,7 +200,8 @@ export const TableMenu = React.memo(
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label={t("Toggle header column")}>
|
||||
<Tooltip position="top" label={t("Toggle header column")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={toggleHeaderColumn}
|
||||
variant="default"
|
||||
@@ -219,9 +224,9 @@ export const TableMenu = React.memo(
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ActionIcon.Group>
|
||||
</BubbleMenu>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default TableMenu;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import React, { useCallback } from "react";
|
||||
import { sticky } from "tippy.js";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
@@ -17,6 +22,16 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive("video");
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
@@ -37,37 +52,17 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive("video") && editor.getAttributes("video").src;
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const getReferencedVirtualElement = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === "video";
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||
return {
|
||||
getBoundingClientRect: () => domRect,
|
||||
getClientRects: () => [domRect],
|
||||
};
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const alignVideoLeft = useCallback(() => {
|
||||
@@ -110,11 +105,15 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
editor={editor}
|
||||
pluginKey={`video-menu`}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
placement: "top",
|
||||
offset: 8,
|
||||
flip: false,
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: "flip", enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: "popper",
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.videoWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
|
||||
@mixin light {
|
||||
background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%);
|
||||
background-size: 400% 400%;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%);
|
||||
background-size: 400% 400%;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
100% {
|
||||
background-position: -135% 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -1,75 +1,29 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Group, Loader, Text } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import clsx from "clsx";
|
||||
import classes from "./video-view.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function VideoView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { editor, node, selected } = props;
|
||||
const { src, width, align, aspectRatio, placeholder } = node.attrs;
|
||||
const { node, selected } = props;
|
||||
const { src, width, align } = node.attrs;
|
||||
|
||||
const alignClass = useMemo(() => {
|
||||
if (align === "left") return "alignLeft";
|
||||
if (align === "right") return "alignRight";
|
||||
if (align === "center") return "alignCenter";
|
||||
return "alignCenter";
|
||||
}, [align]);
|
||||
const previewSrc = useMemo(() => {
|
||||
editor.storage.shared.videoPreviews =
|
||||
editor.storage.shared.videoPreviews || {};
|
||||
|
||||
if (placeholder?.id) {
|
||||
return editor.storage.shared.videoPreviews[placeholder.id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [placeholder, editor]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<div
|
||||
className={clsx(
|
||||
selected && "ProseMirror-selectednode",
|
||||
classes.videoWrapper,
|
||||
alignClass,
|
||||
)}
|
||||
style={{
|
||||
aspectRatio: aspectRatio ? aspectRatio : src ? undefined : "16 / 9",
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{src && (
|
||||
<video
|
||||
className={classes.video}
|
||||
preload="metadata"
|
||||
controls
|
||||
src={getFileUrl(src)}
|
||||
/>
|
||||
)}
|
||||
{!src && previewSrc && (
|
||||
<Group pos="relative" h="100%" w="100%">
|
||||
<video
|
||||
className={classes.video}
|
||||
preload="metadata"
|
||||
controls
|
||||
src={previewSrc}
|
||||
/>
|
||||
<Loader size={20} pos="absolute" top={6} right={6} />
|
||||
</Group>
|
||||
)}
|
||||
{!src && !previewSrc && (
|
||||
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
|
||||
<Loader size={20} style={{ flexShrink: 0 }} />
|
||||
<Text component="span" size="sm" truncate="end">
|
||||
{placeholder?.name
|
||||
? t("Uploading {{name}}", { name: placeholder.name })
|
||||
: t("Uploading file")}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
<NodeViewWrapper>
|
||||
<video
|
||||
preload="metadata"
|
||||
width={width}
|
||||
controls
|
||||
src={getFileUrl(src)}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||
import { TextAlign } from "@tiptap/extension-text-align";
|
||||
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
||||
import { Placeholder, CharacterCount } from "@tiptap/extensions";
|
||||
import { TaskList } from "@tiptap/extension-task-list";
|
||||
import { TaskItem } from "@tiptap/extension-task-item";
|
||||
import { Underline } from "@tiptap/extension-underline";
|
||||
import { Superscript } from "@tiptap/extension-superscript";
|
||||
import SubScript from "@tiptap/extension-subscript";
|
||||
import { Highlight } from "@tiptap/extension-highlight";
|
||||
import { Typography } from "@tiptap/extension-typography";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
|
||||
import { Youtube } from "@tiptap/extension-youtube";
|
||||
import SlashCommand from "@/features/editor/extensions/slash-command";
|
||||
import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration";
|
||||
import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
|
||||
import { TableOfContents as TiptapTableOfContents } from "@tiptap/extension-table-of-contents";
|
||||
import { Collaboration } from "@tiptap/extension-collaboration";
|
||||
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import {
|
||||
Comment,
|
||||
@@ -37,12 +39,9 @@ import {
|
||||
Embed,
|
||||
SearchAndReplace,
|
||||
Mention,
|
||||
TableDndExtension,
|
||||
Subpages,
|
||||
Heading,
|
||||
Highlight,
|
||||
UniqueID,
|
||||
SharedStorage,
|
||||
TableDndExtension,
|
||||
TableOfContentsNode,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@@ -51,8 +50,11 @@ import {
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import MathInlineView from "@/features/editor/components/math/math-inline.tsx";
|
||||
import MathBlockView from "@/features/editor/components/math/math-block.tsx";
|
||||
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
|
||||
import { Youtube } from "@tiptap/extension-youtube";
|
||||
import ImageView from "@/features/editor/components/image/image-view.tsx";
|
||||
import CalloutView from "@/features/editor/components/callout/callout-view.tsx";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import VideoView from "@/features/editor/components/video/video-view.tsx";
|
||||
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
|
||||
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
||||
@@ -60,7 +62,6 @@ import DrawioView from "../components/drawio/drawio-view";
|
||||
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
||||
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
||||
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||
import powershell from "highlight.js/lib/languages/powershell";
|
||||
import abap from "highlightjs-sap-abap";
|
||||
@@ -77,7 +78,9 @@ import MentionView from "@/features/editor/components/mention/mention-view.tsx";
|
||||
import i18n from "@/i18n.ts";
|
||||
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
|
||||
import EmojiCommand from "./emoji-command";
|
||||
import { CharacterCount } from "@tiptap/extension-character-count";
|
||||
import { countWords } from "alfaaz";
|
||||
import { TableOfContentsNodeview } from "@/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
lowlight.register("mermaid", plaintext);
|
||||
@@ -93,10 +96,7 @@ lowlight.register("scala", scala);
|
||||
|
||||
export const mainExtensions = [
|
||||
StarterKit.configure({
|
||||
heading: false,
|
||||
undoRedo: false,
|
||||
link: false,
|
||||
trailingNode: false,
|
||||
history: false,
|
||||
dropcursor: {
|
||||
width: 3,
|
||||
color: "#70CFF8",
|
||||
@@ -108,12 +108,6 @@ export const mainExtensions = [
|
||||
},
|
||||
},
|
||||
}),
|
||||
SharedStorage,
|
||||
Heading,
|
||||
UniqueID.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
filterTransaction: (transaction) => !isChangeOrigin(transaction),
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
@@ -134,6 +128,7 @@ export const mainExtensions = [
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
}),
|
||||
Underline,
|
||||
LinkExtension.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
@@ -168,9 +163,6 @@ export const mainExtensions = [
|
||||
},
|
||||
}).extend({
|
||||
addNodeView() {
|
||||
// Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191)
|
||||
this.editor.isInitialized = true;
|
||||
|
||||
return ReactNodeViewRenderer(MentionView);
|
||||
},
|
||||
}),
|
||||
@@ -209,7 +201,6 @@ export const mainExtensions = [
|
||||
}),
|
||||
CustomCodeBlock.configure({
|
||||
view: CodeBlockView,
|
||||
//@ts-ignore
|
||||
lowlight,
|
||||
HTMLAttributes: {
|
||||
spellcheck: false,
|
||||
@@ -248,11 +239,15 @@ export const mainExtensions = [
|
||||
Escape: () => {
|
||||
const event = new CustomEvent("closeFindDialogFromEditor", {});
|
||||
document.dispatchEvent(event);
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
}).configure(),
|
||||
TiptapTableOfContents,
|
||||
TableOfContentsNode.configure({
|
||||
view: TableOfContentsNodeview,
|
||||
}),
|
||||
] as any;
|
||||
|
||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||
@@ -260,9 +255,8 @@ type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||
export const collabExtensions: CollabExtensions = (provider, user) => [
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
provider,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
CollaborationCursor.configure({
|
||||
provider,
|
||||
user: {
|
||||
name: user.name,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
function waitForState(checkFn: () => boolean): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (checkFn()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
export const useEditorScroll = ({
|
||||
canScroll,
|
||||
initialScrollTo,
|
||||
}: {
|
||||
canScroll: () => boolean;
|
||||
initialScrollTo?: string;
|
||||
}) => {
|
||||
const [scrollTo, setScrollTo] = useState<string>(initialScrollTo || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialScrollTo) {
|
||||
setScrollTo(window.location.hash ? window.location.hash.slice(1) : "");
|
||||
}
|
||||
}, [initialScrollTo]);
|
||||
|
||||
const handleScrollTo = useCallback(async (editor: Editor, _scrollTo: string | null = null, tryCount: number = 0) => {
|
||||
await waitForState(() => canScroll());
|
||||
return new Promise((resolve) => {
|
||||
const MAX_TRY_COUNT = 10;
|
||||
if (tryCount >= MAX_TRY_COUNT) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = _scrollTo || scrollTo;
|
||||
if (!targetId) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dom = editor.view.dom.querySelector(`[id="${targetId}"]`);
|
||||
if (dom) {
|
||||
dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
resolve(true);
|
||||
} else {
|
||||
setTimeout(async () => {
|
||||
resolve(await handleScrollTo(editor, targetId, tryCount + 1));
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}, [scrollTo, canScroll]);
|
||||
|
||||
return { scrollTo, handleScrollTo };
|
||||
};
|
||||
@@ -1,22 +1,13 @@
|
||||
import "@/features/editor/styles/index.css";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
import {
|
||||
HocuspocusProvider,
|
||||
onStatusParameters,
|
||||
onAuthenticationFailedParameters,
|
||||
WebSocketStatus,
|
||||
HocuspocusProviderWebsocket,
|
||||
onSyncedParameters,
|
||||
} from "@hocuspocus/provider";
|
||||
import {
|
||||
Editor,
|
||||
EditorContent,
|
||||
EditorProvider,
|
||||
useEditor,
|
||||
@@ -65,7 +56,6 @@ import { FIVE_MINUTES } from "@/lib/constants.ts";
|
||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { searchSpotlight } from "@/features/search/constants.ts";
|
||||
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||
|
||||
interface PageEditorProps {
|
||||
pageId: string;
|
||||
@@ -79,139 +69,149 @@ export default function PageEditor({
|
||||
content,
|
||||
}: PageEditorProps) {
|
||||
const collaborationURL = useCollaborationUrl();
|
||||
const isComponentMounted = useRef(false);
|
||||
const editorRef = useRef<Editor | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
isComponentMounted.current = true;
|
||||
}, []);
|
||||
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setEditor] = useAtom(pageEditorAtom);
|
||||
const [, setAsideState] = useAtom(asideStateAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [isLocalSynced, setIsLocalSynced] = useState(false);
|
||||
const [isRemoteSynced, setIsRemoteSynced] = useState(false);
|
||||
const ydocRef = useRef<Y.Doc | null>(null);
|
||||
if (!ydocRef.current) {
|
||||
ydocRef.current = new Y.Doc();
|
||||
}
|
||||
const ydoc = ydocRef.current;
|
||||
const [isLocalSynced, setLocalSynced] = useState(false);
|
||||
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
||||
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
||||
yjsConnectionStatusAtom,
|
||||
);
|
||||
const menuContainerRef = useRef(null);
|
||||
const documentName = `page.${pageId}`;
|
||||
const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken();
|
||||
const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false });
|
||||
const documentState = useDocumentVisibility();
|
||||
const [isCollabReady, setIsCollabReady] = useState(false);
|
||||
const { pageSlug } = useParams();
|
||||
const slugId = extractPageSlugId(pageSlug);
|
||||
const userPageEditMode =
|
||||
currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
|
||||
const canScroll = useCallback(
|
||||
() => Boolean(isComponentMounted.current && editorRef.current),
|
||||
[isComponentMounted],
|
||||
);
|
||||
const { handleScrollTo } = useEditorScroll({ canScroll });
|
||||
|
||||
// Providers only created once per pageId
|
||||
const providersRef = useRef<{
|
||||
local: IndexeddbPersistence;
|
||||
remote: HocuspocusProvider;
|
||||
socket: HocuspocusProviderWebsocket;
|
||||
} | null>(null);
|
||||
const [providersReady, setProvidersReady] = useState(false);
|
||||
|
||||
const localProvider = providersRef.current?.local;
|
||||
const remoteProvider = providersRef.current?.remote;
|
||||
|
||||
// Track when collaborative provider is ready and synced
|
||||
const [collabReady, setCollabReady] = useState(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
remoteProvider?.status === WebSocketStatus.Connected &&
|
||||
isLocalSynced &&
|
||||
isRemoteSynced
|
||||
) {
|
||||
setCollabReady(true);
|
||||
}
|
||||
}, [remoteProvider?.status, isLocalSynced, isRemoteSynced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!providersRef.current) {
|
||||
const documentName = `page.${pageId}`;
|
||||
const ydoc = new Y.Doc();
|
||||
const local = new IndexeddbPersistence(documentName, ydoc);
|
||||
const socket = new HocuspocusProviderWebsocket({
|
||||
url: collaborationURL,
|
||||
});
|
||||
const onLocalSyncedHandler = () => {
|
||||
setIsLocalSynced(true);
|
||||
};
|
||||
const onStatusHandler = (event: onStatusParameters) => {
|
||||
setYjsConnectionStatus(event.status);
|
||||
};
|
||||
const onSyncedHandler = (event: onSyncedParameters) => {
|
||||
setIsRemoteSynced(event.state);
|
||||
};
|
||||
const onAuthenticationFailedHandler = () => {
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken().then((result) => {
|
||||
if (result.data?.token) {
|
||||
socket.disconnect();
|
||||
setTimeout(() => {
|
||||
remote.configuration.token = result.data.token;
|
||||
socket.connect();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
local.on("synced", () => setLocalSynced(true));
|
||||
const remote = new HocuspocusProvider({
|
||||
websocketProvider: socket,
|
||||
name: documentName,
|
||||
url: collaborationURL,
|
||||
document: ydoc,
|
||||
token: collabQuery?.token,
|
||||
onAuthenticationFailed: onAuthenticationFailedHandler,
|
||||
onStatus: onStatusHandler,
|
||||
onSynced: onSyncedHandler,
|
||||
connect: true,
|
||||
preserveConnection: false,
|
||||
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken().then((result) => {
|
||||
if (result.data?.token) {
|
||||
remote.disconnect();
|
||||
setTimeout(() => {
|
||||
remote.configuration.token = result.data.token;
|
||||
remote.connect();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onStatus: (status) => {
|
||||
if (status.status === "connected") {
|
||||
setYjsConnectionStatus(status.status);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
local.on("synced", onLocalSyncedHandler);
|
||||
providersRef.current = { socket, local, remote };
|
||||
remote.on("synced", () => setRemoteSynced(true));
|
||||
remote.on("disconnect", () => {
|
||||
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
||||
});
|
||||
providersRef.current = { local, remote };
|
||||
setProvidersReady(true);
|
||||
} else {
|
||||
setProvidersReady(true);
|
||||
}
|
||||
// Only destroy on final unmount
|
||||
return () => {
|
||||
providersRef.current?.socket.destroy();
|
||||
providersRef.current?.remote.destroy();
|
||||
providersRef.current?.local.destroy();
|
||||
providersRef.current = null;
|
||||
};
|
||||
}, [pageId]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
// Handle token updates by reconnecting with new token
|
||||
if (providersRef.current?.remote && collabQuery?.token) {
|
||||
const currentToken = providersRef.current.remote.configuration.token;
|
||||
if (currentToken !== collabQuery.token) {
|
||||
// Token has changed, need to reconnect with new token
|
||||
providersRef.current.remote.disconnect();
|
||||
providersRef.current.remote.configuration.token = collabQuery.token;
|
||||
providersRef.current.remote.connect();
|
||||
}
|
||||
}
|
||||
}, [collabQuery?.token]);
|
||||
*/
|
||||
|
||||
// Only connect/disconnect on tab/idle, not destroy
|
||||
useEffect(() => {
|
||||
if (!providersReady || !providersRef.current) return;
|
||||
const socket = providersRef.current.socket;
|
||||
|
||||
const remoteProvider = providersRef.current.remote;
|
||||
if (
|
||||
isIdle &&
|
||||
documentState === "hidden" &&
|
||||
yjsConnectionStatus === WebSocketStatus.Connected
|
||||
remoteProvider.status === WebSocketStatus.Connected
|
||||
) {
|
||||
socket.disconnect();
|
||||
remoteProvider.disconnect();
|
||||
setIsCollabReady(false);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
documentState === "visible" &&
|
||||
yjsConnectionStatus === WebSocketStatus.Disconnected
|
||||
remoteProvider.status === WebSocketStatus.Disconnected
|
||||
) {
|
||||
resetIdle();
|
||||
socket.connect();
|
||||
remoteProvider.connect();
|
||||
setTimeout(() => setIsCollabReady(true), 500);
|
||||
}
|
||||
}, [isIdle, documentState, providersReady, resetIdle]);
|
||||
|
||||
// Attach here, to make sure the connection gets properly established
|
||||
providersRef.current?.remote.attach();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
if (!providersReady || !providersRef.current || !currentUser?.user) {
|
||||
return mainExtensions;
|
||||
}
|
||||
|
||||
const remoteProvider = providersRef.current.remote;
|
||||
|
||||
if (!remoteProvider || !currentUser?.user) return mainExtensions;
|
||||
return [
|
||||
...mainExtensions,
|
||||
...collabExtensions(remoteProvider, currentUser?.user),
|
||||
];
|
||||
}, [providersReady, currentUser?.user]);
|
||||
}, [remoteProvider, currentUser?.user]);
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
@@ -254,30 +254,16 @@ export default function PageEditor({
|
||||
}
|
||||
},
|
||||
},
|
||||
handlePaste: (_view, event) => {
|
||||
if (!editorRef.current) return false;
|
||||
|
||||
return handlePaste(
|
||||
editorRef.current,
|
||||
event,
|
||||
pageId,
|
||||
currentUser?.user.id,
|
||||
);
|
||||
},
|
||||
handleDrop: (_view, event, _slice, moved) => {
|
||||
if (!editorRef.current) return false;
|
||||
|
||||
return handleFileDrop(editorRef.current, event, moved, pageId);
|
||||
},
|
||||
handlePaste: (view, event, slice) =>
|
||||
handlePaste(view, event, pageId, currentUser?.user.id),
|
||||
handleDrop: (view, event, _slice, moved) =>
|
||||
handleFileDrop(view, event, moved, pageId),
|
||||
},
|
||||
onCreate({ editor }) {
|
||||
if (editor) {
|
||||
// @ts-ignore
|
||||
setEditor(editor);
|
||||
// @ts-ignore
|
||||
editor.storage.pageId = pageId;
|
||||
handleScrollTo(editor);
|
||||
editorRef.current = editor;
|
||||
}
|
||||
},
|
||||
onUpdate({ editor }) {
|
||||
@@ -287,7 +273,7 @@ export default function PageEditor({
|
||||
debouncedUpdateContent(editorJson);
|
||||
},
|
||||
},
|
||||
[pageId, editable, extensions],
|
||||
[pageId, editable, remoteProvider],
|
||||
);
|
||||
|
||||
const editorIsEditable = useEditorState({
|
||||
@@ -343,17 +329,30 @@ export default function PageEditor({
|
||||
setAsideState({ tab: "", isAsideOpen: false });
|
||||
}, [pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remoteProvider?.status === WebSocketStatus.Connecting) {
|
||||
const timeout = setTimeout(() => {
|
||||
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
||||
}, 5000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [remoteProvider?.status]);
|
||||
|
||||
const isSynced = isLocalSynced && isRemoteSynced;
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (yjsConnectionStatus === WebSocketStatus.Connecting || !isSynced) {
|
||||
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
||||
const collabReadyTimeout = setTimeout(() => {
|
||||
if (
|
||||
!isCollabReady &&
|
||||
isSynced &&
|
||||
remoteProvider?.status === WebSocketStatus.Connected
|
||||
) {
|
||||
setIsCollabReady(true);
|
||||
}
|
||||
}, 7500);
|
||||
}, 500);
|
||||
return () => clearTimeout(collabReadyTimeout);
|
||||
}, [isRemoteSynced, isLocalSynced, remoteProvider?.status]);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [yjsConnectionStatus, isSynced]);
|
||||
useEffect(() => {
|
||||
// Only honor user default page edit mode preference and permissions
|
||||
if (editor) {
|
||||
@@ -375,13 +374,12 @@ export default function PageEditor({
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasConnectedOnceRef.current &&
|
||||
yjsConnectionStatus === WebSocketStatus.Connected &&
|
||||
isSynced
|
||||
remoteProvider?.status === WebSocketStatus.Connected
|
||||
) {
|
||||
hasConnectedOnceRef.current = true;
|
||||
setShowStatic(false);
|
||||
}
|
||||
}, [yjsConnectionStatus, isSynced]);
|
||||
}, [remoteProvider?.status]);
|
||||
|
||||
if (showStatic) {
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import "@/features/editor/styles/index.css";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { EditorProvider } from "@tiptap/react";
|
||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||
import { Document } from "@tiptap/extension-document";
|
||||
import { Heading, UniqueID } from "@docmost/editor-ext";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { Text } from "@tiptap/extension-text";
|
||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||
import { useAtom } from "jotai";
|
||||
import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
import { useEditorScroll } from "./hooks/use-editor-scroll";
|
||||
|
||||
interface PageEditorProps {
|
||||
title: string;
|
||||
@@ -22,34 +21,9 @@ export default function ReadonlyPageEditor({
|
||||
pageId,
|
||||
}: PageEditorProps) {
|
||||
const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom);
|
||||
const isComponentMounted = useRef(false);
|
||||
const editorCreated = useRef(false);
|
||||
|
||||
const canScroll = useCallback(
|
||||
() => isComponentMounted.current && editorCreated.current,
|
||||
[isComponentMounted, editorCreated],
|
||||
);
|
||||
const initialScrollTo = window.location.hash
|
||||
? window.location.hash.slice(1)
|
||||
: "";
|
||||
const { handleScrollTo } = useEditorScroll({ canScroll, initialScrollTo });
|
||||
|
||||
useEffect(() => {
|
||||
isComponentMounted.current = true;
|
||||
}, []);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const filteredExtensions = mainExtensions.filter(
|
||||
(ext) => ext.name !== "uniqueID",
|
||||
);
|
||||
|
||||
return [
|
||||
...filteredExtensions,
|
||||
UniqueID.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
updateDocument: false,
|
||||
}),
|
||||
];
|
||||
return [...mainExtensions];
|
||||
}, []);
|
||||
|
||||
const titleExtensions = [
|
||||
@@ -81,14 +55,10 @@ export default function ReadonlyPageEditor({
|
||||
onCreate={({ editor }) => {
|
||||
if (editor) {
|
||||
if (pageId) {
|
||||
// @ts-ignore
|
||||
editor.storage.pageId = pageId;
|
||||
}
|
||||
// @ts-ignore
|
||||
setReadOnlyEditor(editor);
|
||||
|
||||
handleScrollTo(editor);
|
||||
editorCreated.current = true;
|
||||
}
|
||||
}}
|
||||
></EditorProvider>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-carets__caret {
|
||||
.collaboration-cursor__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-carets__label {
|
||||
.collaboration-cursor__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
);
|
||||
color: light-dark(
|
||||
var(--mantine-color-default-color),
|
||||
var(--mantine-color-white)
|
||||
var(--mantine-color-dark-0)
|
||||
);
|
||||
font-size: var(--mantine-font-size-md);
|
||||
line-height: var(--mantine-line-height-xl);
|
||||
@@ -115,7 +115,7 @@
|
||||
}
|
||||
|
||||
& > .react-renderer {
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
|
||||
&:first-child {
|
||||
@@ -141,7 +141,7 @@
|
||||
|
||||
.selection,
|
||||
*::selection {
|
||||
background-color: light-dark(Highlight, var(--mantine-color-gray-7));
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
.comment-mark {
|
||||
@@ -186,39 +186,6 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ProseMirror > h1,
|
||||
.ProseMirror > h2,
|
||||
.ProseMirror > h3,
|
||||
.ProseMirror > h4,
|
||||
.ProseMirror > h5,
|
||||
.ProseMirror > h6 {
|
||||
|
||||
> .link-btn {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
> .link-btn > .link-btn-content {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transition: opacity 0.15s ease;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&:hover > .link-btn > .link-btn-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
scroll-margin-top: 80px; /* match your header height */
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
@@ -242,3 +209,4 @@
|
||||
.actionIconGroup {
|
||||
background: var(--mantine-color-body);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
/* Highlight colors with dark mode support */
|
||||
|
||||
.ProseMirror {
|
||||
/* Blue */
|
||||
mark[data-color="#98d8f2"] {
|
||||
background-color: light-dark(
|
||||
rgb(224 242 254),
|
||||
rgba(37, 99, 235, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
mark[data-color="#7edb6c"] {
|
||||
background-color: light-dark(
|
||||
rgb(220 252 231),
|
||||
rgba(0, 138, 0, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
mark[data-color="#e0d6ed"] {
|
||||
background-color: light-dark(
|
||||
rgb(243 232 255),
|
||||
rgba(147, 51, 234, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
mark[data-color="#ffc6c2"] {
|
||||
background-color: light-dark(
|
||||
rgb(255 228 230),
|
||||
rgba(224, 0, 0, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
mark[data-color="#faf594"] {
|
||||
background-color: light-dark(
|
||||
rgb(254 249 195),
|
||||
rgba(234, 179, 8, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
mark[data-color="#f5c8a9"] {
|
||||
background-color: light-dark(
|
||||
rgb(251, 236, 221),
|
||||
rgba(255, 165, 0, 0.45)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Pink */
|
||||
mark[data-color="#f5cfe0"] {
|
||||
background-color: light-dark(
|
||||
rgb(252, 241, 246),
|
||||
rgba(186, 64, 129, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Gray */
|
||||
mark[data-color="#dfdfd7"] {
|
||||
background-color: light-dark(
|
||||
rgb(238 238 235),
|
||||
rgba(168, 162, 158, 0.35)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Brown */
|
||||
mark[data-color="#d7c4b7"] {
|
||||
background-color: light-dark(
|
||||
rgb(215 196 183),
|
||||
rgba(146, 64, 14, 0.35)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Color selector trigger button styles */
|
||||
.color-selector-trigger[data-text-color="#2563EB"] {
|
||||
color: #2563EB !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#008A00"] {
|
||||
color: #008A00 !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#9333EA"] {
|
||||
color: #9333EA !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#E00000"] {
|
||||
color: #E00000 !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#EAB308"] {
|
||||
color: #EAB308 !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#FFA500"] {
|
||||
color: #FFA500 !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#BA4081"] {
|
||||
color: #BA4081 !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#A8A29E"] {
|
||||
color: #A8A29E !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-text-color="#92400E"] {
|
||||
color: #92400E !important;
|
||||
}
|
||||
|
||||
/* Highlight background colors with light-dark support - solid colors for trigger button */
|
||||
.color-selector-trigger[data-highlight-color="#98d8f2"] {
|
||||
background-color: light-dark(
|
||||
rgb(224 242 254),
|
||||
rgb(30 64 175)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#7edb6c"] {
|
||||
background-color: light-dark(
|
||||
rgb(220 252 231),
|
||||
rgb(21 128 61)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#e0d6ed"] {
|
||||
background-color: light-dark(
|
||||
rgb(243 232 255),
|
||||
rgb(107 33 168)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#ffc6c2"] {
|
||||
background-color: light-dark(
|
||||
rgb(255 228 230),
|
||||
rgb(185 28 28)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#faf594"] {
|
||||
background-color: light-dark(
|
||||
rgb(254 249 195),
|
||||
rgb(161 98 7)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#f5c8a9"] {
|
||||
background-color: light-dark(
|
||||
rgb(251 236 221),
|
||||
rgb(194 65 12)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#f5cfe0"] {
|
||||
background-color: light-dark(
|
||||
rgb(252 241 246),
|
||||
rgb(157 23 77)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#dfdfd7"] {
|
||||
background-color: light-dark(
|
||||
rgb(238 238 235),
|
||||
rgb(115 115 115)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.color-selector-trigger[data-highlight-color="#d7c4b7"] {
|
||||
background-color: light-dark(
|
||||
rgb(215 196 183),
|
||||
rgb(120 53 15)
|
||||
) !important;
|
||||
}
|
||||
@@ -12,4 +12,3 @@
|
||||
@import "./find.css";
|
||||
@import "./mention.css";
|
||||
@import "./ordered-list.css";
|
||||
@import "./highlight.css";
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.mantine-AppShell-main {
|
||||
padding: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,7 @@ export function TitleEditor({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const anchorId = window.location.hash
|
||||
? window.location.hash.substring(1)
|
||||
: undefined;
|
||||
const pageSlug = buildPageUrl(spaceSlug, slugId, title, anchorId);
|
||||
const pageSlug = buildPageUrl(spaceSlug, slugId, title);
|
||||
navigate(pageSlug, { replace: true });
|
||||
}, [title]);
|
||||
|
||||
@@ -157,9 +154,7 @@ export function TitleEditor({
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
// guard against Cannot access view['hasFocus'] error
|
||||
if (!titleEditor?.isInitialized) return;
|
||||
titleEditor?.commands?.focus("end");
|
||||
titleEditor?.commands.focus("end");
|
||||
}, 500);
|
||||
}, [titleEditor]);
|
||||
|
||||
@@ -197,43 +192,10 @@ export function TitleEditor({
|
||||
const { key } = event;
|
||||
const { $head } = titleEditor.state.selection;
|
||||
|
||||
if (key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
const { $from } = titleEditor.state.selection;
|
||||
const titleText = titleEditor.getText();
|
||||
|
||||
// Get the text offset within the heading node (not document position)
|
||||
const textOffset = $from.parentOffset;
|
||||
|
||||
const textAfterCursor = titleText.slice(textOffset);
|
||||
|
||||
// Delete text after cursor from title (this will be in undo history)
|
||||
const endPos = titleEditor.state.doc.content.size;
|
||||
if (textAfterCursor) {
|
||||
titleEditor.commands.deleteRange({ from: $from.pos, to: endPos });
|
||||
}
|
||||
|
||||
// Don't add to history so undo in page editor won't remove this split
|
||||
pageEditor
|
||||
.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.setMeta("addToHistory", false);
|
||||
return true;
|
||||
})
|
||||
.insertContentAt(0, {
|
||||
type: "paragraph",
|
||||
content: textAfterCursor
|
||||
? [{ type: "text", text: textAfterCursor }]
|
||||
: undefined,
|
||||
})
|
||||
.focus("start")
|
||||
.run();
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldFocusEditor =
|
||||
key === "ArrowDown" || (key === "ArrowRight" && !$head.nodeAfter);
|
||||
key === "Enter" ||
|
||||
key === "ArrowDown" ||
|
||||
(key === "ArrowRight" && !$head.nodeAfter);
|
||||
|
||||
if (shouldFocusEditor) {
|
||||
pageEditor.commands.focus("start");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { IFileTask } from "@/features/file-task/types/file-task.types.ts";
|
||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||
import { IApiKey } from "@/ee/api-key";
|
||||
|
||||
export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> {
|
||||
const req = await api.post<IFileTask>("/file-tasks/info", {
|
||||
@@ -10,10 +8,7 @@ export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getFileTasks(
|
||||
params?: QueryParams,
|
||||
): Promise<IPagination<IFileTask>> {
|
||||
const req = await api.post("/file-tasks", { ...params });
|
||||
export async function getFileTasks(): Promise<IFileTask[]> {
|
||||
const req = await api.post<IFileTask[]>("/file-tasks");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { zodResolver } from 'mantine-form-zod-resolver';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(2).max(100),
|
||||
name: z.string().trim().min(2).max(50),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { zodResolver } from "mantine-form-zod-resolver";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(100),
|
||||
name: z.string().min(2).max(50),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { IGroup } from "@/features/group/types/group.types.ts";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { queryClient } from "@/main.tsx";
|
||||
import { getSpaces } from "@/features/space/services/space-service.ts";
|
||||
import { getGroupMembers } from "@/features/group/services/group-service.ts";
|
||||
import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||
|
||||
export default function GroupList() {
|
||||
const { t } = useTranslation();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGetGroupsQuery({ cursor });
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGetGroupsQuery({ page });
|
||||
|
||||
const prefetchGroupMembers = (groupId: string) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["groupMembers", groupId, {}],
|
||||
queryFn: () => getGroupMembers(groupId, {}),
|
||||
queryKey: ["groupMembers", groupId, { page: 1 }],
|
||||
queryFn: () => getGroupMembers(groupId, { page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,10 +50,10 @@ export default function GroupList() {
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<IconGroupCircle />
|
||||
<div style={{ minWidth: 0, overflow: "hidden" }}>
|
||||
<AutoTooltipText fz="sm" fw={500} lineClamp={1}>
|
||||
<div>
|
||||
<Text fz="sm" fw={500} lineClamp={1}>
|
||||
{group.name}
|
||||
</AutoTooltipText>
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||
{group.description}
|
||||
</Text>
|
||||
@@ -84,10 +84,10 @@ export default function GroupList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
useRemoveGroupMemberMutation,
|
||||
} from "@/features/group/queries/group-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
@@ -12,13 +12,12 @@ import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
|
||||
export default function GroupMembersList() {
|
||||
const { t } = useTranslation();
|
||||
const { groupId } = useParams();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId, { cursor });
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId, { page });
|
||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
@@ -108,10 +107,10 @@ export default function GroupMembersList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const historyAtoms = atom<boolean>(false);
|
||||
export const activeHistoryIdAtom = atom<string>("");
|
||||
export const activeHistoryPrevIdAtom = atom<string>("");
|
||||
export const highlightChangesAtom = atom<boolean>(true);
|
||||
|
||||
export type DiffCounts = { added: number; deleted: number; total: number };
|
||||
export const diffCountsAtom = atom<DiffCounts | null>(null);
|
||||
export const activeHistoryIdAtom = atom<string>('');
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 60px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectorWrapper {
|
||||
padding: var(--mantine-spacing-sm);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selector {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
max-height: rem(300px);
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
|
||||
&[data-combobox-selected] {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
|
||||
.editorArea {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
padding: var(--mantine-spacing-md);
|
||||
padding-bottom: rem(60px);
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
padding-bottom: rem(70px);
|
||||
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.floatingBar {
|
||||
position: fixed;
|
||||
bottom: var(--mantine-spacing-md);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
.history {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-2),
|
||||
var(--mantine-color-dark-8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.historyEditor {
|
||||
:global(.ProseMirror) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
& :global(.history-diff-added) {
|
||||
background: light-dark(#e1f3f2, #01654a) !important;
|
||||
color: light-dark(#007b69, #cafff7) !important;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
& :global(.history-diff-deleted) {
|
||||
text-decoration: line-through;
|
||||
color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4));
|
||||
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1));
|
||||
border-radius: rem(2px);
|
||||
padding: 0 rem(2px);
|
||||
}
|
||||
|
||||
& :global(.history-diff-node-added) {
|
||||
outline: rem(2px) solid
|
||||
light-dark(var(--mantine-color-teal-5), var(--mantine-color-teal-7));
|
||||
outline-offset: rem(2px);
|
||||
border-radius: rem(4px);
|
||||
}
|
||||
|
||||
& :global(.history-diff-node-deleted) {
|
||||
opacity: 0.5;
|
||||
outline: rem(2px) dashed
|
||||
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||
outline-offset: rem(4px);
|
||||
border-radius: rem(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-2),
|
||||
var(--mantine-color-dark-8)
|
||||
);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-height: rem(700px);
|
||||
width: rem(250px);
|
||||
padding: var(--mantine-spacing-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: rem(1px) solid
|
||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.sidebarFlex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebarMain {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebarRightSection {
|
||||
flex: 1;
|
||||
padding: rem(16px) rem(40px);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user