mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-10 04:22:00 +10:00
Merge branch 'docmost:main' into main
This commit is contained in:
@ -21,6 +21,9 @@ AWS_S3_BUCKET=
|
|||||||
AWS_S3_ENDPOINT=
|
AWS_S3_ENDPOINT=
|
||||||
AWS_S3_FORCE_PATH_STYLE=
|
AWS_S3_FORCE_PATH_STYLE=
|
||||||
|
|
||||||
|
# default: 50mb
|
||||||
|
FILE_UPLOAD_SIZE_LIMIT=
|
||||||
|
|
||||||
# options: smtp | postmark
|
# options: smtp | postmark
|
||||||
MAIL_DRIVER=smtp
|
MAIL_DRIVER=smtp
|
||||||
MAIL_FROM_ADDRESS=hello@example.com
|
MAIL_FROM_ADDRESS=hello@example.com
|
||||||
@ -37,3 +40,5 @@ SMTP_IGNORETLS=false
|
|||||||
# Postmark driver config
|
# Postmark driver config
|
||||||
POSTMARK_TOKEN=
|
POSTMARK_TOKEN=
|
||||||
|
|
||||||
|
# for custom drawio server
|
||||||
|
DRAWIO_URL=
|
||||||
@ -30,6 +30,9 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
|
|||||||
COPY --from=builder /app/package.json /app/package.json
|
COPY --from=builder /app/package.json /app/package.json
|
||||||
COPY --from=builder /app/pnpm*.yaml /app/
|
COPY --from=builder /app/pnpm*.yaml /app/
|
||||||
|
|
||||||
|
# Copy patches
|
||||||
|
COPY --from=builder /app/patches /app/patches
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
RUN chown -R node:node /app
|
RUN chown -R node:node /app
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
36
apps/client/eslint.config.mjs
Normal file
36
apps/client/eslint.config.mjs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import pluginQuery from "@tanstack/eslint-plugin-query";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
"@tanstack/query": pluginQuery,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -1,72 +1,79 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.1",
|
"version": "0.6.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.7.1",
|
"@casl/ability": "^6.7.2",
|
||||||
"@casl/react": "^4.0.0",
|
"@casl/react": "^4.0.0",
|
||||||
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@excalidraw/excalidraw": "^0.17.6",
|
"@excalidraw/excalidraw": "^0.17.6",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.14.2",
|
||||||
"@mantine/form": "^7.12.2",
|
"@mantine/form": "^7.14.2",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.14.2",
|
||||||
"@mantine/modals": "^7.12.2",
|
"@mantine/modals": "^7.14.2",
|
||||||
"@mantine/notifications": "^7.12.2",
|
"@mantine/notifications": "^7.14.2",
|
||||||
"@mantine/spotlight": "^7.12.2",
|
"@mantine/spotlight": "^7.14.2",
|
||||||
"@tabler/icons-react": "^3.14.0",
|
"@tabler/icons-react": "^3.22.0",
|
||||||
"@tanstack/react-query": "^5.53.2",
|
"@tanstack/react-query": "^5.61.4",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.8",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^4.1.0",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jotai": "^2.9.3",
|
"i18next": "^23.14.0",
|
||||||
|
"i18next-http-backend": "^2.6.1",
|
||||||
|
"jotai": "^2.10.3",
|
||||||
"jotai-optics": "^0.4.0",
|
"jotai-optics": "^0.4.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"lowlight": "^3.1.0",
|
"lowlight": "^3.2.0",
|
||||||
"mermaid": "^11.0.2",
|
"mermaid": "^11.4.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-arborist": "^3.4.0",
|
"react-arborist": "^3.4.0",
|
||||||
"react-clear-modal": "^2.0.9",
|
"react-clear-modal": "^2.0.11",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drawio": "^0.2.0",
|
"react-drawio": "^1.0.1",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-i18next": "^15.0.1",
|
||||||
"socket.io-client": "^4.7.5",
|
"react-router-dom": "^7.0.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap-extension-global-drag-handle": "^0.1.12",
|
"tiptap-extension-global-drag-handle": "^0.1.16",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/eslint-plugin-query": "^5.53.0",
|
"@eslint/js": "^9.16.0",
|
||||||
|
"@tanstack/eslint-plugin-query": "^5.62.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/node": "22.5.2",
|
"@types/node": "22.10.0",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"eslint": "^9.15.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint": "^9.9.1",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"eslint-plugin-react-refresh": "^0.4.11",
|
"globals": "^15.13.0",
|
||||||
"optics-ts": "^2.4.1",
|
"optics-ts": "^2.4.1",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.49",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.4.1",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.2"
|
"typescript-eslint": "^8.17.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
342
apps/client/public/locales/de-DE/translation.json
Normal file
342
apps/client/public/locales/de-DE/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "Konto",
|
||||||
|
"Active": "Aktiv",
|
||||||
|
"Add": "Hinzufügen",
|
||||||
|
"Add group members": "Gruppenmitglieder hinzufügen",
|
||||||
|
"Add groups": "Gruppen hinzufügen",
|
||||||
|
"Add members": "Mitglieder hinzufügen",
|
||||||
|
"Add to groups": "Zu Gruppen hinzufügen",
|
||||||
|
"Add space members": "Bereichsmitglieder hinzufügen",
|
||||||
|
"Admin": "Administrator",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||||
|
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||||
|
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diesen Benutzer aus der Gruppe entfernen möchten? Der Benutzer verliert den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||||
|
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sind Sie sicher, dass Sie diesen Benutzer aus dem Bereich entfernen möchten? Der Benutzer verliert den gesamten Zugang zu diesem Bereich.",
|
||||||
|
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sind Sie sicher, dass Sie diese Version wiederherstellen möchten? Alle nicht versionierten Änderungen gehen verloren.",
|
||||||
|
"Can become members of groups and spaces in workspace": "Kann Mitglied von Gruppen und Bereichen im Arbeitsbereich werden",
|
||||||
|
"Can create and edit pages in space.": "Kann Seiten im Bereich erstellen und bearbeiten.",
|
||||||
|
"Can edit": "Kann bearbeiten",
|
||||||
|
"Can manage workspace": "Kann Arbeitsbereich verwalten",
|
||||||
|
"Can manage workspace but cannot delete it": "Kann Arbeitsbereich verwalten, aber nicht löschen",
|
||||||
|
"Can view": "Kann anzeigen",
|
||||||
|
"Can view pages in space but not edit.": "Kann Seiten im Bereich anzeigen, aber nicht bearbeiten.",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Change email": "E-Mail ändern",
|
||||||
|
"Change password": "Passwort ändern",
|
||||||
|
"Change photo": "Foto ändern",
|
||||||
|
"Choose a role": "Wählen Sie eine Rolle",
|
||||||
|
"Choose your preferred color scheme.": "Wählen Sie Ihr bevorzugtes Farbschema.",
|
||||||
|
"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 link": "Link kopieren",
|
||||||
|
"Create": "Erstellen",
|
||||||
|
"Create group": "Gruppe erstellen",
|
||||||
|
"Create page": "Seite erstellen",
|
||||||
|
"Create space": "Bereich erstellen",
|
||||||
|
"Create workspace": "Arbeitsbereich erstellen",
|
||||||
|
"Current password": "Aktuelles Passwort",
|
||||||
|
"Dark": "Dunkel",
|
||||||
|
"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? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||||
|
"Description": "Beschreibung",
|
||||||
|
"Details": "Einzelheiten",
|
||||||
|
"e.g ACME": "z.B. ACME",
|
||||||
|
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||||
|
"e.g Developers": "z.B. Entwickler",
|
||||||
|
"e.g Group for developers": "z.B. Gruppe für Entwickler",
|
||||||
|
"e.g product": "z.B. Produkt",
|
||||||
|
"e.g Product Team": "z.B. Produktteam",
|
||||||
|
"e.g Sales": "z.B. Vertrieb",
|
||||||
|
"e.g Space for product team": "z.B. Bereich für das Produktteam",
|
||||||
|
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit",
|
||||||
|
"Edit": "Bearbeiten",
|
||||||
|
"Edit group": "Gruppe bearbeiten",
|
||||||
|
"Email": "E-Mail",
|
||||||
|
"Enter a strong password": "Geben Sie ein starkes Passwort ein",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
|
||||||
|
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben",
|
||||||
|
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
|
||||||
|
"enter your full name": "Geben Sie Ihren vollständigen Namen ein",
|
||||||
|
"Enter your new password": "Geben Sie Ihr neues Passwort ein",
|
||||||
|
"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 der Seitengeschichte.",
|
||||||
|
"Export": "Exportieren",
|
||||||
|
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||||
|
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||||
|
"Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten",
|
||||||
|
"Failed to import pages": "Import der Seiten fehlgeschlagen",
|
||||||
|
"Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
|
||||||
|
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
|
||||||
|
"Full access": "Voller Zugriff",
|
||||||
|
"Full page width": "Volle Seitenbreite",
|
||||||
|
"Full width": "Volle Breite",
|
||||||
|
"General": "Allgemein",
|
||||||
|
"Group": "Gruppe",
|
||||||
|
"Group description": "Gruppenbeschreibung",
|
||||||
|
"Group name": "Gruppenname",
|
||||||
|
"Groups": "Gruppen",
|
||||||
|
"Has full access to space settings and pages.": "Hat vollen Zugriff auf die Bereichseinstellungen und Seiten.",
|
||||||
|
"Home": "Startseite",
|
||||||
|
"Import pages": "Seiten importieren",
|
||||||
|
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
|
||||||
|
"Importing pages": "Seiten werden importiert",
|
||||||
|
"invalid invitation link": "ungültiger Einladungslink",
|
||||||
|
"Invitation signup": "Einladung zur Anmeldung",
|
||||||
|
"Invite by email": "Einladen per E-Mail",
|
||||||
|
"Invite members": "Mitglieder einladen",
|
||||||
|
"Invite new members": "Neue Mitglieder einladen",
|
||||||
|
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
|
||||||
|
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
|
||||||
|
"Join the workspace": "Dem Arbeitsbereich beitreten",
|
||||||
|
"Language": "Sprache",
|
||||||
|
"Light": "Hell",
|
||||||
|
"Link copied": "Link kopiert",
|
||||||
|
"Login": "Anmelden",
|
||||||
|
"Logout": "Abmelden",
|
||||||
|
"Manage Group": "Gruppe verwalten",
|
||||||
|
"Manage members": "Mitglieder verwalten",
|
||||||
|
"member": "Mitglied",
|
||||||
|
"Member": "Mitglied",
|
||||||
|
"members": "Mitglieder",
|
||||||
|
"Members": "Mitglieder",
|
||||||
|
"My preferences": "Meine Vorlieben",
|
||||||
|
"My Profile": "Mein Profil",
|
||||||
|
"My profile": "Mein Profil",
|
||||||
|
"Name": "Name",
|
||||||
|
"New email": "Neue E-Mail",
|
||||||
|
"New page": "Neue Seite",
|
||||||
|
"New password": "Neues Passwort",
|
||||||
|
"No group found": "Keine Gruppe gefunden",
|
||||||
|
"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",
|
||||||
|
"Overview": "Überblick",
|
||||||
|
"Owner": "Besitzer",
|
||||||
|
"page": "Seite",
|
||||||
|
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||||
|
"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",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"Password changed successfully": "Passwort erfolgreich geändert",
|
||||||
|
"Pending": "Ausstehend",
|
||||||
|
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
|
||||||
|
"Preferences": "Vorlieben",
|
||||||
|
"Print PDF": "PDF drucken",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recently updated": "Kürzlich aktualisiert",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"Remove group member": "Gruppenmitglied entfernen",
|
||||||
|
"Remove space member": "Bereichsmitglied entfernen",
|
||||||
|
"Restore": "Wiederherstellen",
|
||||||
|
"Role": "Rolle",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"Search": "Suche",
|
||||||
|
"Search for groups": "Suche nach Gruppen",
|
||||||
|
"Search for users": "Suche nach Benutzern",
|
||||||
|
"Search for users and groups": "Suche nach Benutzern und Gruppen",
|
||||||
|
"Search...": "Suche...",
|
||||||
|
"Select language": "Sprache auswählen",
|
||||||
|
"Select role": "Rolle auswählen",
|
||||||
|
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen",
|
||||||
|
"Select theme": "Design auswählen",
|
||||||
|
"Send invitation": "Einladung senden",
|
||||||
|
"Settings": "Einstellungen",
|
||||||
|
"Setup workspace": "Arbeitsbereich einrichten",
|
||||||
|
"Sign In": "Anmelden",
|
||||||
|
"Sign Up": "Registrieren",
|
||||||
|
"Slug": "Slug",
|
||||||
|
"Space": "Bereich",
|
||||||
|
"Space description": "Bereichsbeschreibung",
|
||||||
|
"Space menu": "Bereichsmenü",
|
||||||
|
"Space name": "Bereichsname",
|
||||||
|
"Space settings": "Bereichseinstellungen",
|
||||||
|
"Space slug": "Slug des Bereichs",
|
||||||
|
"Spaces": "Bereiche",
|
||||||
|
"Spaces you belong to": "Bereiche, denen Sie angehören",
|
||||||
|
"No space found": "Keine Bereiche gefunden",
|
||||||
|
"Search for spaces": "Nach Bereichen suchen",
|
||||||
|
"Start typing to search...": "Anfangen zu tippen, um zu suchen...",
|
||||||
|
"Status": "Status",
|
||||||
|
"Successfully imported": "Erfolgreich importiert",
|
||||||
|
"Successfully restored": "Erfolgreich wiederhergestellt",
|
||||||
|
"System settings": "Systemeinstellungen",
|
||||||
|
"Theme": "Design",
|
||||||
|
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
|
||||||
|
"Toggle full page width": "Volle Seitenbreite umschalten",
|
||||||
|
"Unable to import pages. Please try again.": "Seiten konnten nicht importiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"untitled": "ohne Titel",
|
||||||
|
"Untitled": "Ohne Titel",
|
||||||
|
"Updated successfully": "Erfolgreich aktualisiert",
|
||||||
|
"User": "Benutzer",
|
||||||
|
"Workspace": "Arbeitsbereich",
|
||||||
|
"Workspace Name": "Arbeitsbereichsname",
|
||||||
|
"Workspace settings": "Arbeitsbereich-Einstellungen",
|
||||||
|
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
|
||||||
|
"Your Email": "Ihre E-Mail",
|
||||||
|
"Your import is complete.": "Ihr Import ist abgeschlossen.",
|
||||||
|
"Your name": "Ihr Name",
|
||||||
|
"Your Name": "Ihr Name",
|
||||||
|
"Your password": "Ihr Passwort",
|
||||||
|
"Your password must be a minimum of 8 characters.": "Ihr Passwort muss mindestens 8 Zeichen lang sein.",
|
||||||
|
"Sidebar toggle": "Seitenleiste umschalten",
|
||||||
|
"Comments": "Kommentare",
|
||||||
|
"404 page not found": "404 Seite nicht gefunden",
|
||||||
|
"Sorry, we can't find the page you are looking for.": "Entschuldigung, wir können die gesuchte Seite nicht finden.",
|
||||||
|
"Take me back to homepage": "Zurück zur Startseite",
|
||||||
|
"Forgot password": "Passwort vergessen",
|
||||||
|
"Forgot your password?": "Passwort vergessen?",
|
||||||
|
"A password reset link has been sent to your email. Please check your inbox.": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||||
|
"Send reset link": "Zurücksetzungslink senden",
|
||||||
|
"Password reset": "Passwort zurücksetzen",
|
||||||
|
"Your new password": "Ihr neues Passwort",
|
||||||
|
"Set password": "Passwort festlegen",
|
||||||
|
"Write a comment": "Einen Kommentar schreiben",
|
||||||
|
"Reply...": "Antworten...",
|
||||||
|
"Error loading comments.": "Fehler beim Laden der Kommentare.",
|
||||||
|
"No comments yet.": "Noch keine Kommentare.",
|
||||||
|
"Edit comment": "Kommentar bearbeiten",
|
||||||
|
"Delete comment": "Kommentar löschen",
|
||||||
|
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||||
|
"Comment created successfully": "Kommentar erfolgreich erstellt",
|
||||||
|
"Error creating comment": "Fehler beim Erstellen des Kommentars",
|
||||||
|
"Comment updated successfully": "Kommentar erfolgreich aktualisiert",
|
||||||
|
"Failed to update comment": "Aktualisierung des Kommentars fehlgeschlagen",
|
||||||
|
"Comment deleted successfully": "Kommentar erfolgreich gelöscht",
|
||||||
|
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
|
||||||
|
"Comment resolved successfully": "Kommentar erfolgreich gelöst",
|
||||||
|
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
|
||||||
|
"Revoke invitation": "Einladung widerrufen",
|
||||||
|
"Revoke": "Widerrufen",
|
||||||
|
"Don't": "Nicht",
|
||||||
|
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sind Sie sicher, dass Sie diese Einladung widerrufen möchten? Der Benutzer kann dem Arbeitsbereich nicht beitreten.",
|
||||||
|
"Resend invitation": "Einladung erneut senden",
|
||||||
|
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
|
||||||
|
"Invite link": "Einladungslink",
|
||||||
|
"Copy": "Kopieren",
|
||||||
|
"Copied": "Kopiert",
|
||||||
|
"Select a user": "Benutzer auswählen",
|
||||||
|
"Select a group": "Gruppe auswählen",
|
||||||
|
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
|
||||||
|
"Delete space": "Bereich löschen",
|
||||||
|
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
|
||||||
|
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
|
||||||
|
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
|
||||||
|
"Format": "Format",
|
||||||
|
"Include subpages": "Unterseiten einbeziehen",
|
||||||
|
"Include attachments": "Anhänge einbeziehen",
|
||||||
|
"Select export format": "Exportformat auswählen",
|
||||||
|
"Export failed:": "Export fehlgeschlagen:",
|
||||||
|
"export error": "Exportfehler",
|
||||||
|
"Export page": "Seite exportieren",
|
||||||
|
"Export space": "Bereich exportieren",
|
||||||
|
"Export {{type}}": "Exportiere {{type}}",
|
||||||
|
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
|
||||||
|
"Align left": "Links ausrichten",
|
||||||
|
"Align right": "Rechts ausrichten",
|
||||||
|
"Align center": "Zentrieren",
|
||||||
|
"Merge cells": "Zellen zusammenführen",
|
||||||
|
"Split cell": "Zelle teilen",
|
||||||
|
"Delete column": "Spalte löschen",
|
||||||
|
"Delete row": "Zeile löschen",
|
||||||
|
"Add left column": "Linke Spalte hinzufügen",
|
||||||
|
"Add right column": "Rechte Spalte hinzufügen",
|
||||||
|
"Add row above": "Zeile oben hinzufügen",
|
||||||
|
"Add row below": "Zeile unten hinzufügen",
|
||||||
|
"Delete table": "Tabelle löschen",
|
||||||
|
"Info": "Info",
|
||||||
|
"Success": "Erfolg",
|
||||||
|
"Warning": "Warnung",
|
||||||
|
"Danger": "Gefahr",
|
||||||
|
"Mermaid diagram error:": "Fehler im Mermaid-Diagramm:",
|
||||||
|
"Invalid Mermaid diagram": "Ungültiges Mermaid-Diagramm",
|
||||||
|
"Double-click to edit Draw.io diagram": "Zum Bearbeiten des Draw.io-Diagramms doppelklicken",
|
||||||
|
"Exit": "Beenden",
|
||||||
|
"Save & Exit": "Speichern & Beenden",
|
||||||
|
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
|
||||||
|
"Paste link": "Link einfügen",
|
||||||
|
"Edit link": "Link bearbeiten",
|
||||||
|
"Remove link": "Link entfernen",
|
||||||
|
"Add link": "Link hinzufügen",
|
||||||
|
"Please enter a valid url": "Bitte geben Sie eine gültige URL ein",
|
||||||
|
"Empty equation": "Leere Gleichung",
|
||||||
|
"Invalid equation": "Ungültige Gleichung",
|
||||||
|
"Color": "Farbe",
|
||||||
|
"Text color": "Textfarbe",
|
||||||
|
"Default": "Standard",
|
||||||
|
"Blue": "Blau",
|
||||||
|
"Green": "Grün",
|
||||||
|
"Purple": "Lila",
|
||||||
|
"Red": "Rot",
|
||||||
|
"Yellow": "Gelb",
|
||||||
|
"Orange": "Orange",
|
||||||
|
"Pink": "Rosa",
|
||||||
|
"Gray": "Grau",
|
||||||
|
"Embed link": "Link einbetten",
|
||||||
|
"Invalid {{provider}} embed link": "Ungültiger {{provider}}-Einbettungslink",
|
||||||
|
"Embed {{provider}}": "{{provider}} einbetten",
|
||||||
|
"Enter {{provider}} link to embed": "Geben Sie den Einbettungslink für {{provider}} ein",
|
||||||
|
"Bold": "Fett",
|
||||||
|
"Italic": "Kursiv",
|
||||||
|
"Underline": "Unterstreichen",
|
||||||
|
"Strike": "Durchstreichen",
|
||||||
|
"Code": "Code",
|
||||||
|
"Comment": "Kommentar",
|
||||||
|
"Text": "Text",
|
||||||
|
"Heading 1": "Überschrift 1",
|
||||||
|
"Heading 2": "Überschrift 2",
|
||||||
|
"Heading 3": "Überschrift 3",
|
||||||
|
"To-do List": "To-do-Liste",
|
||||||
|
"Bullet List": "Aufzählungsliste",
|
||||||
|
"Numbered List": "Nummerierte Liste",
|
||||||
|
"Blockquote": "Blockzitat",
|
||||||
|
"Just start typing with plain text.": "Tippen Sie einfach mit normalem Text los.",
|
||||||
|
"Track tasks with a to-do list.": "Verfolgen Sie Aufgaben mit einer To-do-Liste.",
|
||||||
|
"Big section heading.": "Große Abschnittsüberschrift.",
|
||||||
|
"Medium section heading.": "Mittlere Abschnittsüberschrift.",
|
||||||
|
"Small section heading.": "Kleine Abschnittsüberschrift.",
|
||||||
|
"Create a simple bullet list.": "Erstellen Sie eine einfache Aufzählungsliste.",
|
||||||
|
"Create a list with numbering.": "Erstellen Sie eine nummerierte Liste.",
|
||||||
|
"Create block quote.": "Erstellen Sie ein Blockzitat.",
|
||||||
|
"Insert code snippet.": "Code-Snippet einfügen.",
|
||||||
|
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
|
||||||
|
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
|
||||||
|
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
|
||||||
|
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||||
|
"Table": "Tabelle",
|
||||||
|
"Insert a table.": "Tabelle einfügen.",
|
||||||
|
"Insert collapsible block.": "Einklappbaren Block einfügen.",
|
||||||
|
"Video": "Video",
|
||||||
|
"Divider": "Trennlinie",
|
||||||
|
"Quote": "Zitat",
|
||||||
|
"Image": "Bild",
|
||||||
|
"File attachment": "Dateianhang",
|
||||||
|
"Toggle block": "Block umschalten",
|
||||||
|
"Callout": "Hinweisbox",
|
||||||
|
"Insert callout notice.": "Hinweisbox einfügen.",
|
||||||
|
"Math inline": "Mathe inline",
|
||||||
|
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
|
||||||
|
"Math block": "Matheblock",
|
||||||
|
"Insert math equation": "Mathe-Gleichung einfügen",
|
||||||
|
"Mermaid diagram": "Mermaid-Diagramm",
|
||||||
|
"Insert mermaid diagram": "Mermaid-Diagramm einfügen",
|
||||||
|
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
|
||||||
|
"Insert current date": "Aktuelles Datum einfügen",
|
||||||
|
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
|
||||||
|
"Multiple": "Mehrere",
|
||||||
|
"Heading {{level}}": "Überschrift {{level}}",
|
||||||
|
"Toggle title": "Titel umschalten",
|
||||||
|
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein",
|
||||||
|
"Names do not match": "Namen stimmen nicht überein",
|
||||||
|
"Today, {{time}}": "Heute, {{time}}",
|
||||||
|
"Yesterday, {{time}}": "Gestern, {{time}}"
|
||||||
|
}
|
||||||
342
apps/client/public/locales/en-US/translation.json
Normal file
342
apps/client/public/locales/en-US/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "Account",
|
||||||
|
"Active": "Active",
|
||||||
|
"Add": "Add",
|
||||||
|
"Add group members": "Add group members",
|
||||||
|
"Add groups": "Add groups",
|
||||||
|
"Add members": "Add members",
|
||||||
|
"Add to groups": "Add to groups",
|
||||||
|
"Add space members": "Add space members",
|
||||||
|
"Admin": "Admin",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||||
|
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||||
|
"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 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 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.": "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 become members of groups and spaces in workspace",
|
||||||
|
"Can create and edit pages in space.": "Can create and edit pages in space.",
|
||||||
|
"Can edit": "Can edit",
|
||||||
|
"Can manage workspace": "Can manage workspace",
|
||||||
|
"Can manage workspace but cannot delete it": "Can manage workspace but cannot delete it",
|
||||||
|
"Can view": "Can view",
|
||||||
|
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Change email": "Change email",
|
||||||
|
"Change password": "Change password",
|
||||||
|
"Change photo": "Change photo",
|
||||||
|
"Choose a role": "Choose a role",
|
||||||
|
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||||
|
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||||
|
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||||
|
"Confirm": "Confirm",
|
||||||
|
"Copy link": "Copy link",
|
||||||
|
"Create": "Create",
|
||||||
|
"Create group": "Create group",
|
||||||
|
"Create page": "Create page",
|
||||||
|
"Create space": "Create space",
|
||||||
|
"Create workspace": "Create workspace",
|
||||||
|
"Current password": "Current password",
|
||||||
|
"Dark": "Dark",
|
||||||
|
"Date": "Date",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Delete group": "Delete group",
|
||||||
|
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
|
||||||
|
"Description": "Description",
|
||||||
|
"Details": "Details",
|
||||||
|
"e.g ACME": "e.g ACME",
|
||||||
|
"e.g ACME Inc": "e.g ACME Inc",
|
||||||
|
"e.g Developers": "e.g Developers",
|
||||||
|
"e.g Group for developers": "e.g Group for developers",
|
||||||
|
"e.g product": "e.g product",
|
||||||
|
"e.g Product Team": "e.g Product Team",
|
||||||
|
"e.g Sales": "e.g Sales",
|
||||||
|
"e.g Space for product team": "e.g Space for product team",
|
||||||
|
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||||
|
"Edit": "Edit",
|
||||||
|
"Edit group": "Edit group",
|
||||||
|
"Email": "Email",
|
||||||
|
"Enter a strong password": "Enter a strong password",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||||
|
"enter valid emails addresses": "enter valid emails addresses",
|
||||||
|
"Enter your current password": "Enter your current password",
|
||||||
|
"enter your full name": "enter your full name",
|
||||||
|
"Enter your new password": "Enter your new password",
|
||||||
|
"Enter your new preferred email": "Enter your new preferred email",
|
||||||
|
"Enter your password": "Enter your password",
|
||||||
|
"Error fetching page data.": "Error fetching page data.",
|
||||||
|
"Error loading page history.": "Error loading page history.",
|
||||||
|
"Export": "Export",
|
||||||
|
"Failed to create page": "Failed to create page",
|
||||||
|
"Failed to delete page": "Failed to delete page",
|
||||||
|
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||||
|
"Failed to import pages": "Failed to import pages",
|
||||||
|
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||||
|
"Failed to update data": "Failed to update data",
|
||||||
|
"Full access": "Full access",
|
||||||
|
"Full page width": "Full page width",
|
||||||
|
"Full width": "Full width",
|
||||||
|
"General": "General",
|
||||||
|
"Group": "Group",
|
||||||
|
"Group description": "Group description",
|
||||||
|
"Group name": "Group name",
|
||||||
|
"Groups": "Groups",
|
||||||
|
"Has full access to space settings and pages.": "Has full access to space settings and pages.",
|
||||||
|
"Home": "Home",
|
||||||
|
"Import pages": "Import pages",
|
||||||
|
"Import pages & space settings": "Import pages & space settings",
|
||||||
|
"Importing pages": "Importing pages",
|
||||||
|
"invalid invitation link": "invalid invitation link",
|
||||||
|
"Invitation signup": "Invitation signup",
|
||||||
|
"Invite by email": "Invite by email",
|
||||||
|
"Invite members": "Invite members",
|
||||||
|
"Invite new members": "Invite new members",
|
||||||
|
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||||
|
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||||
|
"Join the workspace": "Join the workspace",
|
||||||
|
"Language": "Language",
|
||||||
|
"Light": "Light",
|
||||||
|
"Link copied": "Link copied",
|
||||||
|
"Login": "Login",
|
||||||
|
"Logout": "Logout",
|
||||||
|
"Manage Group": "Manage Group",
|
||||||
|
"Manage members": "Manage members",
|
||||||
|
"member": "member",
|
||||||
|
"Member": "Member",
|
||||||
|
"members": "members",
|
||||||
|
"Members": "Members",
|
||||||
|
"My preferences": "My preferences",
|
||||||
|
"My Profile": "My Profile",
|
||||||
|
"My profile": "My profile",
|
||||||
|
"Name": "Name",
|
||||||
|
"New email": "New email",
|
||||||
|
"New page": "New page",
|
||||||
|
"New password": "New password",
|
||||||
|
"No group found": "No group found",
|
||||||
|
"No page history saved yet.": "No page history saved yet.",
|
||||||
|
"No pages yet": "No pages yet",
|
||||||
|
"No results found...": "No results found...",
|
||||||
|
"No user found": "No user found",
|
||||||
|
"Overview": "Overview",
|
||||||
|
"Owner": "Owner",
|
||||||
|
"page": "page",
|
||||||
|
"Page deleted successfully": "Page deleted successfully",
|
||||||
|
"Page history": "Page history",
|
||||||
|
"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",
|
||||||
|
"Password": "Password",
|
||||||
|
"Password changed successfully": "Password changed successfully",
|
||||||
|
"Pending": "Pending",
|
||||||
|
"Please confirm your action": "Please confirm your action",
|
||||||
|
"Preferences": "Preferences",
|
||||||
|
"Print PDF": "Print PDF",
|
||||||
|
"Profile": "Profile",
|
||||||
|
"Recently updated": "Recently updated",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"Remove group member": "Remove group member",
|
||||||
|
"Remove space member": "Remove space member",
|
||||||
|
"Restore": "Restore",
|
||||||
|
"Role": "Role",
|
||||||
|
"Save": "Save",
|
||||||
|
"Search": "Search",
|
||||||
|
"Search for groups": "Search for groups",
|
||||||
|
"Search for users": "Search for users",
|
||||||
|
"Search for users and groups": "Search for users and groups",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Select language": "Select language",
|
||||||
|
"Select role": "Select role",
|
||||||
|
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||||
|
"Select theme": "Select theme",
|
||||||
|
"Send invitation": "Send invitation",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"Setup workspace": "Setup workspace",
|
||||||
|
"Sign In": "Sign In",
|
||||||
|
"Sign Up": "Sign Up",
|
||||||
|
"Slug": "Slug",
|
||||||
|
"Space": "Space",
|
||||||
|
"Space description": "Space description",
|
||||||
|
"Space menu": "Space menu",
|
||||||
|
"Space name": "Space name",
|
||||||
|
"Space settings": "Space settings",
|
||||||
|
"Space slug": "Space slug",
|
||||||
|
"Spaces": "Spaces",
|
||||||
|
"Spaces you belong to": "Spaces you belong to",
|
||||||
|
"No space found": "No space found",
|
||||||
|
"Search for spaces": "Search for spaces",
|
||||||
|
"Start typing to search...": "Start typing to search...",
|
||||||
|
"Status": "Status",
|
||||||
|
"Successfully imported": "Successfully imported",
|
||||||
|
"Successfully restored": "Successfully restored",
|
||||||
|
"System settings": "System settings",
|
||||||
|
"Theme": "Theme",
|
||||||
|
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||||
|
"Toggle full page width": "Toggle full page width",
|
||||||
|
"Unable to import pages. Please try again.": "Unable to import pages. Please try again.",
|
||||||
|
"untitled": "untitled",
|
||||||
|
"Untitled": "Untitled",
|
||||||
|
"Updated successfully": "Updated successfully",
|
||||||
|
"User": "User",
|
||||||
|
"Workspace": "Workspace",
|
||||||
|
"Workspace Name": "Workspace Name",
|
||||||
|
"Workspace settings": "Workspace settings",
|
||||||
|
"You can change your password here.": "You can change your password here.",
|
||||||
|
"Your Email": "Your Email",
|
||||||
|
"Your import is complete.": "Your import is complete.",
|
||||||
|
"Your name": "Your name",
|
||||||
|
"Your Name": "Your Name",
|
||||||
|
"Your password": "Your password",
|
||||||
|
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||||
|
"Sidebar toggle": "Sidebar toggle",
|
||||||
|
"Comments": "Comments",
|
||||||
|
"404 page not found": "404 page not found",
|
||||||
|
"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": "Take me back to homepage",
|
||||||
|
"Forgot password": "Forgot password",
|
||||||
|
"Forgot your password?": "Forgot your password?",
|
||||||
|
"A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.",
|
||||||
|
"Send reset link": "Send reset link",
|
||||||
|
"Password reset": "Password reset",
|
||||||
|
"Your new password": "Your new password",
|
||||||
|
"Set password": "Set password",
|
||||||
|
"Write a comment": "Write a comment",
|
||||||
|
"Reply...": "Reply...",
|
||||||
|
"Error loading comments.": "Error loading comments.",
|
||||||
|
"No comments yet.": "No comments yet.",
|
||||||
|
"Edit comment": "Edit comment",
|
||||||
|
"Delete comment": "Delete comment",
|
||||||
|
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||||
|
"Comment created successfully": "Comment created successfully",
|
||||||
|
"Error creating comment": "Error creating comment",
|
||||||
|
"Comment updated successfully": "Comment updated successfully",
|
||||||
|
"Failed to update comment": "Failed to update comment",
|
||||||
|
"Comment deleted successfully": "Comment deleted successfully",
|
||||||
|
"Failed to delete comment": "Failed to delete comment",
|
||||||
|
"Comment resolved successfully": "Comment resolved successfully",
|
||||||
|
"Failed to resolve comment": "Failed to resolve comment",
|
||||||
|
"Revoke invitation": "Revoke invitation",
|
||||||
|
"Revoke": "Revoke",
|
||||||
|
"Don't": "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": "Resend invitation",
|
||||||
|
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
|
||||||
|
"Invite link": "Invite link",
|
||||||
|
"Copy": "Copy",
|
||||||
|
"Copied": "Copied",
|
||||||
|
"Select a user": "Select a user",
|
||||||
|
"Select a group": "Select a group",
|
||||||
|
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
|
||||||
|
"Delete space": "Delete space",
|
||||||
|
"Are you sure you want to delete this space?": "Are you sure you want to delete this space?",
|
||||||
|
"Delete this space with all its pages and data.": "Delete this space with all its pages and data.",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.",
|
||||||
|
"Confirm space name": "Confirm space name",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Type the space name <b>{{spaceName}}</b> to confirm your action.",
|
||||||
|
"Format": "Format",
|
||||||
|
"Include subpages": "Include subpages",
|
||||||
|
"Include attachments": "Include attachments",
|
||||||
|
"Select export format": "Select export format",
|
||||||
|
"Export failed:": "Export failed:",
|
||||||
|
"export error": "export error",
|
||||||
|
"Export page": "Export page",
|
||||||
|
"Export space": "Export space",
|
||||||
|
"Export {{type}}": "Export {{type}}",
|
||||||
|
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||||
|
"Align left": "Align left",
|
||||||
|
"Align right": "Align right",
|
||||||
|
"Align center": "Align center",
|
||||||
|
"Merge cells": "Merge cells",
|
||||||
|
"Split cell": "Split cell",
|
||||||
|
"Delete column": "Delete column",
|
||||||
|
"Delete row": "Delete row",
|
||||||
|
"Add left column": "Add left column",
|
||||||
|
"Add right column": "Add right column",
|
||||||
|
"Add row above": "Add row above",
|
||||||
|
"Add row below": "Add row below",
|
||||||
|
"Delete table": "Delete table",
|
||||||
|
"Info": "Info",
|
||||||
|
"Success": "Success",
|
||||||
|
"Warning": "Warning",
|
||||||
|
"Danger": "Danger",
|
||||||
|
"Mermaid diagram error:": "Mermaid diagram error:",
|
||||||
|
"Invalid Mermaid diagram": "Invalid Mermaid diagram",
|
||||||
|
"Double-click to edit Draw.io diagram": "Double-click to edit Draw.io diagram",
|
||||||
|
"Exit": "Exit",
|
||||||
|
"Save & Exit": "Save & Exit",
|
||||||
|
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
|
||||||
|
"Paste link": "Paste link",
|
||||||
|
"Edit link": "Edit link",
|
||||||
|
"Remove link": "Remove link",
|
||||||
|
"Add link": "Add link",
|
||||||
|
"Please enter a valid url": "Please enter a valid url",
|
||||||
|
"Empty equation": "Empty equation",
|
||||||
|
"Invalid equation": "Invalid equation",
|
||||||
|
"Color": "Color",
|
||||||
|
"Text color": "Text color",
|
||||||
|
"Default": "Default",
|
||||||
|
"Blue": "Blue",
|
||||||
|
"Green": "Green",
|
||||||
|
"Purple": "Purple",
|
||||||
|
"Red": "Red",
|
||||||
|
"Yellow": "Yellow",
|
||||||
|
"Orange": "Orange",
|
||||||
|
"Pink": "Pink",
|
||||||
|
"Gray": "Gray",
|
||||||
|
"Embed link": "Embed link",
|
||||||
|
"Invalid {{provider}} embed link": "Invalid {{provider}} embed link",
|
||||||
|
"Embed {{provider}}": "Embed {{provider}}",
|
||||||
|
"Enter {{provider}} link to embed": "Enter {{provider}} link to embed",
|
||||||
|
"Bold": "Bold",
|
||||||
|
"Italic": "Italic",
|
||||||
|
"Underline": "Underline",
|
||||||
|
"Strike": "Strike",
|
||||||
|
"Code": "Code",
|
||||||
|
"Comment": "Comment",
|
||||||
|
"Text": "Text",
|
||||||
|
"Heading 1": "Heading 1",
|
||||||
|
"Heading 2": "Heading 2",
|
||||||
|
"Heading 3": "Heading 3",
|
||||||
|
"To-do List": "To-do List",
|
||||||
|
"Bullet List": "Bullet List",
|
||||||
|
"Numbered List": "Numbered List",
|
||||||
|
"Blockquote": "Blockquote",
|
||||||
|
"Just start typing with plain text.": "Just start typing with plain text.",
|
||||||
|
"Track tasks with a to-do list.": "Track tasks with a to-do list.",
|
||||||
|
"Big section heading.": "Big section heading.",
|
||||||
|
"Medium section heading.": "Medium section heading.",
|
||||||
|
"Small section heading.": "Small section heading.",
|
||||||
|
"Create a simple bullet list.": "Create a simple bullet list.",
|
||||||
|
"Create a list with numbering.": "Create a list with numbering.",
|
||||||
|
"Create block quote.": "Create block quote.",
|
||||||
|
"Insert code snippet.": "Insert code snippet.",
|
||||||
|
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||||
|
"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.",
|
||||||
|
"Table": "Table",
|
||||||
|
"Insert a table.": "Insert a table.",
|
||||||
|
"Insert collapsible block.": "Insert collapsible block.",
|
||||||
|
"Video": "Video",
|
||||||
|
"Divider": "Divider",
|
||||||
|
"Quote": "Quote",
|
||||||
|
"Image": "Image",
|
||||||
|
"File attachment": "File attachment",
|
||||||
|
"Toggle block": "Toggle block",
|
||||||
|
"Callout": "Callout",
|
||||||
|
"Insert callout notice.": "Insert callout notice.",
|
||||||
|
"Math inline": "Math inline",
|
||||||
|
"Insert inline math equation.": "Insert inline math equation.",
|
||||||
|
"Math block": "Math block",
|
||||||
|
"Insert math equation": "Insert math equation",
|
||||||
|
"Mermaid diagram": "Mermaid diagram",
|
||||||
|
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||||
|
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||||
|
"Insert current date": "Insert current date",
|
||||||
|
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||||
|
"Multiple": "Multiple",
|
||||||
|
"Heading {{level}}": "Heading {{level}}",
|
||||||
|
"Toggle title": "Toggle title",
|
||||||
|
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||||
|
"Names do not match": "Names do not match",
|
||||||
|
"Today, {{time}}": "Today, {{time}}",
|
||||||
|
"Yesterday, {{time}}": "Yesterday, {{time}}"
|
||||||
|
}
|
||||||
342
apps/client/public/locales/es-ES/translation.json
Normal file
342
apps/client/public/locales/es-ES/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "Account",
|
||||||
|
"Active": "Active",
|
||||||
|
"Add": "Add",
|
||||||
|
"Add group members": "Add group members",
|
||||||
|
"Add groups": "Add groups",
|
||||||
|
"Add members": "Add members",
|
||||||
|
"Add to groups": "Add to groups",
|
||||||
|
"Add space members": "Add space members",
|
||||||
|
"Admin": "Admin",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||||
|
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||||
|
"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 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 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.": "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 become members of groups and spaces in workspace",
|
||||||
|
"Can create and edit pages in space.": "Can create and edit pages in space.",
|
||||||
|
"Can edit": "Can edit",
|
||||||
|
"Can manage workspace": "Can manage workspace",
|
||||||
|
"Can manage workspace but cannot delete it": "Can manage workspace but cannot delete it",
|
||||||
|
"Can view": "Can view",
|
||||||
|
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Change email": "Change email",
|
||||||
|
"Change password": "Change password",
|
||||||
|
"Change photo": "Change photo",
|
||||||
|
"Choose a role": "Choose a role",
|
||||||
|
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||||
|
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||||
|
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||||
|
"Confirm": "Confirm",
|
||||||
|
"Copy link": "Copy link",
|
||||||
|
"Create": "Create",
|
||||||
|
"Create group": "Create group",
|
||||||
|
"Create page": "Create page",
|
||||||
|
"Create space": "Create space",
|
||||||
|
"Create workspace": "Create workspace",
|
||||||
|
"Current password": "Current password",
|
||||||
|
"Dark": "Dark",
|
||||||
|
"Date": "Date",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Delete group": "Delete group",
|
||||||
|
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
|
||||||
|
"Description": "Description",
|
||||||
|
"Details": "Details",
|
||||||
|
"e.g ACME": "e.g ACME",
|
||||||
|
"e.g ACME Inc": "e.g ACME Inc",
|
||||||
|
"e.g Developers": "e.g Developers",
|
||||||
|
"e.g Group for developers": "e.g Group for developers",
|
||||||
|
"e.g product": "e.g product",
|
||||||
|
"e.g Product Team": "e.g Product Team",
|
||||||
|
"e.g Sales": "e.g Sales",
|
||||||
|
"e.g Space for product team": "e.g Space for product team",
|
||||||
|
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||||
|
"Edit": "Edit",
|
||||||
|
"Edit group": "Edit group",
|
||||||
|
"Email": "Email",
|
||||||
|
"Enter a strong password": "Enter a strong password",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||||
|
"enter valid emails addresses": "enter valid emails addresses",
|
||||||
|
"Enter your current password": "Enter your current password",
|
||||||
|
"enter your full name": "enter your full name",
|
||||||
|
"Enter your new password": "Enter your new password",
|
||||||
|
"Enter your new preferred email": "Enter your new preferred email",
|
||||||
|
"Enter your password": "Enter your password",
|
||||||
|
"Error fetching page data.": "Error fetching page data.",
|
||||||
|
"Error loading page history.": "Error loading page history.",
|
||||||
|
"Export": "Export",
|
||||||
|
"Failed to create page": "Failed to create page",
|
||||||
|
"Failed to delete page": "Failed to delete page",
|
||||||
|
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||||
|
"Failed to import pages": "Failed to import pages",
|
||||||
|
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||||
|
"Failed to update data": "Failed to update data",
|
||||||
|
"Full access": "Full access",
|
||||||
|
"Full page width": "Full page width",
|
||||||
|
"Full width": "Full width",
|
||||||
|
"General": "General",
|
||||||
|
"Group": "Group",
|
||||||
|
"Group description": "Group description",
|
||||||
|
"Group name": "Group name",
|
||||||
|
"Groups": "Groups",
|
||||||
|
"Has full access to space settings and pages.": "Has full access to space settings and pages.",
|
||||||
|
"Home": "Home",
|
||||||
|
"Import pages": "Import pages",
|
||||||
|
"Import pages & space settings": "Import pages & space settings",
|
||||||
|
"Importing pages": "Importing pages",
|
||||||
|
"invalid invitation link": "invalid invitation link",
|
||||||
|
"Invitation signup": "Invitation signup",
|
||||||
|
"Invite by email": "Invite by email",
|
||||||
|
"Invite members": "Invite members",
|
||||||
|
"Invite new members": "Invite new members",
|
||||||
|
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||||
|
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||||
|
"Join the workspace": "Join the workspace",
|
||||||
|
"Language": "Language",
|
||||||
|
"Light": "Light",
|
||||||
|
"Link copied": "Link copied",
|
||||||
|
"Login": "Login",
|
||||||
|
"Logout": "Logout",
|
||||||
|
"Manage Group": "Manage Group",
|
||||||
|
"Manage members": "Manage members",
|
||||||
|
"member": "member",
|
||||||
|
"Member": "Member",
|
||||||
|
"members": "members",
|
||||||
|
"Members": "Members",
|
||||||
|
"My preferences": "My preferences",
|
||||||
|
"My Profile": "My Profile",
|
||||||
|
"My profile": "My profile",
|
||||||
|
"Name": "Name",
|
||||||
|
"New email": "New email",
|
||||||
|
"New page": "New page",
|
||||||
|
"New password": "New password",
|
||||||
|
"No group found": "No group found",
|
||||||
|
"No page history saved yet.": "No page history saved yet.",
|
||||||
|
"No pages yet": "No pages yet",
|
||||||
|
"No results found...": "No results found...",
|
||||||
|
"No user found": "No user found",
|
||||||
|
"Overview": "Overview",
|
||||||
|
"Owner": "Owner",
|
||||||
|
"page": "page",
|
||||||
|
"Page deleted successfully": "Page deleted successfully",
|
||||||
|
"Page history": "Page history",
|
||||||
|
"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",
|
||||||
|
"Password": "Password",
|
||||||
|
"Password changed successfully": "Password changed successfully",
|
||||||
|
"Pending": "Pending",
|
||||||
|
"Please confirm your action": "Please confirm your action",
|
||||||
|
"Preferences": "Preferences",
|
||||||
|
"Print PDF": "Print PDF",
|
||||||
|
"Profile": "Profile",
|
||||||
|
"Recently updated": "Recently updated",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"Remove group member": "Remove group member",
|
||||||
|
"Remove space member": "Remove space member",
|
||||||
|
"Restore": "Restore",
|
||||||
|
"Role": "Role",
|
||||||
|
"Save": "Save",
|
||||||
|
"Search": "Search",
|
||||||
|
"Search for groups": "Search for groups",
|
||||||
|
"Search for users": "Search for users",
|
||||||
|
"Search for users and groups": "Search for users and groups",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Select language": "Select language",
|
||||||
|
"Select role": "Select role",
|
||||||
|
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||||
|
"Select theme": "Select theme",
|
||||||
|
"Send invitation": "Send invitation",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"Setup workspace": "Setup workspace",
|
||||||
|
"Sign In": "Sign In",
|
||||||
|
"Sign Up": "Sign Up",
|
||||||
|
"Slug": "Slug",
|
||||||
|
"Space": "Space",
|
||||||
|
"Space description": "Space description",
|
||||||
|
"Space menu": "Space menu",
|
||||||
|
"Space name": "Space name",
|
||||||
|
"Space settings": "Space settings",
|
||||||
|
"Space slug": "Space slug",
|
||||||
|
"Spaces": "Spaces",
|
||||||
|
"Spaces you belong to": "Spaces you belong to",
|
||||||
|
"No space found": "No space found",
|
||||||
|
"Search for spaces": "Search for spaces",
|
||||||
|
"Start typing to search...": "Start typing to search...",
|
||||||
|
"Status": "Status",
|
||||||
|
"Successfully imported": "Successfully imported",
|
||||||
|
"Successfully restored": "Successfully restored",
|
||||||
|
"System settings": "System settings",
|
||||||
|
"Theme": "Theme",
|
||||||
|
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||||
|
"Toggle full page width": "Toggle full page width",
|
||||||
|
"Unable to import pages. Please try again.": "Unable to import pages. Please try again.",
|
||||||
|
"untitled": "untitled",
|
||||||
|
"Untitled": "Untitled",
|
||||||
|
"Updated successfully": "Updated successfully",
|
||||||
|
"User": "User",
|
||||||
|
"Workspace": "Workspace",
|
||||||
|
"Workspace Name": "Workspace Name",
|
||||||
|
"Workspace settings": "Workspace settings",
|
||||||
|
"You can change your password here.": "You can change your password here.",
|
||||||
|
"Your Email": "Your Email",
|
||||||
|
"Your import is complete.": "Your import is complete.",
|
||||||
|
"Your name": "Your name",
|
||||||
|
"Your Name": "Your Name",
|
||||||
|
"Your password": "Your password",
|
||||||
|
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||||
|
"Sidebar toggle": "Sidebar toggle",
|
||||||
|
"Comments": "Comments",
|
||||||
|
"404 page not found": "404 page not found",
|
||||||
|
"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": "Take me back to homepage",
|
||||||
|
"Forgot password": "Forgot password",
|
||||||
|
"Forgot your password?": "Forgot your password?",
|
||||||
|
"A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.",
|
||||||
|
"Send reset link": "Send reset link",
|
||||||
|
"Password reset": "Password reset",
|
||||||
|
"Your new password": "Your new password",
|
||||||
|
"Set password": "Set password",
|
||||||
|
"Write a comment": "Write a comment",
|
||||||
|
"Reply...": "Reply...",
|
||||||
|
"Error loading comments.": "Error loading comments.",
|
||||||
|
"No comments yet.": "No comments yet.",
|
||||||
|
"Edit comment": "Edit comment",
|
||||||
|
"Delete comment": "Delete comment",
|
||||||
|
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||||
|
"Comment created successfully": "Comment created successfully",
|
||||||
|
"Error creating comment": "Error creating comment",
|
||||||
|
"Comment updated successfully": "Comment updated successfully",
|
||||||
|
"Failed to update comment": "Failed to update comment",
|
||||||
|
"Comment deleted successfully": "Comment deleted successfully",
|
||||||
|
"Failed to delete comment": "Failed to delete comment",
|
||||||
|
"Comment resolved successfully": "Comment resolved successfully",
|
||||||
|
"Failed to resolve comment": "Failed to resolve comment",
|
||||||
|
"Revoke invitation": "Revoke invitation",
|
||||||
|
"Revoke": "Revoke",
|
||||||
|
"Don't": "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": "Resend invitation",
|
||||||
|
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
|
||||||
|
"Invite link": "Invite link",
|
||||||
|
"Copy": "Copy",
|
||||||
|
"Copied": "Copied",
|
||||||
|
"Select a user": "Select a user",
|
||||||
|
"Select a group": "Select a group",
|
||||||
|
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
|
||||||
|
"Delete space": "Delete space",
|
||||||
|
"Are you sure you want to delete this space?": "Are you sure you want to delete this space?",
|
||||||
|
"Delete this space with all its pages and data.": "Delete this space with all its pages and data.",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.",
|
||||||
|
"Confirm space name": "Confirm space name",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Type the space name <b>{{spaceName}}</b> to confirm your action.",
|
||||||
|
"Format": "Format",
|
||||||
|
"Include subpages": "Include subpages",
|
||||||
|
"Include attachments": "Include attachments",
|
||||||
|
"Select export format": "Select export format",
|
||||||
|
"Export failed:": "Export failed:",
|
||||||
|
"export error": "export error",
|
||||||
|
"Export page": "Export page",
|
||||||
|
"Export space": "Export space",
|
||||||
|
"Export {{type}}": "Export {{type}}",
|
||||||
|
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||||
|
"Align left": "Align left",
|
||||||
|
"Align right": "Align right",
|
||||||
|
"Align center": "Align center",
|
||||||
|
"Merge cells": "Merge cells",
|
||||||
|
"Split cell": "Split cell",
|
||||||
|
"Delete column": "Delete column",
|
||||||
|
"Delete row": "Delete row",
|
||||||
|
"Add left column": "Add left column",
|
||||||
|
"Add right column": "Add right column",
|
||||||
|
"Add row above": "Add row above",
|
||||||
|
"Add row below": "Add row below",
|
||||||
|
"Delete table": "Delete table",
|
||||||
|
"Info": "Info",
|
||||||
|
"Success": "Success",
|
||||||
|
"Warning": "Warning",
|
||||||
|
"Danger": "Danger",
|
||||||
|
"Mermaid diagram error:": "Mermaid diagram error:",
|
||||||
|
"Invalid Mermaid diagram": "Invalid Mermaid diagram",
|
||||||
|
"Double-click to edit Draw.io diagram": "Double-click to edit Draw.io diagram",
|
||||||
|
"Exit": "Exit",
|
||||||
|
"Save & Exit": "Save & Exit",
|
||||||
|
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
|
||||||
|
"Paste link": "Paste link",
|
||||||
|
"Edit link": "Edit link",
|
||||||
|
"Remove link": "Remove link",
|
||||||
|
"Add link": "Add link",
|
||||||
|
"Please enter a valid url": "Please enter a valid url",
|
||||||
|
"Empty equation": "Empty equation",
|
||||||
|
"Invalid equation": "Invalid equation",
|
||||||
|
"Color": "Color",
|
||||||
|
"Text color": "Text color",
|
||||||
|
"Default": "Default",
|
||||||
|
"Blue": "Blue",
|
||||||
|
"Green": "Green",
|
||||||
|
"Purple": "Purple",
|
||||||
|
"Red": "Red",
|
||||||
|
"Yellow": "Yellow",
|
||||||
|
"Orange": "Orange",
|
||||||
|
"Pink": "Pink",
|
||||||
|
"Gray": "Gray",
|
||||||
|
"Embed link": "Embed link",
|
||||||
|
"Invalid {{provider}} embed link": "Invalid {{provider}} embed link",
|
||||||
|
"Embed {{provider}}": "Embed {{provider}}",
|
||||||
|
"Enter {{provider}} link to embed": "Enter {{provider}} link to embed",
|
||||||
|
"Bold": "Bold",
|
||||||
|
"Italic": "Italic",
|
||||||
|
"Underline": "Underline",
|
||||||
|
"Strike": "Strike",
|
||||||
|
"Code": "Code",
|
||||||
|
"Comment": "Comment",
|
||||||
|
"Text": "Text",
|
||||||
|
"Heading 1": "Heading 1",
|
||||||
|
"Heading 2": "Heading 2",
|
||||||
|
"Heading 3": "Heading 3",
|
||||||
|
"To-do List": "To-do List",
|
||||||
|
"Bullet List": "Bullet List",
|
||||||
|
"Numbered List": "Numbered List",
|
||||||
|
"Blockquote": "Blockquote",
|
||||||
|
"Just start typing with plain text.": "Just start typing with plain text.",
|
||||||
|
"Track tasks with a to-do list.": "Track tasks with a to-do list.",
|
||||||
|
"Big section heading.": "Big section heading.",
|
||||||
|
"Medium section heading.": "Medium section heading.",
|
||||||
|
"Small section heading.": "Small section heading.",
|
||||||
|
"Create a simple bullet list.": "Create a simple bullet list.",
|
||||||
|
"Create a list with numbering.": "Create a list with numbering.",
|
||||||
|
"Create block quote.": "Create block quote.",
|
||||||
|
"Insert code snippet.": "Insert code snippet.",
|
||||||
|
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||||
|
"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.",
|
||||||
|
"Table": "Table",
|
||||||
|
"Insert a table.": "Insert a table.",
|
||||||
|
"Insert collapsible block.": "Insert collapsible block.",
|
||||||
|
"Video": "Video",
|
||||||
|
"Divider": "Divider",
|
||||||
|
"Quote": "Quote",
|
||||||
|
"Image": "Image",
|
||||||
|
"File attachment": "File attachment",
|
||||||
|
"Toggle block": "Toggle block",
|
||||||
|
"Callout": "Callout",
|
||||||
|
"Insert callout notice.": "Insert callout notice.",
|
||||||
|
"Math inline": "Math inline",
|
||||||
|
"Insert inline math equation.": "Insert inline math equation.",
|
||||||
|
"Math block": "Math block",
|
||||||
|
"Insert math equation": "Insert math equation",
|
||||||
|
"Mermaid diagram": "Mermaid diagram",
|
||||||
|
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||||
|
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||||
|
"Insert current date": "Insert current date",
|
||||||
|
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||||
|
"Multiple": "Multiple",
|
||||||
|
"Heading {{level}}": "Heading {{level}}",
|
||||||
|
"Toggle title": "Toggle title",
|
||||||
|
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||||
|
"Names do not match": "Names do not match",
|
||||||
|
"Today, {{time}}": "Today, {{time}}",
|
||||||
|
"Yesterday, {{time}}": "Yesterday, {{time}}"
|
||||||
|
}
|
||||||
342
apps/client/public/locales/fr-FR/translation.json
Normal file
342
apps/client/public/locales/fr-FR/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "Compte",
|
||||||
|
"Active": "Actif",
|
||||||
|
"Add": "Ajouter",
|
||||||
|
"Add group members": "Ajouter des membres au groupe",
|
||||||
|
"Add groups": "Ajouter des groupes",
|
||||||
|
"Add members": "Ajouter des membres",
|
||||||
|
"Add to groups": "Ajouter aux groupes",
|
||||||
|
"Add space members": "Ajouter des membres à l'espace",
|
||||||
|
"Admin": "Admin",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Les membres perdront l'accès aux ressources auxquelles ce groupe a accès.",
|
||||||
|
"Are you sure you want to delete this page?": "Êtes-vous sûr de vouloir supprimer cette page ?",
|
||||||
|
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir retirer cet utilisateur du groupe ? L'utilisateur perdra l'accès aux ressources auxquelles ce groupe a accès.",
|
||||||
|
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Êtes-vous sûr de vouloir retirer cet utilisateur de l'espace ? L'utilisateur perdra tout accès à cet espace.",
|
||||||
|
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Êtes-vous sûr de vouloir restaurer cette version ? Toutes les modifications non versionnées seront perdues.",
|
||||||
|
"Can become members of groups and spaces in workspace": "Peut devenir membre de groupes et d'espaces dans l'espace de travail",
|
||||||
|
"Can create and edit pages in space.": "Peut créer et modifier des pages dans l'espace.",
|
||||||
|
"Can edit": "Peut modifier",
|
||||||
|
"Can manage workspace": "Peut gérer l'espace de travail",
|
||||||
|
"Can manage workspace but cannot delete it": "Peut gérer l'espace de travail mais ne peut pas le supprimer",
|
||||||
|
"Can view": "Peut voir",
|
||||||
|
"Can view pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
|
||||||
|
"Cancel": "Annuler",
|
||||||
|
"Change email": "Changer l'email",
|
||||||
|
"Change password": "Changer le mot de passe",
|
||||||
|
"Change photo": "Changer la photo",
|
||||||
|
"Choose a role": "Choisir un rôle",
|
||||||
|
"Choose your preferred color scheme.": "Choisissez votre palette de couleurs préférée.",
|
||||||
|
"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 link": "Copier le lien",
|
||||||
|
"Create": "Créer",
|
||||||
|
"Create group": "Créer groupe",
|
||||||
|
"Create page": "Créer page",
|
||||||
|
"Create space": "Créer espace",
|
||||||
|
"Create workspace": "Créer espace de travail",
|
||||||
|
"Current password": "Mot de passe actuel",
|
||||||
|
"Dark": "Sombre",
|
||||||
|
"Date": "Date",
|
||||||
|
"Delete": "Supprimer",
|
||||||
|
"Delete group": "Supprimer groupe",
|
||||||
|
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Êtes-vous sûr de vouloir supprimer cette page ? Cela supprimera ses enfants et l'historique de la page. Cette action est irréversible.",
|
||||||
|
"Description": "Description",
|
||||||
|
"Details": "Détails",
|
||||||
|
"e.g ACME": "par ex. ACME",
|
||||||
|
"e.g ACME Inc": "par ex. ACME Inc",
|
||||||
|
"e.g Developers": "par ex. Développeurs",
|
||||||
|
"e.g Group for developers": "par ex. Groupe pour développeurs",
|
||||||
|
"e.g product": "par ex. produit",
|
||||||
|
"e.g Product Team": "par ex. Équipe Produit",
|
||||||
|
"e.g Sales": "par ex. Ventes",
|
||||||
|
"e.g Space for product team": "par ex. Espace pour l'équipe produit",
|
||||||
|
"e.g Space for sales team to collaborate": "par ex. Espace pour l'équipe de vente pour collaborer",
|
||||||
|
"Edit": "Modifier",
|
||||||
|
"Edit group": "Modifier groupe",
|
||||||
|
"Email": "Email",
|
||||||
|
"Enter a strong password": "Entrez un mot de passe fort",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "Entrez des adresses email valides séparées par une virgule ou un espace [max : 50]",
|
||||||
|
"enter valid emails addresses": "entrez des adresses email valides",
|
||||||
|
"Enter your current password": "Entrez votre mot de passe actuel",
|
||||||
|
"enter your full name": "entrez votre nom complet",
|
||||||
|
"Enter your new password": "Entrez votre nouveau mot de passe",
|
||||||
|
"Enter your new preferred email": "Entrez votre nouvel email préféré",
|
||||||
|
"Enter your password": "Entrez votre mot de passe",
|
||||||
|
"Error fetching page data.": "Erreur lors de la récupération des données de la page.",
|
||||||
|
"Error loading page history.": "Erreur lors du chargement de l'historique de la page.",
|
||||||
|
"Export": "Exporter",
|
||||||
|
"Failed to create page": "Échec de la création de la page",
|
||||||
|
"Failed to delete page": "Échec de la suppression de la page",
|
||||||
|
"Failed to fetch recent pages": "Échec de la récupération des pages récentes",
|
||||||
|
"Failed to import pages": "Échec de l'importation des pages",
|
||||||
|
"Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.",
|
||||||
|
"Failed to update data": "Échec de la mise à jour des données",
|
||||||
|
"Full access": "Accès complet",
|
||||||
|
"Full page width": "Largeur de page complète",
|
||||||
|
"Full width": "Largeur complète",
|
||||||
|
"General": "Général",
|
||||||
|
"Group": "Groupe",
|
||||||
|
"Group description": "Description du groupe",
|
||||||
|
"Group name": "Nom du groupe",
|
||||||
|
"Groups": "Groupes",
|
||||||
|
"Has full access to space settings and pages.": "A un accès complet aux paramètres de l'espace et aux pages.",
|
||||||
|
"Home": "Accueil",
|
||||||
|
"Import pages": "Importer des pages",
|
||||||
|
"Import pages & space settings": "Importer des pages et paramètres de l'espace",
|
||||||
|
"Importing pages": "Importation des pages",
|
||||||
|
"invalid invitation link": "lien d'invitation invalide",
|
||||||
|
"Invitation signup": "Inscription par invitation",
|
||||||
|
"Invite by email": "Inviter par email",
|
||||||
|
"Invite members": "Inviter des membres",
|
||||||
|
"Invite new members": "Inviter de nouveaux membres",
|
||||||
|
"Invited members who are yet to accept their invitation will appear here.": "Les membres invités qui n'ont pas encore accepté leur invitation apparaîtront ici.",
|
||||||
|
"Invited members will be granted access to spaces the groups can access": "Les membres invités auront accès aux espaces auxquels les groupes peuvent accéder",
|
||||||
|
"Join the workspace": "Rejoindre l'espace de travail",
|
||||||
|
"Language": "Langue",
|
||||||
|
"Light": "Clair",
|
||||||
|
"Link copied": "Lien copié",
|
||||||
|
"Login": "Connexion",
|
||||||
|
"Logout": "Déconnexion",
|
||||||
|
"Manage Group": "Gérer le groupe",
|
||||||
|
"Manage members": "Gérer les membres",
|
||||||
|
"member": "membre",
|
||||||
|
"Member": "Membre",
|
||||||
|
"members": "membres",
|
||||||
|
"Members": "Membres",
|
||||||
|
"My preferences": "Mes préférences",
|
||||||
|
"My Profile": "Mon Profil",
|
||||||
|
"My profile": "Mon profil",
|
||||||
|
"Name": "Nom",
|
||||||
|
"New email": "Nouvel email",
|
||||||
|
"New page": "Nouvelle page",
|
||||||
|
"New password": "Nouveau mot de passe",
|
||||||
|
"No group found": "Aucun groupe trouvé",
|
||||||
|
"No page history saved yet.": "Aucun historique de la page enregistré pour l'instant.",
|
||||||
|
"No pages yet": "Aucune page pour l'instant",
|
||||||
|
"No results found...": "Aucun résultat trouvé...",
|
||||||
|
"No user found": "Aucun utilisateur trouvé",
|
||||||
|
"Overview": "Vue d'ensemble",
|
||||||
|
"Owner": "Propriétaire",
|
||||||
|
"page": "page",
|
||||||
|
"Page deleted successfully": "Page supprimée avec succès",
|
||||||
|
"Page history": "Historique de la page",
|
||||||
|
"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",
|
||||||
|
"Password": "Mot de passe",
|
||||||
|
"Password changed successfully": "Mot de passe changé avec succès",
|
||||||
|
"Pending": "En attente",
|
||||||
|
"Please confirm your action": "Veuillez confirmer votre action",
|
||||||
|
"Preferences": "Préférences",
|
||||||
|
"Print PDF": "Imprimer PDF",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recently updated": "Récemment mis à jour",
|
||||||
|
"Remove": "Retirer",
|
||||||
|
"Remove group member": "Retirer un membre du groupe",
|
||||||
|
"Remove space member": "Retirer un membre de l'espace",
|
||||||
|
"Restore": "Restaurer",
|
||||||
|
"Role": "Rôle",
|
||||||
|
"Save": "Enregistrer",
|
||||||
|
"Search": "Rechercher",
|
||||||
|
"Search for groups": "Rechercher des groupes",
|
||||||
|
"Search for users": "Rechercher des utilisateurs",
|
||||||
|
"Search for users and groups": "Rechercher des utilisateurs et des groupes",
|
||||||
|
"Search...": "Rechercher...",
|
||||||
|
"Select language": "Sélectionner la langue",
|
||||||
|
"Select role": "Sélectionner un rôle",
|
||||||
|
"Select role to assign to all invited members": "Sélectionner le rôle à attribuer à tous les membres invités",
|
||||||
|
"Select theme": "Sélectionner le thème",
|
||||||
|
"Send invitation": "Envoyer l'invitation",
|
||||||
|
"Settings": "Paramètres",
|
||||||
|
"Setup workspace": "Configurer l'espace de travail",
|
||||||
|
"Sign In": "Se connecter",
|
||||||
|
"Sign Up": "S'inscrire",
|
||||||
|
"Slug": "Slug",
|
||||||
|
"Space": "Espace",
|
||||||
|
"Space description": "Description de l'espace",
|
||||||
|
"Space menu": "Menu de l'espace",
|
||||||
|
"Space name": "Nom de l'espace",
|
||||||
|
"Space settings": "Paramètres de l'espace",
|
||||||
|
"Space slug": "Slug de l'espace",
|
||||||
|
"Spaces": "Espaces",
|
||||||
|
"Spaces you belong to": "Espaces auxquels vous appartenez",
|
||||||
|
"No space found": "Aucun espace trouvé",
|
||||||
|
"Search for spaces": "Rechercher des espaces",
|
||||||
|
"Start typing to search...": "Commencez à taper pour rechercher...",
|
||||||
|
"Status": "Statut",
|
||||||
|
"Successfully imported": "Importé avec succès",
|
||||||
|
"Successfully restored": "Restauré avec succès",
|
||||||
|
"System settings": "Paramètres système",
|
||||||
|
"Theme": "Thème",
|
||||||
|
"To change your email, you have to enter your password and new email.": "Pour changer votre email, vous devez entrer votre mot de passe et votre nouvel email.",
|
||||||
|
"Toggle full page width": "Basculer sur la largeur complète de la page",
|
||||||
|
"Unable to import pages. Please try again.": "Impossible d'importer les pages. Veuillez réessayer.",
|
||||||
|
"untitled": "sans titre",
|
||||||
|
"Untitled": "Sans titre",
|
||||||
|
"Updated successfully": "Mis à jour avec succès",
|
||||||
|
"User": "Utilisateur",
|
||||||
|
"Workspace": "Espace de travail",
|
||||||
|
"Workspace Name": "Nom de l'espace de travail",
|
||||||
|
"Workspace settings": "Paramètres de l'espace de travail",
|
||||||
|
"You can change your password here.": "Vous pouvez changer votre mot de passe ici.",
|
||||||
|
"Your Email": "Votre Email",
|
||||||
|
"Your import is complete.": "Votre importation est terminée.",
|
||||||
|
"Your name": "Votre nom",
|
||||||
|
"Your Name": "Votre Nom",
|
||||||
|
"Your password": "Votre mot de passe",
|
||||||
|
"Your password must be a minimum of 8 characters.": "Votre mot de passe doit contenir au moins 8 caractères.",
|
||||||
|
"Sidebar toggle": "Bascule de la barre latérale",
|
||||||
|
"Comments": "Commentaires",
|
||||||
|
"404 page not found": "404 page non trouvée",
|
||||||
|
"Sorry, we can't find the page you are looking for.": "Désolé, nous ne pouvons pas trouver la page que vous cherchez.",
|
||||||
|
"Take me back to homepage": "Ramenez-moi à la page d'accueil",
|
||||||
|
"Forgot password": "Mot de passe oublié",
|
||||||
|
"Forgot your password?": "Mot de passe oublié?",
|
||||||
|
"A password reset link has been sent to your email. Please check your inbox.": "Un lien de réinitialisation de mot de passe a été envoyé à votre e-mail. Veuillez vérifier votre boîte de réception.",
|
||||||
|
"Send reset link": "Envoyer le lien de réinitialisation",
|
||||||
|
"Password reset": "Réinitialisation du mot de passe",
|
||||||
|
"Your new password": "Votre nouveau mot de passe",
|
||||||
|
"Set password": "Définir le mot de passe",
|
||||||
|
"Write a comment": "Écrire un commentaire",
|
||||||
|
"Reply...": "Répondre...",
|
||||||
|
"Error loading comments.": "Erreur lors du chargement des commentaires.",
|
||||||
|
"No comments yet.": "Pas de commentaires pour l'instant.",
|
||||||
|
"Edit comment": "Modifier le commentaire",
|
||||||
|
"Delete comment": "Supprimer le commentaire",
|
||||||
|
"Are you sure you want to delete this comment?": "Êtes-vous sûr de vouloir supprimer ce commentaire ?",
|
||||||
|
"Comment created successfully": "Commentaire créé avec succès",
|
||||||
|
"Error creating comment": "Erreur lors de la création du commentaire",
|
||||||
|
"Comment updated successfully": "Commentaire mis à jour avec succès",
|
||||||
|
"Failed to update comment": "Échec de la mise à jour du commentaire",
|
||||||
|
"Comment deleted successfully": "Commentaire supprimé avec succès",
|
||||||
|
"Failed to delete comment": "Échec de la suppression du commentaire",
|
||||||
|
"Comment resolved successfully": "Commentaire résolu avec succès",
|
||||||
|
"Failed to resolve comment": "Échec de la résolution du commentaire",
|
||||||
|
"Revoke invitation": "Révoquer l'invitation",
|
||||||
|
"Revoke": "Révoquer",
|
||||||
|
"Don't": "Ne pas",
|
||||||
|
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Êtes-vous sûr de vouloir révoquer cette invitation ? L'utilisateur ne pourra pas rejoindre l'espace de travail.",
|
||||||
|
"Resend invitation": "Renvoyer l'invitation",
|
||||||
|
"Anyone with this link can join this workspace.": "Toute personne ayant ce lien peut rejoindre cet espace de travail.",
|
||||||
|
"Invite link": "Lien d'invitation",
|
||||||
|
"Copy": "Copier",
|
||||||
|
"Copied": "Copié",
|
||||||
|
"Select a user": "Sélectionner un utilisateur",
|
||||||
|
"Select a group": "Sélectionner un groupe",
|
||||||
|
"Export all pages and attachments in this space.": "Exporter toutes les pages et pièces jointes dans cet espace.",
|
||||||
|
"Delete space": "Supprimer l'espace",
|
||||||
|
"Are you sure you want to delete this space?": "Êtes-vous sûr de vouloir supprimer cet espace ?",
|
||||||
|
"Delete this space with all its pages and data.": "Supprimer cet espace avec toutes ses pages et données.",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Toutes les pages, commentaires, pièces jointes et autorisations dans cet espace seront supprimés irréversiblement.",
|
||||||
|
"Confirm space name": "Confirmer le nom de l'espace",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Tapez le nom de l'espace <b>{{spaceName}}</b> pour confirmer votre action.",
|
||||||
|
"Format": "Format",
|
||||||
|
"Include subpages": "Inclure les sous-pages",
|
||||||
|
"Include attachments": "Inclure les pièces jointes",
|
||||||
|
"Select export format": "Sélectionner le format d'exportation",
|
||||||
|
"Export failed:": "Échec de l'exportation :",
|
||||||
|
"export error": "exporter l'erreur",
|
||||||
|
"Export page": "Exporter la page",
|
||||||
|
"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",
|
||||||
|
"Align left": "Aligner à gauche",
|
||||||
|
"Align right": "Aligner à droite",
|
||||||
|
"Align center": "Aligner au centre",
|
||||||
|
"Merge cells": "Fusionner les cellules",
|
||||||
|
"Split cell": "Diviser la cellule",
|
||||||
|
"Delete column": "Supprimer la colonne",
|
||||||
|
"Delete row": "Supprimer la ligne",
|
||||||
|
"Add left column": "Ajouter colonne à gauche",
|
||||||
|
"Add right column": "Ajouter colonne à droite",
|
||||||
|
"Add row above": "Ajouter une ligne au-dessus",
|
||||||
|
"Add row below": "Ajouter une ligne en dessous",
|
||||||
|
"Delete table": "Supprimer le tableau",
|
||||||
|
"Info": "Info",
|
||||||
|
"Success": "Succès",
|
||||||
|
"Warning": "Avertissement",
|
||||||
|
"Danger": "Danger",
|
||||||
|
"Mermaid diagram error:": "Erreur de diagramme Mermaid :",
|
||||||
|
"Invalid Mermaid diagram": "Diagramme Mermaid invalide",
|
||||||
|
"Double-click to edit Draw.io diagram": "Double-cliquez pour modifier le diagramme Draw.io",
|
||||||
|
"Exit": "Quitter",
|
||||||
|
"Save & Exit": "Enregistrer & Quitter",
|
||||||
|
"Double-click to edit Excalidraw diagram": "Double-cliquez pour modifier le diagramme Excalidraw",
|
||||||
|
"Paste link": "Coller le lien",
|
||||||
|
"Edit link": "Modifier le lien",
|
||||||
|
"Remove link": "Supprimer le lien",
|
||||||
|
"Add link": "Ajouter un lien",
|
||||||
|
"Please enter a valid url": "Veuillez entrer une URL valide",
|
||||||
|
"Empty equation": "Équation vide",
|
||||||
|
"Invalid equation": "Équation invalide",
|
||||||
|
"Color": "Couleur",
|
||||||
|
"Text color": "Couleur du texte",
|
||||||
|
"Default": "Par défaut",
|
||||||
|
"Blue": "Bleu",
|
||||||
|
"Green": "Vert",
|
||||||
|
"Purple": "Violet",
|
||||||
|
"Red": "Rouge",
|
||||||
|
"Yellow": "Jaune",
|
||||||
|
"Orange": "Orange",
|
||||||
|
"Pink": "Rose",
|
||||||
|
"Gray": "Gris",
|
||||||
|
"Embed link": "Intégrer un lien",
|
||||||
|
"Invalid {{provider}} embed link": "Lien d'intégration {{provider}} non valide",
|
||||||
|
"Embed {{provider}}": "Intégrer {{provider}}",
|
||||||
|
"Enter {{provider}} link to embed": "Entrez le lien {{provider}} à intégrer",
|
||||||
|
"Bold": "Gras",
|
||||||
|
"Italic": "Italique",
|
||||||
|
"Underline": "Souligner",
|
||||||
|
"Strike": "Barrer",
|
||||||
|
"Code": "Code",
|
||||||
|
"Comment": "Commentaire",
|
||||||
|
"Text": "Texte",
|
||||||
|
"Heading 1": "Titre 1",
|
||||||
|
"Heading 2": "Titre 2",
|
||||||
|
"Heading 3": "Titre 3",
|
||||||
|
"To-do List": "Liste de tâches",
|
||||||
|
"Bullet List": "Liste à puces",
|
||||||
|
"Numbered List": "Liste numérotée",
|
||||||
|
"Blockquote": "Bloc de citation",
|
||||||
|
"Just start typing with plain text.": "Commencez simplement à taper avec du texte brut.",
|
||||||
|
"Track tasks with a to-do list.": "Suivez les tâches avec une liste de tâches.",
|
||||||
|
"Big section heading.": "Grand titre de section.",
|
||||||
|
"Medium section heading.": "Titre de section moyen.",
|
||||||
|
"Small section heading.": "Petit titre de section.",
|
||||||
|
"Create a simple bullet list.": "Créez une simple liste à puces.",
|
||||||
|
"Create a list with numbering.": "Créez une liste numérotée.",
|
||||||
|
"Create block quote.": "Créez un bloc de citation.",
|
||||||
|
"Insert code snippet.": "Insérez un extrait de code.",
|
||||||
|
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
|
||||||
|
"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.",
|
||||||
|
"Table": "Tableau",
|
||||||
|
"Insert a table.": "Insérez un tableau.",
|
||||||
|
"Insert collapsible block.": "Insérer un bloc repliable.",
|
||||||
|
"Video": "Vidéo",
|
||||||
|
"Divider": "Diviseur",
|
||||||
|
"Quote": "Citation",
|
||||||
|
"Image": "Image",
|
||||||
|
"File attachment": "Pièce jointe",
|
||||||
|
"Toggle block": "Basculer le bloc",
|
||||||
|
"Callout": "Appel",
|
||||||
|
"Insert callout notice.": "Insérer un avis d'appel.",
|
||||||
|
"Math inline": "Mathématiques en ligne",
|
||||||
|
"Insert inline math equation.": "Insérez une équation mathématique en ligne.",
|
||||||
|
"Math block": "Bloc mathématiques",
|
||||||
|
"Insert math equation": "Insérer une équation mathématique",
|
||||||
|
"Mermaid diagram": "Diagramme Mermaid",
|
||||||
|
"Insert mermaid diagram": "Insérer un diagramme Mermaid",
|
||||||
|
"Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio",
|
||||||
|
"Insert current date": "Insérer la date actuelle",
|
||||||
|
"Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw",
|
||||||
|
"Multiple": "Multiple",
|
||||||
|
"Heading {{level}}": "Titre {{level}}",
|
||||||
|
"Toggle title": "Basculer le titre",
|
||||||
|
"Write anything. Enter \"/\" for commands": "Écrivez n'importe quoi. Entrez \"/\" pour les commandes",
|
||||||
|
"Names do not match": "Les noms ne correspondent pas",
|
||||||
|
"Today, {{time}}": "Aujourd'hui, {{time}}",
|
||||||
|
"Yesterday, {{time}}": "Hier, {{time}}"
|
||||||
|
}
|
||||||
342
apps/client/public/locales/pt-BR/translation.json
Normal file
342
apps/client/public/locales/pt-BR/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "Conta",
|
||||||
|
"Active": "Ativo",
|
||||||
|
"Add": "Adicionar",
|
||||||
|
"Add group members": "Adicionar membros ao grupo",
|
||||||
|
"Add groups": "Adicionar grupos",
|
||||||
|
"Add members": "Adicionar membros",
|
||||||
|
"Add to groups": "Adicionar aos grupos",
|
||||||
|
"Add space members": "Adicionar membros do espaço",
|
||||||
|
"Admin": "Administrador",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
|
||||||
|
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
|
||||||
|
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Tem certeza de que deseja remover este usuário do grupo? O usuário perderá acesso aos recursos que este grupo possui.",
|
||||||
|
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Tem certeza de que deseja remover este usuário do espaço? O usuário perderá todo acesso a este espaço.",
|
||||||
|
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Tem certeza de que deseja restaurar esta versão? Quaisquer alterações não versionadas serão perdidas.",
|
||||||
|
"Can become members of groups and spaces in workspace": "Pode se tornar membro de grupos e espaços no workspace",
|
||||||
|
"Can create and edit pages in space.": "Pode criar e editar páginas no espaço.",
|
||||||
|
"Can edit": "Pode editar",
|
||||||
|
"Can manage workspace": "Pode gerenciar o workspace",
|
||||||
|
"Can manage workspace but cannot delete it": "Pode gerenciar o workspace, mas não pode excluí-lo",
|
||||||
|
"Can view": "Pode visualizar",
|
||||||
|
"Can view pages in space but not edit.": "Pode visualizar páginas no espaço, mas não editar.",
|
||||||
|
"Cancel": "Cancelar",
|
||||||
|
"Change email": "Alterar email",
|
||||||
|
"Change password": "Alterar senha",
|
||||||
|
"Change photo": "Alterar foto",
|
||||||
|
"Choose a role": "Escolha um papel",
|
||||||
|
"Choose your preferred color scheme.": "Escolha seu esquema de cores preferido.",
|
||||||
|
"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 link": "Copiar link",
|
||||||
|
"Create": "Criar",
|
||||||
|
"Create group": "Criar grupo",
|
||||||
|
"Create page": "Criar página",
|
||||||
|
"Create space": "Criar espaço",
|
||||||
|
"Create workspace": "Criar workspace",
|
||||||
|
"Current password": "Senha atual",
|
||||||
|
"Dark": "Escuro",
|
||||||
|
"Date": "Data",
|
||||||
|
"Delete": "Excluir",
|
||||||
|
"Delete group": "Excluir grupo",
|
||||||
|
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Você tem certeza que quer deletar essa página? Isso irá deletar todas as páginas filhas e to o histórico. Esta ação é irreversível.",
|
||||||
|
"Description": "Descrição",
|
||||||
|
"Details": "Detalhes",
|
||||||
|
"e.g ACME": "ex.: ACME",
|
||||||
|
"e.g ACME Inc": "ex.: ACME Inc",
|
||||||
|
"e.g Developers": "ex.: Desenvolvedores",
|
||||||
|
"e.g Group for developers": "ex.: Grupo para desenvolvedores",
|
||||||
|
"e.g product": "ex.: produto",
|
||||||
|
"e.g Product Team": "ex.: Equipe de Produto",
|
||||||
|
"e.g Sales": "ex.: Vendas",
|
||||||
|
"e.g Space for product team": "ex.: Espaço para a equipe de produto",
|
||||||
|
"e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar",
|
||||||
|
"Edit": "Editar",
|
||||||
|
"Edit group": "Editar grupo",
|
||||||
|
"Email": "Email",
|
||||||
|
"Enter a strong password": "Insira uma senha forte",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
|
||||||
|
"enter valid emails addresses": "insira endereços de email válidos",
|
||||||
|
"Enter your current password": "Insira sua senha atual",
|
||||||
|
"enter your full name": "insira seu nome completo",
|
||||||
|
"Enter your new password": "Insira sua nova senha",
|
||||||
|
"Enter your new preferred email": "Insira seu novo email preferido",
|
||||||
|
"Enter your password": "Insira sua senha",
|
||||||
|
"Error fetching page data.": "Erro ao buscar dados da página.",
|
||||||
|
"Error loading page history.": "Erro ao carregar o histórico da página.",
|
||||||
|
"Export": "Exportar",
|
||||||
|
"Failed to create page": "Falha ao criar página",
|
||||||
|
"Failed to delete page": "Falha ao excluir página",
|
||||||
|
"Failed to fetch recent pages": "Falha ao buscar páginas recentes",
|
||||||
|
"Failed to import pages": "Falha ao importar páginas",
|
||||||
|
"Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
|
||||||
|
"Failed to update data": "Falha ao atualizar dados",
|
||||||
|
"Full access": "Acesso total",
|
||||||
|
"Full page width": "Usar largura total da página",
|
||||||
|
"Full width": "Largura total",
|
||||||
|
"General": "Geral",
|
||||||
|
"Group": "Grupo",
|
||||||
|
"Group description": "Descrição do grupo",
|
||||||
|
"Group name": "Nome do grupo",
|
||||||
|
"Groups": "Grupos",
|
||||||
|
"Has full access to space settings and pages.": "Tem acesso total às configurações do espaço e às páginas.",
|
||||||
|
"Home": "Início",
|
||||||
|
"Import pages": "Importar páginas",
|
||||||
|
"Import pages & space settings": "Importar páginas e configurações de espaço",
|
||||||
|
"Importing pages": "Importando páginas",
|
||||||
|
"invalid invitation link": "link de convite inválido",
|
||||||
|
"Invitation signup": "Cadastro por convite",
|
||||||
|
"Invite by email": "Convidar por email",
|
||||||
|
"Invite members": "Convidar membros",
|
||||||
|
"Invite new members": "Convidar novos membros",
|
||||||
|
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
|
||||||
|
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
|
||||||
|
"Join the workspace": "Entrar no workspace",
|
||||||
|
"Language": "Idioma",
|
||||||
|
"Light": "Claro",
|
||||||
|
"Link copied": "Link copiado",
|
||||||
|
"Login": "Entrar",
|
||||||
|
"Logout": "Sair",
|
||||||
|
"Manage Group": "Gerenciar Grupo",
|
||||||
|
"Manage members": "Gerenciar membros",
|
||||||
|
"member": "membro",
|
||||||
|
"Member": "Membro",
|
||||||
|
"members": "membros",
|
||||||
|
"Members": "Membros",
|
||||||
|
"My preferences": "Minhas preferências",
|
||||||
|
"My Profile": "Meu Perfil",
|
||||||
|
"My profile": "Meu perfil",
|
||||||
|
"Name": "Nome",
|
||||||
|
"New email": "Novo email",
|
||||||
|
"New page": "Nova página",
|
||||||
|
"New password": "Nova senha",
|
||||||
|
"No group found": "Nenhum grupo encontrado",
|
||||||
|
"No page history saved yet.": "Nenhum histórico de página salvo ainda.",
|
||||||
|
"No pages yet": "Nenhuma página ainda",
|
||||||
|
"No results found...": "Nenhum resultado encontrado...",
|
||||||
|
"No user found": "Nenhum usuário encontrado",
|
||||||
|
"Overview": "Visão geral",
|
||||||
|
"Owner": "Proprietário",
|
||||||
|
"page": "página",
|
||||||
|
"Page deleted successfully": "Página excluída com sucesso",
|
||||||
|
"Page history": "Histórico da página",
|
||||||
|
"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",
|
||||||
|
"Password": "Senha",
|
||||||
|
"Password changed successfully": "Senha alterada com sucesso",
|
||||||
|
"Pending": "Pendente",
|
||||||
|
"Please confirm your action": "Por favor, confirme sua ação",
|
||||||
|
"Preferences": "Preferências",
|
||||||
|
"Print PDF": "Imprimir PDF",
|
||||||
|
"Profile": "Perfil",
|
||||||
|
"Recently updated": "Atualizado recentemente",
|
||||||
|
"Remove": "Remover",
|
||||||
|
"Remove group member": "Remover membro do grupo",
|
||||||
|
"Remove space member": "Remover membro do espaço",
|
||||||
|
"Restore": "Restaurar",
|
||||||
|
"Role": "Função",
|
||||||
|
"Save": "Salvar",
|
||||||
|
"Search": "Buscar",
|
||||||
|
"Search for groups": "Buscar grupos",
|
||||||
|
"Search for users": "Buscar usuários",
|
||||||
|
"Search for users and groups": "Buscar usuários e grupos",
|
||||||
|
"Search...": "Buscar...",
|
||||||
|
"Select language": "Selecionar idioma",
|
||||||
|
"Select role": "Selecionar função",
|
||||||
|
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
|
||||||
|
"Select theme": "Selecionar tema",
|
||||||
|
"Send invitation": "Enviar convite",
|
||||||
|
"Settings": "Configurações",
|
||||||
|
"Setup workspace": "Configurar workspace",
|
||||||
|
"Sign In": "Entrar",
|
||||||
|
"Sign Up": "Registrar-se",
|
||||||
|
"Slug": "Slug",
|
||||||
|
"Space": "Espaço",
|
||||||
|
"Space description": "Descrição do espaço",
|
||||||
|
"Space menu": "Menu do espaço",
|
||||||
|
"Space name": "Nome do espaço",
|
||||||
|
"Space settings": "Configurações do espaço",
|
||||||
|
"Space slug": "Slug do espaço",
|
||||||
|
"Spaces": "Espaços",
|
||||||
|
"Spaces you belong to": "Espaços aos quais você pertence",
|
||||||
|
"No space found": "Nenhum espaço encontrado",
|
||||||
|
"Search for spaces": "Pesquisar espaços",
|
||||||
|
"Start typing to search...": "Comece a digitar para buscar...",
|
||||||
|
"Status": "Estado",
|
||||||
|
"Successfully imported": "Importado com sucesso",
|
||||||
|
"Successfully restored": "Restaurado com sucesso",
|
||||||
|
"System settings": "Configurações do sistema",
|
||||||
|
"Theme": "Tema",
|
||||||
|
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
|
||||||
|
"Toggle full page width": "Alternar para largura total da página",
|
||||||
|
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
|
||||||
|
"untitled": "sem título",
|
||||||
|
"Untitled": "Sem título",
|
||||||
|
"Updated successfully": "Atualizado com sucesso",
|
||||||
|
"User": "Usuário",
|
||||||
|
"Workspace": "Espaço de Trabalho",
|
||||||
|
"Workspace Name": "Nome do Workspace",
|
||||||
|
"Workspace settings": "Configurações do workspace",
|
||||||
|
"You can change your password here.": "Você pode alterar sua senha aqui.",
|
||||||
|
"Your Email": "Seu email",
|
||||||
|
"Your import is complete.": "Sua importação está concluída.",
|
||||||
|
"Your name": "Seu nome",
|
||||||
|
"Your Name": "Seu Nome",
|
||||||
|
"Your password": "Sua senha",
|
||||||
|
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
|
||||||
|
"Sidebar toggle": "Interruptor do painel lateral",
|
||||||
|
"Comments": "Comentários",
|
||||||
|
"404 page not found": "Erro 404: Página não encontrada",
|
||||||
|
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
|
||||||
|
"Take me back to homepage": "Leve-me de volta para a página inicial",
|
||||||
|
"Forgot password": "Esqueci a senha",
|
||||||
|
"Forgot your password?": "Esqueceu sua senha?",
|
||||||
|
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
|
||||||
|
"Send reset link": "Enviar link de recuperação",
|
||||||
|
"Password reset": "Resetar a senha",
|
||||||
|
"Your new password": "Sua nova senha",
|
||||||
|
"Set password": "Definir a senha",
|
||||||
|
"Write a comment": "Escreva um comentário",
|
||||||
|
"Reply...": "Responder...",
|
||||||
|
"Error loading comments.": "Erro ao carregar comentários.",
|
||||||
|
"No comments yet.": "Ainda sem comentários.",
|
||||||
|
"Edit comment": "Editar comentário",
|
||||||
|
"Delete comment": "Excluir comentário",
|
||||||
|
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
|
||||||
|
"Comment created successfully": "Comentário criado com sucesso",
|
||||||
|
"Error creating comment": "Erro ao criar comentário",
|
||||||
|
"Comment updated successfully": "Comentário atualizado com sucesso",
|
||||||
|
"Failed to update comment": "Falha ao atualizar comentário",
|
||||||
|
"Comment deleted successfully": "Comentário excluído com sucesso",
|
||||||
|
"Failed to delete comment": "Falha ao excluir comentário",
|
||||||
|
"Comment resolved successfully": "Comentário resolvido com sucesso",
|
||||||
|
"Failed to resolve comment": "Falha ao resolver comentário",
|
||||||
|
"Revoke invitation": "Cancelar o convite",
|
||||||
|
"Revoke": "Anular",
|
||||||
|
"Don't": "Não",
|
||||||
|
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Tem certeza de que deseja revogar este convite? O usuário não poderá participar do espaço de trabalho.",
|
||||||
|
"Resend invitation": "Reenviar convite",
|
||||||
|
"Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.",
|
||||||
|
"Invite link": "Link do convite",
|
||||||
|
"Copy": "Copiar",
|
||||||
|
"Copied": "Copiado",
|
||||||
|
"Select a user": "Selecione um usuário",
|
||||||
|
"Select a group": "Selecione um grupo",
|
||||||
|
"Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.",
|
||||||
|
"Delete space": "Excluir Espaço",
|
||||||
|
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
|
||||||
|
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
|
||||||
|
"Confirm space name": "Confirme o nome do espaço",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
|
||||||
|
"Format": "Formato",
|
||||||
|
"Include subpages": "Incluir subpáginas",
|
||||||
|
"Include attachments": "Incluir anexos",
|
||||||
|
"Select export format": "Selecionado o formato para exportação",
|
||||||
|
"Export failed:": "Falha ao exportar:",
|
||||||
|
"export error": "erro de exportação",
|
||||||
|
"Export page": "Exportar página",
|
||||||
|
"Export space": "Exportar espaço",
|
||||||
|
"Export {{type}}": "Exportar para {{type}}",
|
||||||
|
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
|
||||||
|
"Align left": "Alinhar à esquerda",
|
||||||
|
"Align right": "Alinhar à direita",
|
||||||
|
"Align center": "Alinhar ao centro",
|
||||||
|
"Merge cells": "Mesclar células",
|
||||||
|
"Split cell": "Dividir célula",
|
||||||
|
"Delete column": "Excluir coluna",
|
||||||
|
"Delete row": "Excluir linha",
|
||||||
|
"Add left column": "Adicionar coluna à esquerda",
|
||||||
|
"Add right column": "Adicionar coluna à direita",
|
||||||
|
"Add row above": "Adicionar linha acima",
|
||||||
|
"Add row below": "Adicionar linha abaixo",
|
||||||
|
"Delete table": "Excluir tabela",
|
||||||
|
"Info": "Informação",
|
||||||
|
"Success": "Sucesso",
|
||||||
|
"Warning": "Aviso",
|
||||||
|
"Danger": "Perigo",
|
||||||
|
"Mermaid diagram error:": "Erro no diagrama Mermaid:",
|
||||||
|
"Invalid Mermaid diagram": "Diagrama Mermaid inválido",
|
||||||
|
"Double-click to edit Draw.io diagram": "Clique duas vezes para editar o diagrama Draw.io",
|
||||||
|
"Exit": "Sair",
|
||||||
|
"Save & Exit": "Salvar e Sair",
|
||||||
|
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
|
||||||
|
"Paste link": "Colar link",
|
||||||
|
"Edit link": "Editar link",
|
||||||
|
"Remove link": "Remover link",
|
||||||
|
"Add link": "Adicionar link",
|
||||||
|
"Please enter a valid url": "Por favor, insira uma URL válida",
|
||||||
|
"Empty equation": "Equação vazia",
|
||||||
|
"Invalid equation": "Equação inválida",
|
||||||
|
"Color": "Cor",
|
||||||
|
"Text color": "Cor do texto",
|
||||||
|
"Default": "Padrão",
|
||||||
|
"Blue": "Azul",
|
||||||
|
"Green": "Verde",
|
||||||
|
"Purple": "Violeta",
|
||||||
|
"Red": "Vermelho",
|
||||||
|
"Yellow": "Amarelo",
|
||||||
|
"Orange": "Laranja",
|
||||||
|
"Pink": "Rosa",
|
||||||
|
"Gray": "Cinza",
|
||||||
|
"Embed link": "Link embutido",
|
||||||
|
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido",
|
||||||
|
"Embed {{provider}}": "Incorporar {{provider}}",
|
||||||
|
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
|
||||||
|
"Bold": "Negrito",
|
||||||
|
"Italic": "Itálico",
|
||||||
|
"Underline": "Sublinhado",
|
||||||
|
"Strike": "Tracejado",
|
||||||
|
"Code": "Código",
|
||||||
|
"Comment": "Comentário",
|
||||||
|
"Text": "Texto",
|
||||||
|
"Heading 1": "Título 1",
|
||||||
|
"Heading 2": "Título 2",
|
||||||
|
"Heading 3": "Título 3",
|
||||||
|
"To-do List": "Lista de Tarefas",
|
||||||
|
"Bullet List": "Lista de Pontos",
|
||||||
|
"Numbered List": "Lista Numerada",
|
||||||
|
"Blockquote": "Bloco de Citação",
|
||||||
|
"Just start typing with plain text.": "Basta começar a digita.",
|
||||||
|
"Track tasks with a to-do list.": "Acompanhe tarefas com uma lista de tarefas.",
|
||||||
|
"Big section heading.": "Título de seção grande.",
|
||||||
|
"Medium section heading.": "Título de seção média.",
|
||||||
|
"Small section heading.": "Título de seção pequena.",
|
||||||
|
"Create a simple bullet list.": "Crie uma lista simples com marcadores.",
|
||||||
|
"Create a list with numbering.": "Crie uma lista com numeração.",
|
||||||
|
"Create block quote.": "Crie uma citação em bloco.",
|
||||||
|
"Insert code snippet.": "Insira um trecho de código.",
|
||||||
|
"Insert horizontal rule divider": "Insira um divisor horizontal",
|
||||||
|
"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.",
|
||||||
|
"Table": "Tabela",
|
||||||
|
"Insert a table.": "Insira uma tabela.",
|
||||||
|
"Insert collapsible block.": "Insira um bloco colapsável.",
|
||||||
|
"Video": "Vídeo",
|
||||||
|
"Divider": "Divisor",
|
||||||
|
"Quote": "Citação",
|
||||||
|
"Image": "Imagem",
|
||||||
|
"File attachment": "Anexo de arquivo",
|
||||||
|
"Toggle block": "Bloco colapsável",
|
||||||
|
"Callout": "Aviso",
|
||||||
|
"Insert callout notice.": "Insira um aviso.",
|
||||||
|
"Math inline": "Matemática inline",
|
||||||
|
"Insert inline math equation.": "Insira uma equação matemática inline.",
|
||||||
|
"Math block": "Bloco de matemática",
|
||||||
|
"Insert math equation": "Insira uma equação matemática",
|
||||||
|
"Mermaid diagram": "Diagrama Mermaid",
|
||||||
|
"Insert mermaid diagram": "Insira um diagrama Mermaid",
|
||||||
|
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio",
|
||||||
|
"Insert current date": "Insira a data atual",
|
||||||
|
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw",
|
||||||
|
"Multiple": "Múltiplo",
|
||||||
|
"Heading {{level}}": "Título {{level}}",
|
||||||
|
"Toggle title": "Alternar título",
|
||||||
|
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos",
|
||||||
|
"Names do not match": "Os nomes não coincidem",
|
||||||
|
"Today, {{time}}": "Hoje, {{time}}",
|
||||||
|
"Yesterday, {{time}}": "Ontem, {{time}}"
|
||||||
|
}
|
||||||
342
apps/client/public/locales/zh-CN/translation.json
Normal file
342
apps/client/public/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
{
|
||||||
|
"Account": "账户",
|
||||||
|
"Active": "活跃",
|
||||||
|
"Add": "添加",
|
||||||
|
"Add group members": "添加群组成员",
|
||||||
|
"Add groups": "添加群组",
|
||||||
|
"Add members": "添加成员",
|
||||||
|
"Add to groups": "添加到群组",
|
||||||
|
"Add space members": "添加空间成员",
|
||||||
|
"Admin": "管理员",
|
||||||
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
|
||||||
|
"Are you sure you want to delete this page?": "您确定要删除这个页面吗?",
|
||||||
|
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。",
|
||||||
|
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。",
|
||||||
|
"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 edit": "可以编辑",
|
||||||
|
"Can manage workspace": "可以管理工作区",
|
||||||
|
"Can manage workspace but cannot delete it": "可以管理工作区但不能删除它",
|
||||||
|
"Can view": "可以查看",
|
||||||
|
"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.": "选择您喜欢的页面宽度。",
|
||||||
|
"Confirm": "确认",
|
||||||
|
"Copy link": "复制链接",
|
||||||
|
"Create": "创建",
|
||||||
|
"Create group": "创建群组",
|
||||||
|
"Create page": "创建页面",
|
||||||
|
"Create space": "创建空间",
|
||||||
|
"Create workspace": "创建工作空间",
|
||||||
|
"Current password": "当前密码",
|
||||||
|
"Dark": "深色",
|
||||||
|
"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.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。",
|
||||||
|
"Description": "描述",
|
||||||
|
"Details": "详情",
|
||||||
|
"e.g ACME": "例如:ACME",
|
||||||
|
"e.g ACME Inc": "例如:ACME Inc",
|
||||||
|
"e.g 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": "例如:销售团队协作的空间",
|
||||||
|
"Edit": "编辑",
|
||||||
|
"Edit group": "编辑群组",
|
||||||
|
"Email": "电子邮箱",
|
||||||
|
"Enter a strong password": "输入一个强密码",
|
||||||
|
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
|
||||||
|
"enter valid emails addresses": "输入有效的电子邮箱地址",
|
||||||
|
"Enter your current password": "输入您的当前密码",
|
||||||
|
"enter your full name": "输入您的全名",
|
||||||
|
"Enter your new password": "输入您的新密码",
|
||||||
|
"Enter your new preferred email": "输入您新的首选电子邮箱",
|
||||||
|
"Enter your password": "输入您的密码",
|
||||||
|
"Error fetching page data.": "获取页面数据时出错。",
|
||||||
|
"Error loading page history.": "加载页面历史时出错。",
|
||||||
|
"Export": "导出",
|
||||||
|
"Failed to create page": "创建页面失败",
|
||||||
|
"Failed to delete page": "删除页面失败",
|
||||||
|
"Failed to fetch recent pages": "获取最近页面失败",
|
||||||
|
"Failed to import pages": "导入页面失败",
|
||||||
|
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
|
||||||
|
"Failed to update data": "数据更新失败",
|
||||||
|
"Full access": "完全访问",
|
||||||
|
"Full page width": "全页宽度",
|
||||||
|
"Full width": "全宽",
|
||||||
|
"General": "常规",
|
||||||
|
"Group": "群组",
|
||||||
|
"Group description": "群组描述",
|
||||||
|
"Group name": "群组名称",
|
||||||
|
"Groups": "群组",
|
||||||
|
"Has full access to space settings and pages.": "能够更改全部空间设置和页面",
|
||||||
|
"Home": "首页",
|
||||||
|
"Import pages": "导入页面",
|
||||||
|
"Import pages & space settings": "导入页面和空间设置",
|
||||||
|
"Importing pages": "正在导入页面",
|
||||||
|
"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": "被邀请的成员将被授予访问群组可以访问的空间的权限",
|
||||||
|
"Join the workspace": "加入工作空间",
|
||||||
|
"Language": "语言",
|
||||||
|
"Light": "浅色",
|
||||||
|
"Link copied": "链接已复制",
|
||||||
|
"Login": "登录",
|
||||||
|
"Logout": "退出登录",
|
||||||
|
"Manage Group": "管理群组",
|
||||||
|
"Manage members": "管理成员",
|
||||||
|
"member": "成员",
|
||||||
|
"Member": "成员",
|
||||||
|
"members": "成员",
|
||||||
|
"Members": "成员",
|
||||||
|
"My preferences": "我的偏好设置",
|
||||||
|
"My Profile": "我的个人资料",
|
||||||
|
"My profile": "我的个人资料",
|
||||||
|
"Name": "名称",
|
||||||
|
"New email": "新电子邮箱",
|
||||||
|
"New page": "新建页面",
|
||||||
|
"New password": "新密码",
|
||||||
|
"No group found": "未找到群组",
|
||||||
|
"No page history saved yet.": "尚未保存页面历史。",
|
||||||
|
"No pages yet": "暂无页面",
|
||||||
|
"No results found...": "未找到结果...",
|
||||||
|
"No user found": "未找到用户",
|
||||||
|
"Overview": "概览",
|
||||||
|
"Owner": "所有者",
|
||||||
|
"page": "个页面",
|
||||||
|
"Page deleted successfully": "页面已成功删除",
|
||||||
|
"Page history": "页面历史",
|
||||||
|
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||||
|
"Pages": "页面",
|
||||||
|
"pages": "个页面",
|
||||||
|
"Password": "密码",
|
||||||
|
"Password changed successfully": "密码更改成功",
|
||||||
|
"Pending": "待定",
|
||||||
|
"Please confirm your action": "请确认您的操作",
|
||||||
|
"Preferences": "偏好设置",
|
||||||
|
"Print PDF": "打印 PDF",
|
||||||
|
"Profile": "个人资料",
|
||||||
|
"Recently updated": "最近更新",
|
||||||
|
"Remove": "移除",
|
||||||
|
"Remove group member": "移除群组成员",
|
||||||
|
"Remove space member": "移除空间成员",
|
||||||
|
"Restore": "恢复",
|
||||||
|
"Role": "角色",
|
||||||
|
"Save": "保存",
|
||||||
|
"Search": "搜索",
|
||||||
|
"Search for groups": "搜索群组",
|
||||||
|
"Search for users": "搜索用户",
|
||||||
|
"Search for users and groups": "搜索用户和群组",
|
||||||
|
"Search...": "搜索...",
|
||||||
|
"Select language": "选择语言",
|
||||||
|
"Select role": "选择角色",
|
||||||
|
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
|
||||||
|
"Select theme": "选择主题",
|
||||||
|
"Send invitation": "发送邀请",
|
||||||
|
"Settings": "设置",
|
||||||
|
"Setup workspace": "设置工作空间",
|
||||||
|
"Sign In": "登录",
|
||||||
|
"Sign Up": "注册",
|
||||||
|
"Slug": "短链接",
|
||||||
|
"Space": "空间",
|
||||||
|
"Space description": "空间描述",
|
||||||
|
"Space menu": "空间菜单",
|
||||||
|
"Space name": "空间名称",
|
||||||
|
"Space settings": "空间设置",
|
||||||
|
"Space slug": "空间短链接",
|
||||||
|
"Spaces": "空间",
|
||||||
|
"Spaces you belong to": "您所属的空间",
|
||||||
|
"No space found": "未找到空间",
|
||||||
|
"Search for spaces": "搜索空间",
|
||||||
|
"Start typing to search...": "开始输入以搜索...",
|
||||||
|
"Status": "状态",
|
||||||
|
"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.": "无法导入页面。请重试。",
|
||||||
|
"untitled": "无标题",
|
||||||
|
"Untitled": "无标题",
|
||||||
|
"Updated successfully": "更新成功",
|
||||||
|
"User": "用户",
|
||||||
|
"Workspace": "工作区",
|
||||||
|
"Workspace Name": "工作空间名称",
|
||||||
|
"Workspace settings": "工作区设置",
|
||||||
|
"You can change your password here.": "您可以在这里更改密码。",
|
||||||
|
"Your Email": "您的电子邮箱",
|
||||||
|
"Your import is complete.": "导入已完成。",
|
||||||
|
"Your name": "您的姓名",
|
||||||
|
"Your Name": "您的姓名",
|
||||||
|
"Your password": "您的密码",
|
||||||
|
"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.": "抱歉,我们无法找到你所需要的页面",
|
||||||
|
"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": "发送重置链接",
|
||||||
|
"Password reset": "重置密码",
|
||||||
|
"Your new password": "您的新密码",
|
||||||
|
"Set password": "设置密码",
|
||||||
|
"Write a comment": "编写评论",
|
||||||
|
"Reply...": "回复...",
|
||||||
|
"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": "评论更新成功",
|
||||||
|
"Failed to update comment": "更新评论失败",
|
||||||
|
"Comment deleted successfully": "成功删除评论",
|
||||||
|
"Failed to delete comment": "删除评论失败",
|
||||||
|
"Comment resolved successfully": "成功标记评论为解决",
|
||||||
|
"Failed to resolve comment": "标记评论为解决失败",
|
||||||
|
"Revoke invitation": "撤回邀请",
|
||||||
|
"Revoke": "撤销",
|
||||||
|
"Don't": "不要",
|
||||||
|
"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.": "任何拥有此连接的人都可以加入此工作区",
|
||||||
|
"Invite link": "邀请链接",
|
||||||
|
"Copy": "复制",
|
||||||
|
"Copied": "已复制",
|
||||||
|
"Select a user": "选择一个用户",
|
||||||
|
"Select a group": "选择一个组",
|
||||||
|
"Export all pages and attachments in this space.": "导出当前空间的所有页面和附件",
|
||||||
|
"Delete space": "删除空间",
|
||||||
|
"Are you sure you want to delete this space?": "您确定要删除这个空间吗?",
|
||||||
|
"Delete this space with all its pages and data.": "删除空间及其所有页面和数据",
|
||||||
|
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "此空间中的所有页面、评论、附件和权限将被不可逆转地删除。",
|
||||||
|
"Confirm space name": "确认空间名称",
|
||||||
|
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "输入空间名称<b>{{spaceName}}</b>以确认您的操作。",
|
||||||
|
"Format": "格式",
|
||||||
|
"Include subpages": "包括子页面",
|
||||||
|
"Include attachments": "包括附件",
|
||||||
|
"Select export format": "选择导出格式",
|
||||||
|
"Export failed:": "导出失败:",
|
||||||
|
"export error": "导出出错",
|
||||||
|
"Export page": "导出页面",
|
||||||
|
"Export space": "导出空间",
|
||||||
|
"Export {{type}}": "导出为 {{type}}",
|
||||||
|
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
|
||||||
|
"Align left": "靠左对齐",
|
||||||
|
"Align right": "靠右对齐",
|
||||||
|
"Align center": "居中对齐",
|
||||||
|
"Merge cells": "合并单元格",
|
||||||
|
"Split cell": "分割单元格",
|
||||||
|
"Delete column": "删除整列",
|
||||||
|
"Delete row": "删除整行",
|
||||||
|
"Add left column": "在左侧添加列",
|
||||||
|
"Add right column": "在右侧添加列",
|
||||||
|
"Add row above": "在上方添加行",
|
||||||
|
"Add row below": "在下方插入行",
|
||||||
|
"Delete table": "删除表格",
|
||||||
|
"Info": "信息",
|
||||||
|
"Success": "成功",
|
||||||
|
"Warning": "警告",
|
||||||
|
"Danger": "危险",
|
||||||
|
"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 图表",
|
||||||
|
"Paste link": "粘贴链接",
|
||||||
|
"Edit link": "编辑链接",
|
||||||
|
"Remove link": "移除链接",
|
||||||
|
"Add link": "添加链接",
|
||||||
|
"Please enter a valid url": "请输入一个合法的 URL",
|
||||||
|
"Empty equation": "空白公式",
|
||||||
|
"Invalid equation": "无效的公式",
|
||||||
|
"Color": "颜色",
|
||||||
|
"Text color": "文字颜色",
|
||||||
|
"Default": "默认",
|
||||||
|
"Blue": "蓝色",
|
||||||
|
"Green": "绿色",
|
||||||
|
"Purple": "紫色",
|
||||||
|
"Red": "红色",
|
||||||
|
"Yellow": "黄色",
|
||||||
|
"Orange": "橙色",
|
||||||
|
"Pink": "粉色",
|
||||||
|
"Gray": "灰色",
|
||||||
|
"Embed link": "嵌入链接",
|
||||||
|
"Invalid {{provider}} embed link": "无效的 {{provider}} 嵌入链接",
|
||||||
|
"Embed {{provider}}": "嵌入 {{provider}}",
|
||||||
|
"Enter {{provider}} link to embed": "输入 {{provider}} 链接来嵌入",
|
||||||
|
"Bold": "粗体",
|
||||||
|
"Italic": "斜体",
|
||||||
|
"Underline": "下划线",
|
||||||
|
"Strike": "删除线",
|
||||||
|
"Code": "代码",
|
||||||
|
"Comment": "评论",
|
||||||
|
"Text": "文字",
|
||||||
|
"Heading 1": "1 级标题",
|
||||||
|
"Heading 2": "2 级标题",
|
||||||
|
"Heading 3": "3 级标题",
|
||||||
|
"To-do List": "代办列表",
|
||||||
|
"Bullet List": "无需列表",
|
||||||
|
"Numbered List": "有序列表",
|
||||||
|
"Blockquote": "引用块",
|
||||||
|
"Just start typing with plain text.": "只需开始键入纯文本",
|
||||||
|
"Track tasks with a to-do list.": "使用代办列表跟踪任务",
|
||||||
|
"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.": "插入一个折叠块",
|
||||||
|
"Video": "视频",
|
||||||
|
"Divider": "分割线",
|
||||||
|
"Quote": "引用",
|
||||||
|
"Image": "图像",
|
||||||
|
"File attachment": "文件附件",
|
||||||
|
"Toggle block": "切换块",
|
||||||
|
"Callout": "标注块",
|
||||||
|
"Insert callout notice.": "插入标注提示块",
|
||||||
|
"Math inline": "行内公式",
|
||||||
|
"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 图表",
|
||||||
|
"Multiple": "多个",
|
||||||
|
"Heading {{level}}": "{{level}} 级标题",
|
||||||
|
"Toggle title": "切换标题",
|
||||||
|
"Write anything. Enter \"/\" for commands": "开始编写内容,输入 \"/\" 以使用指令",
|
||||||
|
"Names do not match": "名称不匹配",
|
||||||
|
"Today, {{time}}": "今天,{{time}}",
|
||||||
|
"Yesterday, {{time}}": "昨天,{{time}}"
|
||||||
|
}
|
||||||
@ -26,8 +26,10 @@ import { ErrorBoundary } from "react-error-boundary";
|
|||||||
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
||||||
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
||||||
import PasswordReset from "./pages/auth/password-reset";
|
import PasswordReset from "./pages/auth/password-reset";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [, setSocket] = useAtom(socketAtom);
|
const [, setSocket] = useAtom(socketAtom);
|
||||||
const authToken = useAtomValue(authTokensAtom);
|
const authToken = useAtomValue(authTokensAtom);
|
||||||
|
|
||||||
@ -78,7 +80,7 @@ export default function App() {
|
|||||||
path={"/s/:spaceSlug/p/:pageSlug"}
|
path={"/s/:spaceSlug/p/:pageSlug"}
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={<>Failed to load page. An error occurred.</>}
|
fallback={<>{t("Failed to load page. An error occurred.")}</>}
|
||||||
>
|
>
|
||||||
<Page />
|
<Page />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
153
apps/client/src/components/common/export-modal.tsx
Normal file
153
apps/client/src/components/common/export-modal.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Divider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { exportSpace } from "@/features/space/services/space-service";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface ExportModalProps {
|
||||||
|
id: string;
|
||||||
|
type: "space" | "page";
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExportModal({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: ExportModalProps) {
|
||||||
|
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||||
|
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||||
|
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
if (type === "page") {
|
||||||
|
await exportPage({ pageId: id, format, includeChildren });
|
||||||
|
}
|
||||||
|
if (type === "space") {
|
||||||
|
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||||
|
}
|
||||||
|
setIncludeChildren(false);
|
||||||
|
setIncludeAttachments(true);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifications.show({
|
||||||
|
message: "Export failed:" + err.response?.data.message,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
console.error("export error", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (format: ExportFormat) => {
|
||||||
|
setFormat(format);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Root
|
||||||
|
opened={open}
|
||||||
|
onClose={onClose}
|
||||||
|
size={500}
|
||||||
|
padding="xl"
|
||||||
|
yOffset="10vh"
|
||||||
|
xOffset={0}
|
||||||
|
mah={400}
|
||||||
|
>
|
||||||
|
<Modal.Overlay />
|
||||||
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
|
<Modal.Header py={0}>
|
||||||
|
<Modal.Title fw={500}>Export {type}</Modal.Title>
|
||||||
|
<Modal.CloseButton />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Format")}</Text>
|
||||||
|
</div>
|
||||||
|
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{type === "page" && (
|
||||||
|
<>
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Include subpages")}</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(event) =>
|
||||||
|
setIncludeChildren(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
checked={includeChildren}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "space" && (
|
||||||
|
<>
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Include attachments")}</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(event) =>
|
||||||
|
setIncludeAttachments(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
checked={includeAttachments}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group justify="center" mt="md">
|
||||||
|
<Button onClick={onClose} variant="default">
|
||||||
|
{t("Cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||||
|
</Group>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExportFormatSelection {
|
||||||
|
format: ExportFormat;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ value: "markdown", label: "Markdown" },
|
||||||
|
{ value: "html", label: "HTML" },
|
||||||
|
]}
|
||||||
|
defaultValue={format}
|
||||||
|
onChange={onChange}
|
||||||
|
styles={{ wrapper: { maxWidth: 120 } }}
|
||||||
|
comboboxProps={{ width: "120" }}
|
||||||
|
allowDeselect={false}
|
||||||
|
withCheckIcon={false}
|
||||||
|
aria-label={t("Select export format")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Badge,
|
Badge,
|
||||||
Table,
|
Table,
|
||||||
ScrollArea,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
@ -14,11 +13,14 @@ import { formattedDate } from '@/lib/time.ts';
|
|||||||
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
||||||
import { IconFileDescription } from '@tabler/icons-react';
|
import { IconFileDescription } from '@tabler/icons-react';
|
||||||
import { getSpaceUrl } from '@/lib/config.ts';
|
import { getSpaceUrl } from '@/lib/config.ts';
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
spaceId?: string;
|
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) {
|
if (isLoading) {
|
||||||
@ -26,11 +28,11 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <Text>Failed to fetch recent pages</Text>;
|
return <Text>{t("Failed to fetch recent pages")}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages && pages.items.length > 0 ? (
|
return pages && pages.items.length > 0 ? (
|
||||||
<ScrollArea>
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{pages.items.map((page) => (
|
{pages.items.map((page) => (
|
||||||
@ -48,7 +50,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Text fw={500} size="md" lineClamp={1}>
|
<Text fw={500} size="md" lineClamp={1}>
|
||||||
{page.title || 'Untitled'}
|
{page.title || t("Untitled")}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
@ -67,7 +69,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
)}
|
)}
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text c="dimmed" size="xs" fw={500}>
|
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}>
|
||||||
{formattedDate(page.updatedAt)}
|
{formattedDate(page.updatedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@ -75,10 +77,10 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</Table.ScrollContainer>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" ta="center">
|
<Text size="md" ta="center">
|
||||||
No pages yet
|
{t("No pages yet")}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
32
apps/client/src/components/icons/airtable-icon.tsx
Normal file
32
apps/client/src/components/icons/airtable-icon.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AirtableIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 256 215"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#ffbf00"
|
||||||
|
d="M114.259 2.701 18.86 42.176c-5.305 2.195-5.25 9.73.089 11.847l95.797 37.989a35.544 35.544 0 0 0 26.208 0l95.799-37.99c5.337-2.115 5.393-9.65.086-11.846L141.442 2.7a35.549 35.549 0 0 0-27.183 0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#26b5f8"
|
||||||
|
d="M136.35 112.757v94.902c0 4.514 4.55 7.605 8.746 5.942l106.748-41.435a6.39 6.39 0 0 0 4.035-5.941V71.322c0-4.514-4.551-7.604-8.747-5.941l-106.748 41.434a6.392 6.392 0 0 0-4.035 5.942"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ed3049"
|
||||||
|
d="m111.423 117.654-31.68 15.296-3.217 1.555L9.65 166.548C5.411 168.593 0 165.504 0 160.795V71.72c0-1.704.874-3.175 2.046-4.283a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillOpacity={0.25}
|
||||||
|
d="m111.423 117.654-31.68 15.296L2.045 67.438a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
apps/client/src/components/icons/figma-icon.tsx
Normal file
23
apps/client/src/components/icons/figma-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FigmaIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<g fill="none" fillRule="evenodd" transform="translate(4)">
|
||||||
|
<circle cx={12} cy={12} r={4} fill="#19bcfe" />
|
||||||
|
<path fill="#09cf83" d="M4 24a4 4 0 0 0 4-4v-4H4a4 4 0 1 0 0 8z" />
|
||||||
|
<path fill="#a259ff" d="M4 16h4V8H4a4 4 0 1 0 0 8z" />
|
||||||
|
<path fill="#f24e1e" d="M4 8h4V0H4a4 4 0 1 0 0 8z" />
|
||||||
|
<path fill="#ff7262" d="M12 8H8V0h4a4 4 0 1 1 0 8z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
apps/client/src/components/icons/framer-icon.tsx
Normal file
17
apps/client/src/components/icons/framer-icon.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FramerIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path d="M4 0h16v8h-8zm0 8h8l8 8H4zm0 8h8v8z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
apps/client/src/components/icons/google-drive-icon.tsx
Normal file
24
apps/client/src/components/icons/google-drive-icon.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoogleDriveIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 87.3 78"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da" />
|
||||||
|
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47" />
|
||||||
|
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
||||||
|
fill="#ea4335" />
|
||||||
|
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d" />
|
||||||
|
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc" />
|
||||||
|
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
||||||
|
fill="#ffba00" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoogleSheetsIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/>
|
||||||
|
<path fill="#c8e6c9" d="M40 13L30 13 30 3z"/>
|
||||||
|
<path fill="#2e7d32" d="M30 13L40 23 40 13z"/>
|
||||||
|
<path
|
||||||
|
fill="#e8f5e9"
|
||||||
|
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
apps/client/src/components/icons/index.ts
Normal file
11
apps/client/src/components/icons/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { AirtableIcon } from "./airtable-icon.tsx";
|
||||||
|
export { FigmaIcon } from "./figma-icon.tsx";
|
||||||
|
export { TypeformIcon } from "./typeform-icon.tsx";
|
||||||
|
export { VimeoIcon } from "./vimeo-icon.tsx";
|
||||||
|
export { MiroIcon } from "./miro-icon.tsx";
|
||||||
|
export { GoogleDriveIcon } from "./google-drive-icon.tsx";
|
||||||
|
export { GoogleSheetsIcon } from "./google-sheets-icon.tsx";
|
||||||
|
export { FramerIcon } from "./framer-icon.tsx";
|
||||||
|
export { LoomIcon } from "./loom-icon.tsx";
|
||||||
|
export { YoutubeIcon } from "./youtube-icon.tsx";
|
||||||
|
|
||||||
19
apps/client/src/components/icons/loom-icon.tsx
Normal file
19
apps/client/src/components/icons/loom-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoomIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="#625DF5"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24 10.665h-7.018l6.078-3.509-1.335-2.312-6.078 3.509 3.508-6.077L16.843.94l-3.508 6.077V0h-2.67v7.018L7.156.94 4.844 2.275l3.509 6.077-6.078-3.508L.94 7.156l6.078 3.509H0v2.67h7.017L.94 16.844l1.335 2.313 6.077-3.508-3.509 6.077 2.312 1.335 3.509-6.078V24h2.67v-7.017l3.508 6.077 2.312-1.335-3.509-6.078 6.078 3.509 1.335-2.313-6.077-3.508h7.017v-2.67H24zm-12 4.966a3.645 3.645 0 1 1 0-7.29 3.645 3.645 0 0 1 0 7.29z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/client/src/components/icons/miro-icon.tsx
Normal file
18
apps/client/src/components/icons/miro-icon.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MiroIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.392 0H13.9L17 4.808 10.444 0H6.949l3.102 6.3L3.494 0H0l3.05 8.131L0 24h3.494L10.05 6.985 6.949 24h3.494L17 5.494 13.899 24h3.493L24 3.672 17.392 0z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/client/src/components/icons/typeform-icon.tsx
Normal file
18
apps/client/src/components/icons/typeform-icon.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypeformIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.502 13.035c-.5 0-.756-.411-.756-.917 0-.505.252-.894.756-.894.513 0 .756.407.756.894-.004.515-.261.917-.756.917Zm-4.888-1.81c.292 0 .414.17.414.317 0 .357-.365.514-1.126.536 0-.442.253-.854.712-.854Zm-3.241 1.81c-.473 0-.67-.384-.67-.917 0-.527.202-.894.67-.894.477 0 .702.38.702.894 0 .537-.234.917-.702.917Zm-3.997-2.334h-.738l1.224 2.808c-.234.519-.36.648-.522.648-.171 0-.333-.138-.45-.259l-.324.43c.22.232.522.366.832.366.387 0 .685-.224.856-.626l1.413-3.371h-.725l-.738 2.012-.828-2.008Zm19.553.523c.36 0 .432.246.432.823v1.516H24v-1.914c0-.689-.473-.988-.91-.988-.386 0-.742.241-.94.688a.901.901 0 0 0-.891-.688c-.365 0-.73.232-.927.666v-.626h-.64v2.857h.64v-1.22c0-.617.324-1.114.765-1.114.36 0 .427.246.427.823v1.516h.64l-.005-1.225c0-.617.329-1.114.77-1.114Zm-5.1-.523h-.324v2.857h.639v-1.095c0-.693.306-1.163.76-1.163.118 0 .217.005.325.05l.099-.676c-.081-.009-.153-.018-.225-.018-.45 0-.774.309-.964.707V10.7h-.31Zm-2.327-.045c-.846 0-1.418.644-1.418 1.458 0 .845.58 1.475 1.418 1.475.85 0 1.431-.648 1.431-1.475-.004-.818-.594-1.458-1.431-1.458Zm-4.852 2.38c-.333 0-.581-.17-.685-.515.847-.036 1.675-.242 1.675-.988 0-.43-.423-.872-1.03-.872-.82 0-1.374.666-1.374 1.457 0 .828.545 1.476 1.36 1.476.567 0 .927-.228 1.21-.559l-.31-.42c-.329.335-.531.42-.846.42Zm-3.151-2.38c-.324 0-.648.188-.774.483v-.438h-.64v3.98h.64v-1.422c.135.205.445.34.72.34.85 0 1.3-.631 1.3-1.48-.004-.841-.445-1.463-1.246-1.463Zm-4.483-1.1H0v.622h1.18v3.38h.67v-3.38h1.166v-.622Zm9.502 1.145h-.383v.572h.383v2.285h.639v-2.285h.621v-.572h-.621v-.447c0-.286.117-.385.382-.385.1 0 .19.027.311.068l.144-.537c-.117-.067-.351-.094-.504-.094-.612 0-.972.367-.972 1.002v.393Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
apps/client/src/components/icons/vimeo-icon.tsx
Normal file
19
apps/client/src/components/icons/vimeo-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VimeoIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="#1AB7EA"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M23.9765 6.4168c-.105 2.338-1.739 5.5429-4.894 9.6088-3.2679 4.247-6.0258 6.3699-8.2898 6.3699-1.409 0-2.578-1.294-3.553-3.881l-1.9179-7.1138c-.719-2.584-1.488-3.878-2.312-3.878-.179 0-.806.378-1.8809 1.132l-1.129-1.457a315.06 315.06 0 003.501-3.1279c1.579-1.368 2.765-2.085 3.5539-2.159 1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.5069.5389 2.45 1.1309 3.674 1.7759 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.8679 3.434-5.7568 6.7619-5.6368 2.4729.06 3.6279 1.664 3.4929 4.7969z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
apps/client/src/components/icons/youtube-icon.tsx
Normal file
19
apps/client/src/components/icons/youtube-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { rem } from '@mantine/core';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function YoutubeIcon({ size }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="#FF0000"
|
||||||
|
style={{ width: rem(size), height: rem(size) }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
import {Group, Text, Tooltip} from "@mantine/core";
|
||||||
import classes from "./app-header.module.css";
|
import classes from "./app-header.module.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||||
@ -11,10 +11,12 @@ import {
|
|||||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||||
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
|
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
|
||||||
|
|
||||||
export function AppHeader() {
|
export function AppHeader() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ export function AppHeader() {
|
|||||||
|
|
||||||
const items = links.map((link) => (
|
const items = links.map((link) => (
|
||||||
<Link key={link.label} to={link.link} className={classes.link}>
|
<Link key={link.label} to={link.link} className={classes.link}>
|
||||||
{link.label}
|
{t(link.label)}
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -35,21 +37,26 @@ export function AppHeader() {
|
|||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
{!isHomeRoute && (
|
{!isHomeRoute && (
|
||||||
<>
|
<>
|
||||||
|
<Tooltip label={t("Sidebar toggle")}>
|
||||||
|
|
||||||
<SidebarToggle
|
<SidebarToggle
|
||||||
aria-label="sidebar toggle"
|
aria-label={t("Sidebar toggle")}
|
||||||
opened={mobileOpened}
|
opened={mobileOpened}
|
||||||
onClick={toggleMobile}
|
onClick={toggleMobile}
|
||||||
hiddenFrom="sm"
|
hiddenFrom="sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label={t("Sidebar toggle")}>
|
||||||
<SidebarToggle
|
<SidebarToggle
|
||||||
aria-label="sidebar toggle"
|
aria-label={t("Sidebar toggle")}
|
||||||
opened={desktopOpened}
|
opened={desktopOpened}
|
||||||
onClick={toggleDesktop}
|
onClick={toggleDesktop}
|
||||||
visibleFrom="sm"
|
visibleFrom="sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -14,3 +14,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resizeHandle {
|
||||||
|
width: 3px;
|
||||||
|
cursor: col-resize;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
width: 5px;
|
||||||
|
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import CommentList from "@/features/comment/components/comment-list.tsx";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Aside() {
|
export default function Aside() {
|
||||||
const [{ tab }] = useAtom(asideStateAtom);
|
const [{ tab }] = useAtom(asideStateAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
let title: string;
|
let title: string;
|
||||||
let component: ReactNode;
|
let component: ReactNode;
|
||||||
@ -25,7 +27,7 @@ export default function Aside() {
|
|||||||
{component && (
|
{component && (
|
||||||
<>
|
<>
|
||||||
<Text mb="md" fw={500}>
|
<Text mb="md" fw={500}>
|
||||||
{title}
|
{t(title)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { AppShell, Container } from "@mantine/core";
|
import { AppShell, Container } from "@mantine/core";
|
||||||
import React from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
asideStateAtom,
|
asideStateAtom,
|
||||||
desktopSidebarAtom,
|
desktopSidebarAtom,
|
||||||
mobileSidebarAtom,
|
mobileSidebarAtom, sidebarWidthAtom,
|
||||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||||
@ -21,6 +21,46 @@ export default function GlobalAppShell({
|
|||||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const sidebarRef = useRef(null);
|
||||||
|
|
||||||
|
const startResizing = React.useCallback((mouseDownEvent) => {
|
||||||
|
mouseDownEvent.preventDefault();
|
||||||
|
setIsResizing(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopResizing = React.useCallback(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resize = React.useCallback(
|
||||||
|
(mouseMoveEvent) => {
|
||||||
|
if (isResizing) {
|
||||||
|
const newWidth = mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left;
|
||||||
|
if (newWidth < 220) {
|
||||||
|
setSidebarWidth(220);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newWidth > 600) {
|
||||||
|
setSidebarWidth(600);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isResizing]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//https://codesandbox.io/p/sandbox/kz9de
|
||||||
|
window.addEventListener("mousemove", resize);
|
||||||
|
window.addEventListener("mouseup", stopResizing);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", resize);
|
||||||
|
window.removeEventListener("mouseup", stopResizing);
|
||||||
|
};
|
||||||
|
}, [resize, stopResizing]);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||||
@ -33,7 +73,7 @@ export default function GlobalAppShell({
|
|||||||
header={{ height: 45 }}
|
header={{ height: 45 }}
|
||||||
navbar={
|
navbar={
|
||||||
!isHomeRoute && {
|
!isHomeRoute && {
|
||||||
width: 300,
|
width: isSpaceRoute ? sidebarWidth : 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: {
|
collapsed: {
|
||||||
mobile: !mobileOpened,
|
mobile: !mobileOpened,
|
||||||
@ -54,7 +94,8 @@ export default function GlobalAppShell({
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
{!isHomeRoute && (
|
{!isHomeRoute && (
|
||||||
<AppShell.Navbar className={classes.navbar} withBorder={false}>
|
<AppShell.Navbar className={classes.navbar} withBorder={false} ref={sidebarRef}>
|
||||||
|
<div className={classes.resizeHandle} onMouseDown={startResizing} />
|
||||||
{isSpaceRoute && <SpaceSidebar />}
|
{isSpaceRoute && <SpaceSidebar />}
|
||||||
{isSettingsRoute && <SettingsSidebar />}
|
{isSettingsRoute && <SettingsSidebar />}
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|||||||
@ -19,3 +19,5 @@ export const asideStateAtom = atom<AsideStateType>({
|
|||||||
tab: "",
|
tab: "",
|
||||||
isAsideOpen: false,
|
isAsideOpen: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
|
||||||
|
|||||||
@ -13,8 +13,10 @@ import { Link } from "react-router-dom";
|
|||||||
import APP_ROUTE from "@/lib/app-route.ts";
|
import APP_ROUTE from "@/lib/app-route.ts";
|
||||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function TopMenu() {
|
export default function TopMenu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [currentUser] = useAtom(currentUserAtom);
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
|
||||||
@ -44,14 +46,14 @@ export default function TopMenu() {
|
|||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Label>Workspace</Menu.Label>
|
<Menu.Label>{t("Workspace")}</Menu.Label>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||||
leftSection={<IconSettings size={16} />}
|
leftSection={<IconSettings size={16} />}
|
||||||
>
|
>
|
||||||
Workspace settings
|
{t("Workspace settings")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -59,12 +61,12 @@ export default function TopMenu() {
|
|||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||||
leftSection={<IconUsers size={16} />}
|
leftSection={<IconUsers size={16} />}
|
||||||
>
|
>
|
||||||
Manage members
|
{t("Manage members")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Label>Account</Menu.Label>
|
<Menu.Label>{t("Account")}</Menu.Label>
|
||||||
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
||||||
<Group wrap={"nowrap"}>
|
<Group wrap={"nowrap"}>
|
||||||
<CustomAvatar
|
<CustomAvatar
|
||||||
@ -73,11 +75,11 @@ export default function TopMenu() {
|
|||||||
name={user.name}
|
name={user.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div style={{width: 190}}>
|
||||||
<Text size="sm" fw={500} lineClamp={1}>
|
<Text size="sm" fw={500} lineClamp={1}>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed" truncate="end">
|
||||||
{user.email}
|
{user.email}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +90,7 @@ export default function TopMenu() {
|
|||||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||||
leftSection={<IconUserCircle size={16} />}
|
leftSection={<IconUserCircle size={16} />}
|
||||||
>
|
>
|
||||||
My profile
|
{t("My profile")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -96,13 +98,13 @@ export default function TopMenu() {
|
|||||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||||
leftSection={<IconBrush size={16} />}
|
leftSection={<IconBrush size={16} />}
|
||||||
>
|
>
|
||||||
My preferences
|
{t("My preferences")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||||
Logout
|
{t("Logout")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import classes from "./settings.module.css";
|
import classes from "./settings.module.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface DataItem {
|
interface DataItem {
|
||||||
label: string;
|
label: string;
|
||||||
@ -51,6 +52,7 @@ const groupedData: DataGroup[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsSidebar() {
|
export default function SettingsSidebar() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [active, setActive] = useState(location.pathname);
|
const [active, setActive] = useState(location.pathname);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -62,7 +64,7 @@ export default function SettingsSidebar() {
|
|||||||
const menuItems = groupedData.map((group) => (
|
const menuItems = groupedData.map((group) => (
|
||||||
<div key={group.heading}>
|
<div key={group.heading}>
|
||||||
<Text c="dimmed" className={classes.linkHeader}>
|
<Text c="dimmed" className={classes.linkHeader}>
|
||||||
{group.heading}
|
{t(group.heading)}
|
||||||
</Text>
|
</Text>
|
||||||
{group.items.map((item) => (
|
{group.items.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
@ -72,7 +74,7 @@ export default function SettingsSidebar() {
|
|||||||
to={item.path}
|
to={item.path}
|
||||||
>
|
>
|
||||||
<item.icon className={classes.linkIcon} stroke={2} />
|
<item.icon className={classes.linkIcon} stroke={2} />
|
||||||
<span>{item.label}</span>
|
<span>{t(item.label)}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -89,7 +91,7 @@ export default function SettingsSidebar() {
|
|||||||
>
|
>
|
||||||
<IconArrowLeft stroke={2} />
|
<IconArrowLeft stroke={2} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Text fw={500}>Settings</Text>
|
<Text fw={500}>{t("Settings")}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Popover,
|
Popover,
|
||||||
Button,
|
Button,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from "react";
|
||||||
|
const Picker = React.lazy(() => import("@emoji-mart/react"));
|
||||||
const Picker = React.lazy(() => import('@emoji-mart/react'));
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface EmojiPickerInterface {
|
export interface EmojiPickerInterface {
|
||||||
onEmojiSelect: (emoji: any) => void;
|
onEmojiSelect: (emoji: any) => void;
|
||||||
@ -23,8 +23,26 @@ function EmojiPicker({
|
|||||||
removeEmojiAction,
|
removeEmojiAction,
|
||||||
readOnly,
|
readOnly,
|
||||||
}: EmojiPickerInterface) {
|
}: EmojiPickerInterface) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [opened, handlers] = useDisclosure(false);
|
const [opened, handlers] = useDisclosure(false);
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const [target, setTarget] = useState<HTMLElement | null>(null);
|
||||||
|
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useClickOutside(
|
||||||
|
() => handlers.close(),
|
||||||
|
["mousedown", "touchstart"],
|
||||||
|
[dropdown, target],
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need this because the default Mantine popover closeOnEscape does not work
|
||||||
|
useWindowEvent("keydown", (event) => {
|
||||||
|
if (opened && event.key === "Escape") {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
handlers.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleEmojiSelect = (emoji) => {
|
const handleEmojiSelect = (emoji) => {
|
||||||
onEmojiSelect(emoji);
|
onEmojiSelect(emoji);
|
||||||
@ -43,16 +61,17 @@ function EmojiPicker({
|
|||||||
width={332}
|
width={332}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
|
closeOnEscape={true}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target ref={setTarget}>
|
||||||
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
|
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
|
||||||
{icon}
|
{icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown bg="000" style={{ border: 'none' }}>
|
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Picker
|
<Picker
|
||||||
data={async () => (await import('@emoji-mart/data')).default}
|
data={async () => (await import("@emoji-mart/data")).default}
|
||||||
onEmojiSelect={handleEmojiSelect}
|
onEmojiSelect={handleEmojiSelect}
|
||||||
perLine={8}
|
perLine={8}
|
||||||
skinTonePosition="search"
|
skinTonePosition="search"
|
||||||
@ -64,14 +83,14 @@ function EmojiPicker({
|
|||||||
c="gray"
|
c="gray"
|
||||||
size="xs"
|
size="xs"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
bottom: '1rem',
|
bottom: "1rem",
|
||||||
right: '1rem',
|
right: "1rem",
|
||||||
}}
|
}}
|
||||||
onClick={handleRemoveEmoji}
|
onClick={handleRemoveEmoji}
|
||||||
>
|
>
|
||||||
Remove
|
{t("Remove")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -2,21 +2,24 @@ import { Title, Text, Button, Container, Group } from "@mantine/core";
|
|||||||
import classes from "./error-404.module.css";
|
import classes from "./error-404.module.css";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function Error404() {
|
export function Error404() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>404 page not found - Docmost</title>
|
<title>{t("404 page not found")} - Docmost</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Container className={classes.root}>
|
<Container className={classes.root}>
|
||||||
<Title className={classes.title}>404 Page Not Found</Title>
|
<Title className={classes.title}>{t("404 page not found")}</Title>
|
||||||
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
|
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
|
||||||
Sorry, we can't find the page you are looking for.
|
{t("Sorry, we can't find the page you are looking for.")}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Button component={Link} to={"/home"} variant="subtle" size="md">
|
<Button component={Link} to={"/home"} variant="subtle" size="md">
|
||||||
Take me back to homepage
|
{t("Take me back to homepage")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { forwardRef } from "react";
|
|||||||
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
||||||
import { Group, Text, Menu, Button } from "@mantine/core";
|
import { Group, Text, Menu, Button } from "@mantine/core";
|
||||||
import { IRoleData } from "@/lib/types.ts";
|
import { IRoleData } from "@/lib/types.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> {
|
interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> {
|
||||||
name: string;
|
name: string;
|
||||||
@ -36,10 +37,12 @@ export default function RoleSelectMenu({
|
|||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
}: RoleMenuProps) {
|
}: RoleMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu withArrow>
|
<Menu withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<RoleButton name={roleName} disabled={disabled} />
|
<RoleButton name={t(roleName)} disabled={disabled} />
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
@ -50,9 +53,9 @@ export default function RoleSelectMenu({
|
|||||||
>
|
>
|
||||||
<Group flex="1" gap="xs">
|
<Group flex="1" gap="xs">
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm">{item.label}</Text>
|
<Text size="sm">{t(item.label)}</Text>
|
||||||
<Text size="xs" opacity={0.65}>
|
<Text size="xs" opacity={0.65}>
|
||||||
{item.description}
|
{t(item.description)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{item.label === roleName && <IconCheck size={20} />}
|
{item.label === roleName && <IconCheck size={20} />}
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
IconLayoutSidebarRightCollapse,
|
IconLayoutSidebarRightCollapse,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import {
|
import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
BoxProps,
|
|
||||||
ElementProps,
|
|
||||||
MantineColor,
|
|
||||||
MantineSize,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||||
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
||||||
@ -17,13 +11,10 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
|||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SidebarToggle({
|
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
|
||||||
opened,
|
({ opened, size = "sm", ...others }, ref) => {
|
||||||
size = "sm",
|
|
||||||
...others
|
|
||||||
}: SidebarToggleProps) {
|
|
||||||
return (
|
return (
|
||||||
<ActionIcon size={size} {...others} variant="subtle" color="gray">
|
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
|
||||||
{opened ? (
|
{opened ? (
|
||||||
<IconLayoutSidebarRightExpand />
|
<IconLayoutSidebarRightExpand />
|
||||||
) : (
|
) : (
|
||||||
@ -32,3 +23,6 @@ export default function SidebarToggle({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SidebarToggle;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { IForgotPassword } from "@/features/auth/types/auth.types";
|
|||||||
import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core";
|
import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core";
|
||||||
import classes from "./auth.module.css";
|
import classes from "./auth.module.css";
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@ -15,6 +16,7 @@ const formSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function ForgotPasswordForm() {
|
export function ForgotPasswordForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { forgotPassword, isLoading } = useAuth();
|
const { forgotPassword, isLoading } = useAuth();
|
||||||
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
|
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
|
||||||
useRedirectIfAuthenticated();
|
useRedirectIfAuthenticated();
|
||||||
@ -36,7 +38,7 @@ export function ForgotPasswordForm() {
|
|||||||
<Container size={420} my={40} className={classes.container}>
|
<Container size={420} my={40} className={classes.container}>
|
||||||
<Box p="xl" mt={200}>
|
<Box p="xl" mt={200}>
|
||||||
<Title order={2} ta="center" fw={500} mb="md">
|
<Title order={2} ta="center" fw={500} mb="md">
|
||||||
Forgot password
|
{t("Forgot password")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
@ -53,14 +55,15 @@ export function ForgotPasswordForm() {
|
|||||||
|
|
||||||
{isTokenSent && (
|
{isTokenSent && (
|
||||||
<Text>
|
<Text>
|
||||||
A password reset link has been sent to your email. Please check
|
{t(
|
||||||
your inbox.
|
"A password reset link has been sent to your email. Please check your inbox.",
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isTokenSent && (
|
{!isTokenSent && (
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
Send reset link
|
{t("Send reset link")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -17,15 +17,17 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
|||||||
import classes from "@/features/auth/components/auth.module.css";
|
import classes from "@/features/auth/components/auth.module.css";
|
||||||
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2),
|
name: z.string().trim().min(1),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export function InviteSignUpForm() {
|
export function InviteSignUpForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ export function InviteSignUpForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <div>invalid invitation link</div>;
|
return <div>{t("invalid invitation link")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
@ -66,7 +68,7 @@ export function InviteSignUpForm() {
|
|||||||
<Container size={420} my={40} className={classes.container}>
|
<Container size={420} my={40} className={classes.container}>
|
||||||
<Box p="xl" mt={200}>
|
<Box p="xl" mt={200}>
|
||||||
<Title order={2} ta="center" fw={500} mb="md">
|
<Title order={2} ta="center" fw={500} mb="md">
|
||||||
Join the workspace
|
{t("Join the workspace")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Stack align="stretch" justify="center" gap="xl">
|
<Stack align="stretch" justify="center" gap="xl">
|
||||||
@ -74,8 +76,8 @@ export function InviteSignUpForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
label="Name"
|
label={t("Name")}
|
||||||
placeholder="enter your full name"
|
placeholder={t("enter your full name")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
@ -83,7 +85,7 @@ export function InviteSignUpForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
label="Email"
|
label={t("Email")}
|
||||||
value={invitation.email}
|
value={invitation.email}
|
||||||
disabled
|
disabled
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@ -91,14 +93,14 @@ export function InviteSignUpForm() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
placeholder="Your password"
|
placeholder={t("Your password")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
Sign Up
|
{t("Sign Up")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@ -9,13 +9,13 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Box,
|
Box,
|
||||||
|
|
||||||
Anchor,
|
Anchor,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import classes from "./auth.module.css";
|
import classes from "./auth.module.css";
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import APP_ROUTE from "@/lib/app-route.ts";
|
import APP_ROUTE from "@/lib/app-route.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@ -26,6 +26,7 @@ const formSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { signIn, isLoading } = useAuth();
|
const { signIn, isLoading } = useAuth();
|
||||||
useRedirectIfAuthenticated();
|
useRedirectIfAuthenticated();
|
||||||
|
|
||||||
@ -45,29 +46,29 @@ export function LoginForm() {
|
|||||||
<Container size={420} my={40} className={classes.container}>
|
<Container size={420} my={40} className={classes.container}>
|
||||||
<Box p="xl" mt={200}>
|
<Box p="xl" mt={200}>
|
||||||
<Title order={2} ta="center" fw={500} mb="md">
|
<Title order={2} ta="center" fw={500} mb="md">
|
||||||
Login
|
{t("Login")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
label="Email"
|
label={t("Email")}
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps("email")}
|
{...form.getInputProps("email")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
placeholder="Your password"
|
placeholder={t("Your password")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
Sign In
|
{t("Sign In")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ export function LoginForm() {
|
|||||||
underline="never"
|
underline="never"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Forgot your password?
|
{t("Forgot your password?")}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import classes from "./auth.module.css";
|
import classes from "./auth.module.css";
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
newPassword: z
|
newPassword: z
|
||||||
@ -24,6 +25,7 @@ interface PasswordResetFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { passwordReset, isLoading } = useAuth();
|
const { passwordReset, isLoading } = useAuth();
|
||||||
useRedirectIfAuthenticated();
|
useRedirectIfAuthenticated();
|
||||||
|
|
||||||
@ -37,28 +39,28 @@ export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
|||||||
async function onSubmit(data: IPasswordReset) {
|
async function onSubmit(data: IPasswordReset) {
|
||||||
await passwordReset({
|
await passwordReset({
|
||||||
token: resetToken,
|
token: resetToken,
|
||||||
newPassword: data.newPassword
|
newPassword: data.newPassword,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size={420} my={40} className={classes.container}>
|
<Container size={420} my={40} className={classes.container}>
|
||||||
<Box p="xl" mt={200}>
|
<Box p="xl" mt={200}>
|
||||||
<Title order={2} ta="center" fw={500} mb="md">
|
<Title order={2} ta="center" fw={500} mb="md">
|
||||||
Password reset
|
{t("Password reset")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="New password"
|
label={t("New password")}
|
||||||
placeholder="Your new password"
|
placeholder={t("Your new password")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("newPassword")}
|
{...form.getInputProps("newPassword")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
Set password
|
{t("Set password")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -13,10 +13,11 @@ import {
|
|||||||
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
|
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
|
||||||
import useAuth from "@/features/auth/hooks/use-auth";
|
import useAuth from "@/features/auth/hooks/use-auth";
|
||||||
import classes from "@/features/auth/components/auth.module.css";
|
import classes from "@/features/auth/components/auth.module.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
workspaceName: z.string().min(2).max(60),
|
workspaceName: z.string().trim().min(3).max(50),
|
||||||
name: z.string().min(2).max(60),
|
name: z.string().min(1).max(50),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "email is required" })
|
.min(1, { message: "email is required" })
|
||||||
@ -25,6 +26,7 @@ const formSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function SetupWorkspaceForm() {
|
export function SetupWorkspaceForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setupWorkspace, isLoading } = useAuth();
|
const { setupWorkspace, isLoading } = useAuth();
|
||||||
// useRedirectIfAuthenticated();
|
// useRedirectIfAuthenticated();
|
||||||
|
|
||||||
@ -46,15 +48,15 @@ export function SetupWorkspaceForm() {
|
|||||||
<Container size={420} my={40} className={classes.container}>
|
<Container size={420} my={40} className={classes.container}>
|
||||||
<Box p="xl" mt={200}>
|
<Box p="xl" mt={200}>
|
||||||
<Title order={2} ta="center" fw={500} mb="md">
|
<Title order={2} ta="center" fw={500} mb="md">
|
||||||
Create workspace
|
{t("Create workspace")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="workspaceName"
|
id="workspaceName"
|
||||||
type="text"
|
type="text"
|
||||||
label="Workspace Name"
|
label={t("Workspace Name")}
|
||||||
placeholder="e.g ACME Inc"
|
placeholder={t("e.g ACME Inc")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("workspaceName")}
|
{...form.getInputProps("workspaceName")}
|
||||||
@ -63,8 +65,8 @@ export function SetupWorkspaceForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
label="Your Name"
|
label={t("Your Name")}
|
||||||
placeholder="enter your full name"
|
placeholder={t("enter your full name")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
@ -73,7 +75,7 @@ export function SetupWorkspaceForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
label="Your Email"
|
label={t("Your Email")}
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
@ -81,14 +83,14 @@ export function SetupWorkspaceForm() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
placeholder="Enter a strong password"
|
placeholder={t("Enter a strong password")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
Setup workspace
|
{t("Setup workspace")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { acceptInvitation } from "@/features/workspace/services/workspace-servic
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import APP_ROUTE from "@/lib/app-route.ts";
|
import APP_ROUTE from "@/lib/app-route.ts";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export default function useAuth() {
|
export default function useAuth() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -30,6 +31,7 @@ export default function useAuth() {
|
|||||||
|
|
||||||
const [, setCurrentUser] = useAtom(currentUserAtom);
|
const [, setCurrentUser] = useAtom(currentUserAtom);
|
||||||
const [authToken, setAuthToken] = useAtom(authTokensAtom);
|
const [authToken, setAuthToken] = useAtom(authTokensAtom);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const handleSignIn = async (data: ILogin) => {
|
const handleSignIn = async (data: ILogin) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -136,7 +138,8 @@ export default function useAuth() {
|
|||||||
setAuthToken(null);
|
setAuthToken(null);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
Cookies.remove("authTokens");
|
Cookies.remove("authTokens");
|
||||||
navigate(APP_ROUTE.AUTH.LOGIN);
|
queryClient.clear();
|
||||||
|
window.location.replace(APP_ROUTE.AUTH.LOGIN);;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleForgotPassword = async (data: IForgotPassword) => {
|
const handleForgotPassword = async (data: IForgotPassword) => {
|
||||||
|
|||||||
@ -1,14 +1,32 @@
|
|||||||
import { Button, Group } from '@mantine/core';
|
import { Button, Group } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type CommentActionsProps = {
|
type CommentActionsProps = {
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
onCancel?: () => void;
|
||||||
|
isCommentEditor?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentActions({ onSave, isLoading }: CommentActionsProps) {
|
function CommentActions({
|
||||||
|
onSave,
|
||||||
|
isLoading,
|
||||||
|
onCancel,
|
||||||
|
isCommentEditor,
|
||||||
|
}: CommentActionsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group justify="flex-end" pt={2} wrap="nowrap">
|
<Group justify="flex-end" pt="sm" wrap="nowrap">
|
||||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>Save</Button>
|
{isCommentEditor && (
|
||||||
|
<Button size="compact-sm" variant="default" onClick={onCancel}>
|
||||||
|
{t("Cancel")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button size="compact-sm" loading={isLoading} onClick={onSave}>
|
||||||
|
{t("Save")}
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useCreateCommentMutation } from "@/features/comment/queries/comment-que
|
|||||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||||
import { useEditor } from "@tiptap/react";
|
import { useEditor } from "@tiptap/react";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface CommentDialogProps {
|
interface CommentDialogProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: ReturnType<typeof useEditor>;
|
||||||
@ -21,6 +22,7 @@ interface CommentDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||||
@ -107,7 +109,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
|||||||
|
|
||||||
<CommentEditor
|
<CommentEditor
|
||||||
onUpdate={handleCommentEditorChange}
|
onUpdate={handleCommentEditorChange}
|
||||||
placeholder="Write a comment"
|
placeholder={t("Write a comment")}
|
||||||
editable={true}
|
editable={true}
|
||||||
autofocus={true}
|
autofocus={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import classes from "./comment.module.css";
|
|||||||
import { useFocusWithin } from "@mantine/hooks";
|
import { useFocusWithin } from "@mantine/hooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface CommentEditorProps {
|
interface CommentEditorProps {
|
||||||
defaultContent?: any;
|
defaultContent?: any;
|
||||||
@ -27,6 +28,7 @@ const CommentEditor = forwardRef(
|
|||||||
}: CommentEditorProps,
|
}: CommentEditorProps,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { ref: focusRef, focused } = useFocusWithin();
|
const { ref: focusRef, focused } = useFocusWithin();
|
||||||
|
|
||||||
const commentEditor = useEditor({
|
const commentEditor = useEditor({
|
||||||
@ -36,7 +38,7 @@ const CommentEditor = forwardRef(
|
|||||||
dropcursor: false,
|
dropcursor: false,
|
||||||
}),
|
}),
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: placeholder || "Reply...",
|
placeholder: placeholder || t("Reply..."),
|
||||||
}),
|
}),
|
||||||
Underline,
|
Underline,
|
||||||
Link,
|
Link,
|
||||||
|
|||||||
@ -24,7 +24,6 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
|||||||
const { hovered, ref } = useHover();
|
const { hovered, ref } = useHover();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const editor = useAtomValue(pageEditorAtom);
|
const editor = useAtomValue(pageEditorAtom);
|
||||||
const [content, setContent] = useState<string>(comment.content);
|
const [content, setContent] = useState<string>(comment.content);
|
||||||
const updateCommentMutation = useUpdateCommentMutation();
|
const updateCommentMutation = useUpdateCommentMutation();
|
||||||
@ -59,6 +58,9 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
|||||||
function handleEditToggle() {
|
function handleEditToggle() {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}
|
}
|
||||||
|
function cancelEdit() {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={ref} pb="xs">
|
<Box ref={ref} pb="xs">
|
||||||
@ -116,6 +118,8 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
|||||||
<CommentActions
|
<CommentActions
|
||||||
onSave={handleUpdateComment}
|
onSave={handleUpdateComment}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
onCancel={cancelEdit}
|
||||||
|
isCommentEditor={true}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
useCommentsQuery,
|
useCommentsQuery,
|
||||||
useCreateCommentMutation,
|
useCreateCommentMutation,
|
||||||
} from "@/features/comment/queries/comment-query";
|
} from "@/features/comment/queries/comment-query";
|
||||||
|
|
||||||
import CommentEditor from "@/features/comment/components/comment-editor";
|
import CommentEditor from "@/features/comment/components/comment-editor";
|
||||||
import CommentActions from "@/features/comment/components/comment-actions";
|
import CommentActions from "@/features/comment/components/comment-actions";
|
||||||
import { useFocusWithin } from "@mantine/hooks";
|
import { useFocusWithin } from "@mantine/hooks";
|
||||||
@ -14,8 +13,10 @@ import { IComment } from "@/features/comment/types/comment.types.ts";
|
|||||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||||
import { IPagination } from "@/lib/types.ts";
|
import { IPagination } from "@/lib/types.ts";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function CommentList() {
|
function CommentList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||||
const {
|
const {
|
||||||
@ -79,11 +80,11 @@ function CommentList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <div>Error loading comments.</div>;
|
return <div>{t("Error loading comments.")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!comments || comments.items.length === 0) {
|
if (!comments || comments.items.length === 0) {
|
||||||
return <>No comments yet.</>;
|
return <>{t("No comments yet.")}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ActionIcon, Menu } from '@mantine/core';
|
import { ActionIcon, Menu } from "@mantine/core";
|
||||||
import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
import { modals } from '@mantine/modals';
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type CommentMenuProps = {
|
type CommentMenuProps = {
|
||||||
onEditComment: () => void;
|
onEditComment: () => void;
|
||||||
@ -8,34 +9,35 @@ type CommentMenuProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
|
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const openDeleteModal = () =>
|
const openDeleteModal = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Are you sure you want to delete this comment?',
|
title: t("Are you sure you want to delete this comment?"),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||||
confirmProps: { color: 'red' },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: onDeleteComment,
|
onConfirm: onDeleteComment,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={200}>
|
<Menu shadow="md" width={200}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="default" style={{ border: 'none' }}>
|
<ActionIcon variant="default" style={{ border: "none" }}>
|
||||||
<IconDots size={20} stroke={2} />
|
<IconDots size={20} stroke={2} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item onClick={onEditComment}
|
<Menu.Item onClick={onEditComment} leftSection={<IconEdit size={14} />}>
|
||||||
leftSection={<IconEdit size={14} />}>
|
{t("Edit comment")}
|
||||||
Edit comment
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item leftSection={<IconTrash size={14} />}
|
<Menu.Item
|
||||||
|
leftSection={<IconTrash size={14} />}
|
||||||
onClick={openDeleteModal}
|
onClick={openDeleteModal}
|
||||||
>
|
>
|
||||||
Delete comment
|
{t("Delete comment")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -1,34 +1,44 @@
|
|||||||
import { ActionIcon } from '@mantine/core';
|
import { ActionIcon } from "@mantine/core";
|
||||||
import { IconCircleCheck } from '@tabler/icons-react';
|
import { IconCircleCheck } from "@tabler/icons-react";
|
||||||
import { modals } from '@mantine/modals';
|
import { modals } from "@mantine/modals";
|
||||||
import { useResolveCommentMutation } from '@/features/comment/queries/comment-query';
|
import { useResolveCommentMutation } from "@/features/comment/queries/comment-query";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const resolveCommentMutation = useResolveCommentMutation();
|
const resolveCommentMutation = useResolveCommentMutation();
|
||||||
|
|
||||||
const isResolved = resolvedAt != null;
|
const isResolved = resolvedAt != null;
|
||||||
const iconColor = isResolved ? 'green' : 'gray';
|
const iconColor = isResolved ? "green" : "gray";
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const openConfirmModal = () =>
|
const openConfirmModal = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Are you sure you want to resolve this comment thread?',
|
title: t("Are you sure you want to resolve this comment thread?"),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: 'Confirm', cancel: 'Cancel' },
|
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||||
onConfirm: handleResolveToggle,
|
onConfirm: handleResolveToggle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleResolveToggle = async () => {
|
const handleResolveToggle = async () => {
|
||||||
try {
|
try {
|
||||||
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
await resolveCommentMutation.mutateAsync({
|
||||||
|
commentId,
|
||||||
|
resolved: !isResolved,
|
||||||
|
});
|
||||||
//TODO: remove comment mark
|
//TODO: remove comment mark
|
||||||
// Remove comment thread from state on resolve
|
// Remove comment thread from state on resolve
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle resolved state:', error);
|
console.error("Failed to toggle resolved state:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionIcon onClick={openConfirmModal} variant="default" style={{ border: 'none' }}>
|
<ActionIcon
|
||||||
|
onClick={openConfirmModal}
|
||||||
|
variant="default"
|
||||||
|
style={{ border: "none" }}
|
||||||
|
>
|
||||||
<IconCircleCheck size={20} stroke={2} color={iconColor} />
|
<IconCircleCheck size={20} stroke={2} color={iconColor} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/features/comment/types/comment.types";
|
} from "@/features/comment/types/comment.types";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IPagination } from "@/lib/types.ts";
|
import { IPagination } from "@/lib/types.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const RQ_KEY = (pageId: string) => ["comments", pageId];
|
export const RQ_KEY = (pageId: string) => ["comments", pageId];
|
||||||
|
|
||||||
@ -25,7 +26,6 @@ export function useCommentsQuery(
|
|||||||
params: ICommentParams,
|
params: ICommentParams,
|
||||||
): UseQueryResult<IPagination<IComment>, Error> {
|
): UseQueryResult<IPagination<IComment>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
|
||||||
queryKey: RQ_KEY(params.pageId),
|
queryKey: RQ_KEY(params.pageId),
|
||||||
queryFn: () => getPageComments(params),
|
queryFn: () => getPageComments(params),
|
||||||
enabled: !!params.pageId,
|
enabled: !!params.pageId,
|
||||||
@ -34,6 +34,7 @@ export function useCommentsQuery(
|
|||||||
|
|
||||||
export function useCreateCommentMutation() {
|
export function useCreateCommentMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutation<IComment, Error, Partial<IComment>>({
|
return useMutation<IComment, Error, Partial<IComment>>({
|
||||||
mutationFn: (data) => createComment(data),
|
mutationFn: (data) => createComment(data),
|
||||||
@ -46,28 +47,37 @@ export function useCreateCommentMutation() {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) });
|
queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) });
|
||||||
notifications.show({ message: "Comment created successfully" });
|
notifications.show({ message: t("Comment created successfully") });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
notifications.show({ message: "Error creating comment", color: "red" });
|
notifications.show({
|
||||||
|
message: t("Error creating comment"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateCommentMutation() {
|
export function useUpdateCommentMutation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutation<IComment, Error, Partial<IComment>>({
|
return useMutation<IComment, Error, Partial<IComment>>({
|
||||||
mutationFn: (data) => updateComment(data),
|
mutationFn: (data) => updateComment(data),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
notifications.show({ message: "Comment updated successfully" });
|
notifications.show({ message: t("Comment updated successfully") });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
notifications.show({ message: "Failed to update comment", color: "red" });
|
notifications.show({
|
||||||
|
message: t("Failed to update comment"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteCommentMutation(pageId?: string) {
|
export function useDeleteCommentMutation(pageId?: string) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (commentId: string) => deleteComment(commentId),
|
mutationFn: (commentId: string) => deleteComment(commentId),
|
||||||
@ -87,16 +97,20 @@ export function useDeleteCommentMutation(pageId?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.show({ message: "Comment deleted successfully" });
|
notifications.show({ message: t("Comment deleted successfully") });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
notifications.show({ message: "Failed to delete comment", color: "red" });
|
notifications.show({
|
||||||
|
message: t("Failed to delete comment"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useResolveCommentMutation() {
|
export function useResolveCommentMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: IResolveComment) => resolveComment(data),
|
mutationFn: (data: IResolveComment) => resolveComment(data),
|
||||||
@ -115,11 +129,11 @@ export function useResolveCommentMutation() {
|
|||||||
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
|
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
notifications.show({ message: "Comment resolved successfully" });
|
notifications.show({ message: t("Comment resolved successfully") });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: "Failed to resolve comment",
|
message: t("Failed to resolve comment"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { handleAttachmentUpload } from "@docmost/editor-ext";
|
import { handleAttachmentUpload } from "@docmost/editor-ext";
|
||||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||||
|
import { formatBytes } from "@/lib";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
export const uploadAttachmentAction = handleAttachmentUpload({
|
export const uploadAttachmentAction = handleAttachmentUpload({
|
||||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||||
@ -18,10 +21,12 @@ export const uploadAttachmentAction = handleAttachmentUpload({
|
|||||||
if (file.type.includes("image/") || file.type.includes("video/")) {
|
if (file.type.includes("image/") || file.type.includes("video/")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (file.size / 1024 / 1024 > 50) {
|
if (file.size > getFileUploadSizeLimit()) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "red",
|
color: "red",
|
||||||
message: `File exceeds the 50 MB attachment limit`,
|
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||||
|
limit: formatBytes(getFileUploadSizeLimit()),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,10 @@ import {
|
|||||||
showCommentPopupAtom,
|
showCommentPopupAtom,
|
||||||
} from "@/features/comment/atoms/comment-atom";
|
} from "@/features/comment/atoms/comment-atom";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v7 as uuid7 } from "uuid";
|
||||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
@ -39,6 +40,7 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||||
const showCommentPopupRef = useRef(showCommentPopup);
|
const showCommentPopupRef = useRef(showCommentPopup);
|
||||||
@ -49,31 +51,31 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
|
|
||||||
const items: BubbleMenuItem[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
{
|
{
|
||||||
name: "bold",
|
name: "Bold",
|
||||||
isActive: () => props.editor.isActive("bold"),
|
isActive: () => props.editor.isActive("bold"),
|
||||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||||
icon: IconBold,
|
icon: IconBold,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "italic",
|
name: "Italic",
|
||||||
isActive: () => props.editor.isActive("italic"),
|
isActive: () => props.editor.isActive("italic"),
|
||||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||||
icon: IconItalic,
|
icon: IconItalic,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "underline",
|
name: "Underline",
|
||||||
isActive: () => props.editor.isActive("underline"),
|
isActive: () => props.editor.isActive("underline"),
|
||||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||||
icon: IconUnderline,
|
icon: IconUnderline,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "strike",
|
name: "Strike",
|
||||||
isActive: () => props.editor.isActive("strike"),
|
isActive: () => props.editor.isActive("strike"),
|
||||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||||
icon: IconStrikethrough,
|
icon: IconStrikethrough,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "code",
|
name: "Code",
|
||||||
isActive: () => props.editor.isActive("code"),
|
isActive: () => props.editor.isActive("code"),
|
||||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||||
icon: IconCode,
|
icon: IconCode,
|
||||||
@ -81,10 +83,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const commentItem: BubbleMenuItem = {
|
const commentItem: BubbleMenuItem = {
|
||||||
name: "comment",
|
name: "Comment",
|
||||||
isActive: () => props.editor.isActive("comment"),
|
isActive: () => props.editor.isActive("comment"),
|
||||||
command: () => {
|
command: () => {
|
||||||
const commentId = uuidv4();
|
const commentId = uuid7();
|
||||||
|
|
||||||
props.editor.chain().focus().setCommentDecoration().run();
|
props.editor.chain().focus().setCommentDecoration().run();
|
||||||
setDraftCommentId(commentId);
|
setDraftCommentId(commentId);
|
||||||
@ -138,13 +140,13 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
|
|
||||||
<ActionIcon.Group>
|
<ActionIcon.Group>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Tooltip key={index} label={item.name} withArrow>
|
<Tooltip key={index} label={t(item.name)} withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
key={index}
|
key={index}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="0"
|
radius="0"
|
||||||
aria-label={item.name}
|
aria-label={t(item.name)}
|
||||||
className={clsx({ [classes.active]: item.isActive() })}
|
className={clsx({ [classes.active]: item.isActive() })}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
onClick={item.command}
|
onClick={item.command}
|
||||||
@ -175,7 +177,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="0"
|
radius="0"
|
||||||
aria-label={commentItem.name}
|
aria-label={t(commentItem.name)}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
onClick={commentItem.command}
|
onClick={commentItem.command}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import { useEditor } from "@tiptap/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface BubbleColorMenuItem {
|
export interface BubbleColorMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
@ -106,6 +107,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||||
editor.isActive("textStyle", { color }),
|
editor.isActive("textStyle", { color }),
|
||||||
);
|
);
|
||||||
@ -117,7 +119,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Popover width={200} opened={isOpen} withArrow>
|
<Popover width={200} opened={isOpen} withArrow>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Tooltip label="Text color" withArrow>
|
<Tooltip label={t("Text color")} withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
@ -136,8 +138,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
{/* make mah responsive */}
|
{/* make mah responsive */}
|
||||||
<ScrollArea.Autosize type="scroll" mah="400">
|
<ScrollArea.Autosize type="scroll" mah="400">
|
||||||
<Text span c="dimmed" inherit>
|
<Text span c="dimmed" tt="uppercase" inherit>
|
||||||
COLOR
|
{t("Color")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button.Group orientation="vertical">
|
<Button.Group orientation="vertical">
|
||||||
@ -155,7 +157,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.commands.unsetColor();
|
editor.commands.unsetColor();
|
||||||
name !== "Default" &&
|
name !== t("Default") &&
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
@ -165,7 +167,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
}}
|
}}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
>
|
>
|
||||||
{name}
|
{t(name)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Button.Group>
|
</Button.Group>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { IconLink } from "@tabler/icons-react";
|
|||||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import { useEditor } from "@tiptap/react";
|
||||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface LinkSelectorProps {
|
interface LinkSelectorProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: ReturnType<typeof useEditor>;
|
||||||
@ -15,6 +16,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const onLink = useCallback(
|
const onLink = useCallback(
|
||||||
(url: string) => {
|
(url: string) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@ -32,7 +34,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
withArrow
|
withArrow
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Tooltip label="Add link" withArrow>
|
<Tooltip label={t("Add link")} withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
import { Popover, Button, ScrollArea } from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import { useEditor } from "@tiptap/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface NodeSelectorProps {
|
interface NodeSelectorProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: ReturnType<typeof useEditor>;
|
||||||
@ -33,6 +34,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const items: BubbleMenuItem[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
{
|
{
|
||||||
name: "Text",
|
name: "Text",
|
||||||
@ -114,7 +117,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
rightSection={<IconChevronDown size={16} />}
|
rightSection={<IconChevronDown size={16} />}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
{activeItem?.name}
|
{t(activeItem?.name)}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
|
|
||||||
@ -137,7 +140,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
}}
|
}}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
>
|
>
|
||||||
{item.name}
|
{t(item.name)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Button.Group>
|
</Button.Group>
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import {
|
|||||||
IconInfoCircleFilled,
|
IconInfoCircleFilled,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { CalloutType } from "@docmost/editor-ext";
|
import { CalloutType } from "@docmost/editor-ext";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -71,11 +73,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group className="actionIconGroup">
|
<ActionIcon.Group className="actionIconGroup">
|
||||||
<Tooltip position="top" label="Info">
|
<Tooltip position="top" label={t("Info")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("info")}
|
onClick={() => setCalloutType("info")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Info"
|
aria-label={t("Info")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
||||||
}
|
}
|
||||||
@ -84,11 +86,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Success">
|
<Tooltip position="top" label={t("Success")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("success")}
|
onClick={() => setCalloutType("success")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Success"
|
aria-label={t("Success")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("callout", { type: "success" })
|
editor.isActive("callout", { type: "success" })
|
||||||
? "light"
|
? "light"
|
||||||
@ -99,11 +101,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Warning">
|
<Tooltip position="top" label={t("Warning")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("warning")}
|
onClick={() => setCalloutType("warning")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Warning"
|
aria-label={t("Warning")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("callout", { type: "warning" })
|
editor.isActive("callout", { type: "warning" })
|
||||||
? "light"
|
? "light"
|
||||||
@ -114,11 +116,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Danger">
|
<Tooltip position="top" label={t("Danger")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => setCalloutType("danger")}
|
onClick={() => setCalloutType("danger")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Danger"
|
aria-label={t("Danger")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("callout", { type: "danger" })
|
editor.isActive("callout", { type: "danger" })
|
||||||
? "light"
|
? "light"
|
||||||
|
|||||||
@ -2,16 +2,17 @@ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
|||||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from '@mantine/core';
|
import { ActionIcon, CopyButton, Group, Select, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
||||||
//import MermaidView from "@/features/editor/components/code-block/mermaid-view.tsx";
|
|
||||||
import classes from './code-block.module.css';
|
import classes from './code-block.module.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const MermaidView = React.lazy(
|
const MermaidView = React.lazy(
|
||||||
() => import('@/features/editor/components/code-block/mermaid-view.tsx')
|
() => import('@/features/editor/components/code-block/mermaid-view.tsx')
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function CodeBlockView(props: NodeViewProps) {
|
export default function CodeBlockView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { node, updateAttributes, extension, editor, getPos } = props;
|
const { node, updateAttributes, extension, editor, getPos } = props;
|
||||||
const { language } = node.attrs;
|
const { language } = node.attrs;
|
||||||
const [languageValue, setLanguageValue] = useState<string | null>(
|
const [languageValue, setLanguageValue] = useState<string | null>(
|
||||||
@ -61,7 +62,7 @@ export default function CodeBlockView(props: NodeViewProps) {
|
|||||||
<CopyButton value={node?.textContent} timeout={2000}>
|
<CopyButton value={node?.textContent} timeout={2000}>
|
||||||
{({ copied, copy }) => (
|
{({ copied, copy }) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={copied ? 'Copied' : 'Copy'}
|
label={copied ? t('Copied') : t('Copy')}
|
||||||
withArrow
|
withArrow
|
||||||
position="right"
|
position="right"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import classes from "./code-block.module.css";
|
import classes from "./code-block.module.css";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
@ -29,11 +30,11 @@ export default function MermaidView({ props }: MermaidViewProps) {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (props.editor.isEditable) {
|
if (props.editor.isEditable) {
|
||||||
setPreview(
|
setPreview(
|
||||||
`<div class="${classes.error}">Mermaid diagram error: ${err}</div>`,
|
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${err}</div>`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setPreview(
|
setPreview(
|
||||||
`<div class="${classes.error}">Invalid Mermaid Diagram</div>`,
|
`<div class="${classes.error}">${t("Invalid Mermaid diagram")}</div>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,26 +1,36 @@
|
|||||||
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
import { ActionIcon, Card, Image, Modal, Text } from '@mantine/core';
|
import {
|
||||||
import { useRef, useState } from 'react';
|
ActionIcon,
|
||||||
import { uploadFile } from '@/features/page/services/page-service.ts';
|
Card,
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
Image,
|
||||||
import { getFileUrl } from '@/lib/config.ts';
|
Modal,
|
||||||
|
Text,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
|
||||||
import {
|
import {
|
||||||
DrawIoEmbed,
|
DrawIoEmbed,
|
||||||
DrawIoEmbedRef,
|
DrawIoEmbedRef,
|
||||||
EventExit,
|
EventExit,
|
||||||
EventSave,
|
EventSave,
|
||||||
} from 'react-drawio';
|
} from "react-drawio";
|
||||||
import { IAttachment } from '@/lib/types';
|
import { IAttachment } from "@/lib/types";
|
||||||
import { decodeBase64ToSvgString, svgStringToFile } from '@/lib/utils';
|
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
|
||||||
import clsx from 'clsx';
|
import clsx from "clsx";
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function DrawioView(props: NodeViewProps) {
|
export default function DrawioView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { node, updateAttributes, editor, selected } = props;
|
const { node, updateAttributes, editor, selected } = props;
|
||||||
const { src, title, width, attachmentId } = node.attrs;
|
const { src, title, width, attachmentId } = node.attrs;
|
||||||
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
||||||
const [initialXML, setInitialXML] = useState<string>('');
|
const [initialXML, setInitialXML] = useState<string>("");
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const computedColorScheme = useComputedColorScheme();
|
||||||
|
|
||||||
const handleOpen = async () => {
|
const handleOpen = async () => {
|
||||||
if (!editor.isEditable) {
|
if (!editor.isEditable) {
|
||||||
@ -31,15 +41,15 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
if (src) {
|
if (src) {
|
||||||
const url = getFileUrl(src);
|
const url = getFileUrl(src);
|
||||||
const request = await fetch(url, {
|
const request = await fetch(url, {
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
cache: 'no-store',
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
const blob = await request.blob();
|
const blob = await request.blob();
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
let base64data = (reader.result || '') as string;
|
const base64data = (reader.result || "") as string;
|
||||||
setInitialXML(base64data);
|
setInitialXML(base64data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -53,7 +63,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
const handleSave = async (data: EventSave) => {
|
const handleSave = async (data: EventSave) => {
|
||||||
const svgString = decodeBase64ToSvgString(data.xml);
|
const svgString = decodeBase64ToSvgString(data.xml);
|
||||||
|
|
||||||
const fileName = 'diagram.drawio.svg';
|
const fileName = "diagram.drawio.svg";
|
||||||
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
||||||
|
|
||||||
const pageId = editor.storage?.pageId;
|
const pageId = editor.storage?.pageId;
|
||||||
@ -80,14 +90,15 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<Modal.Root opened={opened} onClose={close} fullScreen>
|
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||||
<Modal.Overlay />
|
<Modal.Overlay />
|
||||||
<Modal.Content style={{ overflow: 'hidden' }}>
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div style={{ height: '100vh' }}>
|
<div style={{ height: "100vh" }}>
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
xml={initialXML}
|
xml={initialXML}
|
||||||
|
baseUrl={getDrawioUrl()}
|
||||||
urlParameters={{
|
urlParameters={{
|
||||||
ui: 'kennedy',
|
ui: computedColorScheme === "light" ? "kennedy" : "dark",
|
||||||
spin: true,
|
spin: true,
|
||||||
libraries: true,
|
libraries: true,
|
||||||
saveAndExit: true,
|
saveAndExit: true,
|
||||||
@ -95,7 +106,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
}}
|
}}
|
||||||
onSave={(data: EventSave) => {
|
onSave={(data: EventSave) => {
|
||||||
// If the save is triggered by another event, then do nothing
|
// If the save is triggered by another event, then do nothing
|
||||||
if (data.parentEvent !== 'save') {
|
if (data.parentEvent !== "save") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleSave(data);
|
handleSave(data);
|
||||||
@ -114,7 +125,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
</Modal.Root>
|
</Modal.Root>
|
||||||
|
|
||||||
{src ? (
|
{src ? (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: "relative" }}>
|
||||||
<Image
|
<Image
|
||||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
radius="md"
|
radius="md"
|
||||||
@ -123,8 +134,8 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
src={getFileUrl(src)}
|
src={getFileUrl(src)}
|
||||||
alt={title}
|
alt={title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
selected ? 'ProseMirror-selectednode' : '',
|
selected ? "ProseMirror-selectednode" : "",
|
||||||
'alignCenter'
|
"alignCenter",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -135,7 +146,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
color="gray"
|
color="gray"
|
||||||
mx="xs"
|
mx="xs"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
}}
|
}}
|
||||||
@ -150,20 +161,20 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
p="xs"
|
p="xs"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
withBorder
|
withBorder
|
||||||
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<ActionIcon variant="transparent" color="gray">
|
<ActionIcon variant="transparent" color="gray">
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<Text component="span" size="lg" c="dimmed">
|
<Text component="span" size="lg" c="dimmed">
|
||||||
Double-click to edit drawio diagram
|
{t("Double-click to edit Draw.io diagram")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
132
apps/client/src/features/editor/components/embed/embed-view.tsx
Normal file
132
apps/client/src/features/editor/components/embed/embed-view.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
AspectRatio,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
FocusTrap,
|
||||||
|
Group,
|
||||||
|
Popover,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
getEmbedProviderById,
|
||||||
|
getEmbedUrlAndProvider,
|
||||||
|
} from "@/features/editor/components/embed/providers.ts";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
url: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.url({ message: i18n.t("Please enter a valid url") }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function EmbedView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { node, selected, updateAttributes } = props;
|
||||||
|
const { src, provider } = node.attrs;
|
||||||
|
|
||||||
|
const embedUrl = useMemo(() => {
|
||||||
|
if (src) {
|
||||||
|
return getEmbedUrlAndProvider(src).embedUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
const embedForm = useForm<{ url: string }>({
|
||||||
|
initialValues: {
|
||||||
|
url: "",
|
||||||
|
},
|
||||||
|
validate: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: { url: string }) {
|
||||||
|
if (provider) {
|
||||||
|
const embedProvider = getEmbedProviderById(provider);
|
||||||
|
if (embedProvider.regex.test(data.url)) {
|
||||||
|
updateAttributes({ src: data.url });
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
message: t("Invalid {{provider}} embed link", {
|
||||||
|
provider: embedProvider.name,
|
||||||
|
}),
|
||||||
|
position: "top-right",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
{embedUrl ? (
|
||||||
|
<>
|
||||||
|
<AspectRatio ratio={16 / 9}>
|
||||||
|
<iframe
|
||||||
|
src={embedUrl}
|
||||||
|
allow="encrypted-media"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||||
|
allowFullScreen
|
||||||
|
frameBorder="0"
|
||||||
|
></iframe>
|
||||||
|
</AspectRatio>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Popover width={300} position="bottom" withArrow shadow="md">
|
||||||
|
<Popover.Target>
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="xs"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
withBorder
|
||||||
|
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<ActionIcon variant="transparent" color="gray">
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<Text component="span" size="lg" c="dimmed">
|
||||||
|
{t("Embed {{provider}}", {
|
||||||
|
provider: getEmbedProviderById(provider).name,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown bg="var(--mantine-color-body)">
|
||||||
|
<form onSubmit={embedForm.onSubmit(onSubmit)}>
|
||||||
|
<FocusTrap active={true}>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t("Enter {{provider}} link to embed", {
|
||||||
|
provider: getEmbedProviderById(provider).name,
|
||||||
|
})}
|
||||||
|
key={embedForm.key("url")}
|
||||||
|
{...embedForm.getInputProps("url")}
|
||||||
|
data-autofocus
|
||||||
|
/>
|
||||||
|
</FocusTrap>
|
||||||
|
|
||||||
|
<Group justify="center" mt="xs">
|
||||||
|
<Button type="submit">{t("Embed link")}</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
apps/client/src/features/editor/components/embed/providers.ts
Normal file
129
apps/client/src/features/editor/components/embed/providers.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
export interface IEmbedProvider {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
regex: RegExp;
|
||||||
|
getEmbedUrl: (match: RegExpMatchArray, url?: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const embedProviders: IEmbedProvider[] = [
|
||||||
|
{
|
||||||
|
id: 'loom',
|
||||||
|
name: 'Loom',
|
||||||
|
regex: /^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\da-zA-Z]+)\/?/,
|
||||||
|
getEmbedUrl: (match, url) => {
|
||||||
|
if(url.includes("/embed/")){
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `https://loom.com/embed/${match[1]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'airtable',
|
||||||
|
name: 'Airtable',
|
||||||
|
regex: /^https:\/\/(www.)?airtable.com\/([a-zA-Z0-9]{2,})\/.*/,
|
||||||
|
getEmbedUrl: (match, url: string) => {
|
||||||
|
const path = url.split('airtable.com/');
|
||||||
|
if(url.includes("/embed/")){
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `https://airtable.com/embed/${path[1]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'figma',
|
||||||
|
name: 'Figma',
|
||||||
|
regex: /^https:\/\/[\w\.-]+\.?figma.com\/(file|proto|board|design|slides|deck)\/([0-9a-zA-Z]{22,128})/,
|
||||||
|
getEmbedUrl: (match, url: string) => {
|
||||||
|
return `https://www.figma.com/embed?url=${url}&embed_host=docmost`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'typeform',
|
||||||
|
name: 'Typeform',
|
||||||
|
regex: /^(https?:)?(\/\/)?[\w\.]+\.typeform\.com\/to\/.+/,
|
||||||
|
getEmbedUrl: (match, url: string) => {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'miro',
|
||||||
|
name: 'Miro',
|
||||||
|
regex: /^https:\/\/(www\.)?miro\.com\/app\/board\/([\w-]+=)/,
|
||||||
|
getEmbedUrl: (match, url) => {
|
||||||
|
if(url.includes("/live-embed/")){
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `https://miro.com/app/live-embed/${match[2]}?embedMode=view_only_without_ui&autoplay=true&embedSource=docmost`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'youtube',
|
||||||
|
name: 'YouTube',
|
||||||
|
regex: /^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/,
|
||||||
|
getEmbedUrl: (match, url) => {
|
||||||
|
if (url.includes("/embed/")){
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return `https://www.youtube-nocookie.com/embed/${match[5]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vimeo',
|
||||||
|
name: 'Vimeo',
|
||||||
|
regex: /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
|
||||||
|
getEmbedUrl: (match) => {
|
||||||
|
return `https://player.vimeo.com/video/${match[4]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'framer',
|
||||||
|
name: 'Framer',
|
||||||
|
regex: /^https:\/\/(www\.)?framer\.com\/embed\/([\w-]+)/,
|
||||||
|
getEmbedUrl: (match, url: string) => {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gdrive',
|
||||||
|
name: 'Google Drive',
|
||||||
|
regex: /^((?:https?:)?\/\/)?((?:www|m)\.)?(drive\.google\.com)\/file\/d\/([a-zA-Z0-9_-]+)\/.*$/,
|
||||||
|
getEmbedUrl: (match) => {
|
||||||
|
return `https://drive.google.com/file/d/${match[4]}/preview`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gsheets',
|
||||||
|
name: 'Google Sheets',
|
||||||
|
regex: /^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/e\/([a-zA-Z0-9_-]+)\/.*$/,
|
||||||
|
getEmbedUrl: (match, url: string) => {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getEmbedProviderById(id: string) {
|
||||||
|
return embedProviders.find(provider => provider.id.toLowerCase() === id.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEmbedResult {
|
||||||
|
embedUrl: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEmbedUrlAndProvider(url: string): IEmbedResult {
|
||||||
|
for (const provider of embedProviders) {
|
||||||
|
const match = url.match(provider.regex);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
embedUrl: provider.getEmbedUrl(match, url),
|
||||||
|
provider: provider.name.toLowerCase()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
embedUrl: url,
|
||||||
|
provider: 'iframe',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Button,
|
Button,
|
||||||
@ -7,27 +7,29 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Text,
|
Text,
|
||||||
useComputedColorScheme,
|
useComputedColorScheme,
|
||||||
} from '@mantine/core';
|
} from "@mantine/core";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { uploadFile } from '@/features/page/services/page-service.ts';
|
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||||
import { svgStringToFile } from '@/lib';
|
import { svgStringToFile } from "@/lib";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { getFileUrl } from '@/lib/config.ts';
|
import { getFileUrl } from "@/lib/config.ts";
|
||||||
import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types';
|
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types";
|
||||||
import { IAttachment } from '@/lib/types';
|
import { IAttachment } from "@/lib/types";
|
||||||
import ReactClearModal from 'react-clear-modal';
|
import ReactClearModal from "react-clear-modal";
|
||||||
import clsx from 'clsx';
|
import clsx from "clsx";
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
import { lazy } from 'react';
|
import { lazy } from "react";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Excalidraw = lazy(() =>
|
const Excalidraw = lazy(() =>
|
||||||
import('@excalidraw/excalidraw').then((module) => ({
|
import("@excalidraw/excalidraw").then((module) => ({
|
||||||
default: module.Excalidraw,
|
default: module.Excalidraw,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function ExcalidrawView(props: NodeViewProps) {
|
export default function ExcalidrawView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { node, updateAttributes, editor, selected } = props;
|
const { node, updateAttributes, editor, selected } = props;
|
||||||
const { src, title, width, attachmentId } = node.attrs;
|
const { src, title, width, attachmentId } = node.attrs;
|
||||||
|
|
||||||
@ -46,11 +48,11 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
if (src) {
|
if (src) {
|
||||||
const url = getFileUrl(src);
|
const url = getFileUrl(src);
|
||||||
const request = await fetch(url, {
|
const request = await fetch(url, {
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
cache: 'no-store',
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loadFromBlob } = await import('@excalidraw/excalidraw');
|
const { loadFromBlob } = await import("@excalidraw/excalidraw");
|
||||||
|
|
||||||
const data = await loadFromBlob(await request.blob(), null, null);
|
const data = await loadFromBlob(await request.blob(), null, null);
|
||||||
setExcalidrawData(data);
|
setExcalidrawData(data);
|
||||||
@ -67,13 +69,13 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { exportToSvg } = await import('@excalidraw/excalidraw');
|
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||||
|
|
||||||
const svg = await exportToSvg({
|
const svg = await exportToSvg({
|
||||||
elements: excalidrawAPI?.getSceneElements(),
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
appState: {
|
appState: {
|
||||||
exportEmbedScene: true,
|
exportEmbedScene: true,
|
||||||
exportWithDarkMode: computedColorScheme == 'light' ? false : true,
|
exportWithDarkMode: false,
|
||||||
},
|
},
|
||||||
files: excalidrawAPI?.getFiles(),
|
files: excalidrawAPI?.getFiles(),
|
||||||
});
|
});
|
||||||
@ -83,10 +85,10 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
|
|
||||||
svgString = svgString.replace(
|
svgString = svgString.replace(
|
||||||
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
|
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
|
||||||
'https://unpkg.com/@excalidraw/excalidraw@latest'
|
"https://unpkg.com/@excalidraw/excalidraw@latest",
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileName = 'diagram.excalidraw.svg';
|
const fileName = "diagram.excalidraw.svg";
|
||||||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
||||||
|
|
||||||
const pageId = editor.storage?.pageId;
|
const pageId = editor.storage?.pageId;
|
||||||
@ -112,7 +114,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<ReactClearModal
|
<ReactClearModal
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
}}
|
}}
|
||||||
@ -122,7 +124,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
contentProps={{
|
contentProps={{
|
||||||
style: {
|
style: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
width: '90vw',
|
width: "90vw",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -132,14 +134,14 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
bg="var(--mantine-color-body)"
|
bg="var(--mantine-color-body)"
|
||||||
p="xs"
|
p="xs"
|
||||||
>
|
>
|
||||||
<Button onClick={handleSave} size={'compact-sm'}>
|
<Button onClick={handleSave} size={"compact-sm"}>
|
||||||
Save & Exit
|
{t("Save & Exit")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={close} color="red" size={'compact-sm'}>
|
<Button onClick={close} color="red" size={"compact-sm"}>
|
||||||
Exit
|
{t("Exit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<div style={{ height: '90vh' }}>
|
<div style={{ height: "90vh" }}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||||
@ -147,13 +149,14 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
...excalidrawData,
|
...excalidrawData,
|
||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
}}
|
}}
|
||||||
|
theme={computedColorScheme}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</ReactClearModal>
|
</ReactClearModal>
|
||||||
|
|
||||||
{src ? (
|
{src ? (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: "relative" }}>
|
||||||
<Image
|
<Image
|
||||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
radius="md"
|
radius="md"
|
||||||
@ -162,8 +165,8 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
src={getFileUrl(src)}
|
src={getFileUrl(src)}
|
||||||
alt={title}
|
alt={title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
selected ? 'ProseMirror-selectednode' : '',
|
selected ? "ProseMirror-selectednode" : "",
|
||||||
'alignCenter'
|
"alignCenter",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -174,7 +177,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
color="gray"
|
color="gray"
|
||||||
mx="xs"
|
mx="xs"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
}}
|
}}
|
||||||
@ -189,20 +192,20 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||||
p="xs"
|
p="xs"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
withBorder
|
withBorder
|
||||||
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<ActionIcon variant="transparent" color="gray">
|
<ActionIcon variant="transparent" color="gray">
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<Text component="span" size="lg" c="dimmed">
|
<Text component="span" size="lg" c="dimmed">
|
||||||
Double-click to edit excalidraw diagram
|
{t("Double-click to edit Excalidraw diagram")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import {
|
|||||||
IconLayoutAlignRight,
|
IconLayoutAlignRight,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function ImageMenu({ editor }: EditorMenuProps) {
|
export function ImageMenu({ editor }: EditorMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -96,11 +98,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group className="actionIconGroup">
|
<ActionIcon.Group className="actionIconGroup">
|
||||||
<Tooltip position="top" label="Align image left">
|
<Tooltip position="top" label={t("Align left")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageLeft}
|
onClick={alignImageLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align image left"
|
aria-label={t("Align left")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "left" }) ? "light" : "default"
|
editor.isActive("image", { align: "left" }) ? "light" : "default"
|
||||||
}
|
}
|
||||||
@ -109,11 +111,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Align image center">
|
<Tooltip position="top" label={t("Align center")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageCenter}
|
onClick={alignImageCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align image center"
|
aria-label={t("Align center")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "center" })
|
editor.isActive("image", { align: "center" })
|
||||||
? "light"
|
? "light"
|
||||||
@ -124,11 +126,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Align image right">
|
<Tooltip position="top" label={t("Align right")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignImageRight}
|
onClick={alignImageRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align image right"
|
aria-label={t("Align right")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "right" }) ? "light" : "default"
|
editor.isActive("image", { align: "right" }) ? "light" : "default"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { handleImageUpload } from "@docmost/editor-ext";
|
import { handleImageUpload } from "@docmost/editor-ext";
|
||||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||||
|
import { formatBytes } from "@/lib";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
export const uploadImageAction = handleImageUpload({
|
export const uploadImageAction = handleImageUpload({
|
||||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||||
@ -18,10 +21,12 @@ export const uploadImageAction = handleImageUpload({
|
|||||||
if (!file.type.includes("image/")) {
|
if (!file.type.includes("image/")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (file.size / 1024 / 1024 > 50) {
|
if (file.size > getFileUploadSizeLimit()) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "red",
|
color: "red",
|
||||||
message: `File exceeds the 50 MB attachment limit`,
|
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||||
|
limit: formatBytes(getFileUploadSizeLimit()),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { Button, Group, TextInput } from "@mantine/core";
|
|||||||
import { IconLink } from "@tabler/icons-react";
|
import { IconLink } from "@tabler/icons-react";
|
||||||
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
||||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const LinkEditorPanel = ({
|
export const LinkEditorPanel = ({
|
||||||
onSetLink,
|
onSetLink,
|
||||||
initialUrl,
|
initialUrl,
|
||||||
}: LinkEditorPanelProps) => {
|
}: LinkEditorPanelProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const state = useLinkEditorState({
|
const state = useLinkEditorState({
|
||||||
onSetLink,
|
onSetLink,
|
||||||
initialUrl,
|
initialUrl,
|
||||||
@ -20,12 +22,12 @@ export const LinkEditorPanel = ({
|
|||||||
<TextInput
|
<TextInput
|
||||||
leftSection={<IconLink size={16} />}
|
leftSection={<IconLink size={16} />}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Paste link"
|
placeholder={t("Paste link")}
|
||||||
value={state.url}
|
value={state.url}
|
||||||
onChange={state.onChange}
|
onChange={state.onChange}
|
||||||
/>
|
/>
|
||||||
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
||||||
Save
|
{t("Save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export type LinkPreviewPanelProps = {
|
export type LinkPreviewPanelProps = {
|
||||||
url: string;
|
url: string;
|
||||||
@ -19,6 +20,8 @@ export const LinkPreviewPanel = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
url,
|
url,
|
||||||
}: LinkPreviewPanelProps) => {
|
}: LinkPreviewPanelProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||||
@ -42,13 +45,13 @@ export const LinkPreviewPanel = ({
|
|||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
<Divider mx={4} orientation="vertical" />
|
<Divider mx={4} orientation="vertical" />
|
||||||
|
|
||||||
<Tooltip label="Edit link">
|
<Tooltip label={t("Edit link")}>
|
||||||
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
||||||
<IconPencil size={16} />
|
<IconPencil size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Remove link">
|
<Tooltip label={t("Remove link")}>
|
||||||
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
||||||
<IconLinkOff size={16} />
|
<IconLinkOff size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import classes from "./math.module.css";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { IconTrashX } from "@tabler/icons-react";
|
import { IconTrashX } from "@tabler/icons-react";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function MathBlockView(props: NodeViewProps) {
|
export default function MathBlockView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { node, updateAttributes, editor, getPos } = props;
|
const { node, updateAttributes, editor, getPos } = props;
|
||||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||||
@ -94,9 +96,9 @@ export default function MathBlockView(props: NodeViewProps) {
|
|||||||
></div>
|
></div>
|
||||||
{((isEditing && !preview?.trim().length) ||
|
{((isEditing && !preview?.trim().length) ||
|
||||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||||
<div>Empty equation</div>
|
<div>{t("Empty equation")}</div>
|
||||||
)}
|
)}
|
||||||
{error && <div>Invalid equation</div>}
|
{error && <div>{t("Invalid equation")}</div>}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|||||||
import { Popover, Textarea } from "@mantine/core";
|
import { Popover, Textarea } from "@mantine/core";
|
||||||
import classes from "./math.module.css";
|
import classes from "./math.module.css";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function MathInlineView(props: NodeViewProps) {
|
export default function MathInlineView(props: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { node, updateAttributes, editor, getPos } = props;
|
const { node, updateAttributes, editor, getPos } = props;
|
||||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||||
@ -38,7 +40,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
|||||||
renderMath(preview || "", mathPreviewContainer.current);
|
renderMath(preview || "", mathPreviewContainer.current);
|
||||||
} else if (preview !== null) {
|
} else if (preview !== null) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
updateAttributes({ text: preview });
|
updateAttributes({ text: preview.trim() });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [preview, isEditing]);
|
}, [preview, isEditing]);
|
||||||
@ -84,9 +86,9 @@ export default function MathInlineView(props: NodeViewProps) {
|
|||||||
></div>
|
></div>
|
||||||
{((isEditing && !preview?.trim().length) ||
|
{((isEditing && !preview?.trim().length) ||
|
||||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||||
<div>Empty equation</div>
|
<div>{t("Empty equation")}</div>
|
||||||
)}
|
)}
|
||||||
{error && <div>Invalid equation</div>}
|
{error && <div>{t("Invalid equation")}</div>}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown p={"xs"}>
|
<Popover.Dropdown p={"xs"}>
|
||||||
@ -97,7 +99,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
|||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
classNames={{ input: classes.textInput }}
|
classNames={{ input: classes.textInput }}
|
||||||
value={preview?.trim() ?? ""}
|
value={preview ?? ""}
|
||||||
placeholder={"E = mc^2"}
|
placeholder={"E = mc^2"}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey)) {
|
if (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey)) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4));
|
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4));
|
||||||
@ -17,10 +18,6 @@
|
|||||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.error, .empty) * {
|
|
||||||
font-family: KaTeX_Main, Times New Roman, serif;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mathBlock {
|
.mathBlock {
|
||||||
@ -34,6 +31,7 @@
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
.textInput {
|
.textInput {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@ -52,10 +50,4 @@
|
|||||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.error, .empty) * {
|
|
||||||
font-family: KaTeX_Main, Times New Roman, serif;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import classes from "./slash-menu.module.css";
|
import classes from "./slash-menu.module.css";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CommandList = ({
|
const CommandList = ({
|
||||||
items,
|
items,
|
||||||
@ -25,6 +26,7 @@ const CommandList = ({
|
|||||||
editor: any;
|
editor: any;
|
||||||
range: any;
|
range: any;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const viewportRef = useRef<HTMLDivElement>(null);
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -104,18 +106,17 @@ const CommandList = ({
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
component="div"
|
component="div"
|
||||||
aria-label={item.title}
|
|
||||||
>
|
>
|
||||||
<item.icon size={18} />
|
<item.icon size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
{item.title}
|
{t(item.title)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text c="dimmed" size="xs">
|
<Text c="dimmed" size="xs">
|
||||||
{item.description}
|
{t(item.description)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@ -29,6 +29,17 @@ import { uploadAttachmentAction } from "@/features/editor/components/attachment/
|
|||||||
import IconExcalidraw from "@/components/icons/icon-excalidraw";
|
import IconExcalidraw from "@/components/icons/icon-excalidraw";
|
||||||
import IconMermaid from "@/components/icons/icon-mermaid";
|
import IconMermaid from "@/components/icons/icon-mermaid";
|
||||||
import IconDrawio from "@/components/icons/icon-drawio";
|
import IconDrawio from "@/components/icons/icon-drawio";
|
||||||
|
import {
|
||||||
|
AirtableIcon,
|
||||||
|
FigmaIcon,
|
||||||
|
FramerIcon,
|
||||||
|
GoogleDriveIcon,
|
||||||
|
GoogleSheetsIcon,
|
||||||
|
LoomIcon,
|
||||||
|
MiroIcon,
|
||||||
|
TypeformIcon,
|
||||||
|
VimeoIcon, YoutubeIcon
|
||||||
|
} from "@/components/icons";
|
||||||
|
|
||||||
const CommandGroups: SlashMenuGroupedItemsType = {
|
const CommandGroups: SlashMenuGroupedItemsType = {
|
||||||
basic: [
|
basic: [
|
||||||
@ -343,7 +354,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
return editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.deleteRange(range)
|
.deleteRange(range)
|
||||||
@ -351,6 +362,96 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Airtable",
|
||||||
|
description: "Embed Airtable",
|
||||||
|
searchTerms: ["airtable"],
|
||||||
|
icon: AirtableIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'airtable' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Loom",
|
||||||
|
description: "Embed Loom video",
|
||||||
|
searchTerms: ["loom"],
|
||||||
|
icon: LoomIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'loom' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Figma",
|
||||||
|
description: "Embed Figma files",
|
||||||
|
searchTerms: ["figma"],
|
||||||
|
icon: FigmaIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'figma' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Typeform",
|
||||||
|
description: "Embed Typeform",
|
||||||
|
searchTerms: ["typeform"],
|
||||||
|
icon: TypeformIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'typeform' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Miro",
|
||||||
|
description: "Embed Miro board",
|
||||||
|
searchTerms: ["miro"],
|
||||||
|
icon: MiroIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'miro' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "YouTube",
|
||||||
|
description: "Embed YouTube video",
|
||||||
|
searchTerms: ["youtube", "yt"],
|
||||||
|
icon: YoutubeIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'youtube' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vimeo",
|
||||||
|
description: "Embed Vimeo video",
|
||||||
|
searchTerms: ["vimeo"],
|
||||||
|
icon: VimeoIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'vimeo' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Framer",
|
||||||
|
description: "Embed Framer prototype",
|
||||||
|
searchTerms: ["framer"],
|
||||||
|
icon: FramerIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'framer' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Google Drive",
|
||||||
|
description: "Embed Google Drive content",
|
||||||
|
searchTerms: ["google drive", "gdrive"],
|
||||||
|
icon: GoogleDriveIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gdrive' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Google Sheets",
|
||||||
|
description: "Embed Google Sheets content",
|
||||||
|
searchTerms: ["google sheets", "gsheets"],
|
||||||
|
icon: GoogleSheetsIcon,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gsheets' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -362,10 +463,10 @@ export const getSuggestionItems = ({
|
|||||||
const search = query.toLowerCase();
|
const search = query.toLowerCase();
|
||||||
const filteredGroups: SlashMenuGroupedItemsType = {};
|
const filteredGroups: SlashMenuGroupedItemsType = {};
|
||||||
|
|
||||||
const fuzzyMatch = (query, target) => {
|
const fuzzyMatch = (query: string, target: string) => {
|
||||||
let queryIndex = 0;
|
let queryIndex = 0;
|
||||||
target = target.toLowerCase();
|
target = target.toLowerCase();
|
||||||
for (let char of target) {
|
for (const char of target) {
|
||||||
if (query[queryIndex] === char) queryIndex++;
|
if (query[queryIndex] === char) queryIndex++;
|
||||||
if (queryIndex === query.length) return true;
|
if (queryIndex === query.length) return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,11 @@ import {
|
|||||||
IconRowRemove,
|
IconRowRemove,
|
||||||
IconSquareToggle,
|
IconSquareToggle,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const TableCellMenu = React.memo(
|
export const TableCellMenu = React.memo(
|
||||||
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
|
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ view, state, from }: ShouldShowProps) => {
|
({ view, state, from }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -58,45 +60,45 @@ export const TableCellMenu = React.memo(
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group>
|
<ActionIcon.Group>
|
||||||
<Tooltip position="top" label="Merge cells">
|
<Tooltip position="top" label={t("Merge cells")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={mergeCells}
|
onClick={mergeCells}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Merge cells"
|
aria-label={t("Merge cells")}
|
||||||
>
|
>
|
||||||
<IconBoxMargin size={18} />
|
<IconBoxMargin size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Split cell">
|
<Tooltip position="top" label={t("Split cell")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={splitCell}
|
onClick={splitCell}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Split cell"
|
aria-label={t("Split cell")}
|
||||||
>
|
>
|
||||||
<IconSquareToggle size={18} />
|
<IconSquareToggle size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Delete column">
|
<Tooltip position="top" label={t("Delete column")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteColumn}
|
onClick={deleteColumn}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Delete column"
|
aria-label={t("Delete column")}
|
||||||
>
|
>
|
||||||
<IconColumnRemove size={18} />
|
<IconColumnRemove size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Delete row">
|
<Tooltip position="top" label={t("Delete row")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteRow}
|
onClick={deleteRow}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Delete row"
|
aria-label={t("Delete row")}
|
||||||
>
|
>
|
||||||
<IconRowRemove size={18} />
|
<IconRowRemove size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@ -21,9 +21,11 @@ import {
|
|||||||
IconTrashX,
|
IconTrashX,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { isCellSelection } from "@docmost/editor-ext";
|
import { isCellSelection } from "@docmost/editor-ext";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const TableMenu = React.memo(
|
export const TableMenu = React.memo(
|
||||||
({ editor }: EditorMenuProps): JSX.Element => {
|
({ editor }: EditorMenuProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -111,79 +113,80 @@ export const TableMenu = React.memo(
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group>
|
<ActionIcon.Group>
|
||||||
<Tooltip position="top" label="Add left column">
|
<Tooltip position="top" label={t("Add left column")}
|
||||||
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={addColumnLeft}
|
onClick={addColumnLeft}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Add left column"
|
aria-label={t("Add left column")}
|
||||||
>
|
>
|
||||||
<IconColumnInsertLeft size={18} />
|
<IconColumnInsertLeft size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Add right column">
|
<Tooltip position="top" label={t("Add right column")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={addColumnRight}
|
onClick={addColumnRight}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Add right column"
|
aria-label={t("Add right column")}
|
||||||
>
|
>
|
||||||
<IconColumnInsertRight size={18} />
|
<IconColumnInsertRight size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Delete column">
|
<Tooltip position="top" label={t("Delete column")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteColumn}
|
onClick={deleteColumn}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Delete column"
|
aria-label={t("Delete column")}
|
||||||
>
|
>
|
||||||
<IconColumnRemove size={18} />
|
<IconColumnRemove size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Add row above">
|
<Tooltip position="top" label={t("Add row above")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={addRowAbove}
|
onClick={addRowAbove}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Add row above"
|
aria-label={t("Add row above")}
|
||||||
>
|
>
|
||||||
<IconRowInsertTop size={18} />
|
<IconRowInsertTop size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Add row below">
|
<Tooltip position="top" label={t("Add row below")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={addRowBelow}
|
onClick={addRowBelow}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Add row below"
|
aria-label={t("Add row below")}
|
||||||
>
|
>
|
||||||
<IconRowInsertBottom size={18} />
|
<IconRowInsertBottom size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Delete row">
|
<Tooltip position="top" label={t("Delete row")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteRow}
|
onClick={deleteRow}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Delete row"
|
aria-label={t("Delete row")}
|
||||||
>
|
>
|
||||||
<IconRowRemove size={18} />
|
<IconRowRemove size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Delete table">
|
<Tooltip position="top" label={t("Delete table")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteTable}
|
onClick={deleteTable}
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
color="red"
|
color="red"
|
||||||
aria-label="Delete table"
|
aria-label={t("Delete table")}
|
||||||
>
|
>
|
||||||
<IconTrashX size={18} />
|
<IconTrashX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { handleVideoUpload } from "@docmost/editor-ext";
|
import { handleVideoUpload } from "@docmost/editor-ext";
|
||||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||||
|
import { formatBytes } from "@/lib";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
export const uploadVideoAction = handleVideoUpload({
|
export const uploadVideoAction = handleVideoUpload({
|
||||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||||
@ -19,10 +22,12 @@ export const uploadVideoAction = handleVideoUpload({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size / 1024 / 1024 > 50) {
|
if (file.size > getFileUploadSizeLimit()) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "red",
|
color: "red",
|
||||||
message: `File exceeds the 50 MB attachment limit`,
|
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||||
|
limit: formatBytes(getFileUploadSizeLimit()),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import {
|
|||||||
IconLayoutAlignRight,
|
IconLayoutAlignRight,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function VideoMenu({ editor }: EditorMenuProps) {
|
export function VideoMenu({ editor }: EditorMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -96,11 +98,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group className="actionIconGroup">
|
<ActionIcon.Group className="actionIconGroup">
|
||||||
<Tooltip position="top" label="Align video left">
|
<Tooltip position="top" label={t("Align left")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignVideoLeft}
|
onClick={alignVideoLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align video left"
|
aria-label={t("Align left")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "left" }) ? "light" : "default"
|
editor.isActive("video", { align: "left" }) ? "light" : "default"
|
||||||
}
|
}
|
||||||
@ -109,11 +111,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Align video center">
|
<Tooltip position="top" label={t("Align center")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignVideoCenter}
|
onClick={alignVideoCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align video center"
|
aria-label={t("Align center")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "center" })
|
editor.isActive("video", { align: "center" })
|
||||||
? "light"
|
? "light"
|
||||||
@ -124,11 +126,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label="Align video right">
|
<Tooltip position="top" label={t("Align right")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={alignVideoRight}
|
onClick={alignVideoRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label="Align video right"
|
aria-label={t("Align right")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "right" }) ? "light" : "default"
|
editor.isActive("video", { align: "right" }) ? "light" : "default"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
CustomCodeBlock,
|
CustomCodeBlock,
|
||||||
Drawio,
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
|
Embed,
|
||||||
} from "@docmost/editor-ext";
|
} from "@docmost/editor-ext";
|
||||||
import {
|
import {
|
||||||
randomElement,
|
randomElement,
|
||||||
@ -53,6 +54,7 @@ import AttachmentView from "@/features/editor/components/attachment/attachment-v
|
|||||||
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
||||||
import DrawioView from "../components/drawio/drawio-view";
|
import DrawioView from "../components/drawio/drawio-view";
|
||||||
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
||||||
|
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
||||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||||
import powershell from "highlight.js/lib/languages/powershell";
|
import powershell from "highlight.js/lib/languages/powershell";
|
||||||
import elixir from "highlight.js/lib/languages/elixir";
|
import elixir from "highlight.js/lib/languages/elixir";
|
||||||
@ -62,6 +64,8 @@ import clojure from "highlight.js/lib/languages/clojure";
|
|||||||
import fortran from "highlight.js/lib/languages/fortran";
|
import fortran from "highlight.js/lib/languages/fortran";
|
||||||
import haskell from "highlight.js/lib/languages/haskell";
|
import haskell from "highlight.js/lib/languages/haskell";
|
||||||
import scala from "highlight.js/lib/languages/scala";
|
import scala from "highlight.js/lib/languages/scala";
|
||||||
|
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("mermaid", plaintext);
|
lowlight.register("mermaid", plaintext);
|
||||||
@ -92,13 +96,13 @@ export const mainExtensions = [
|
|||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: ({ node }) => {
|
placeholder: ({ node }) => {
|
||||||
if (node.type.name === "heading") {
|
if (node.type.name === "heading") {
|
||||||
return `Heading ${node.attrs.level}`;
|
return i18n.t("Heading {{level}}", { level: node.attrs.level });
|
||||||
}
|
}
|
||||||
if (node.type.name === "detailsSummary") {
|
if (node.type.name === "detailsSummary") {
|
||||||
return "Toggle title";
|
return i18n.t("Toggle title");
|
||||||
}
|
}
|
||||||
if (node.type.name === "paragraph") {
|
if (node.type.name === "paragraph") {
|
||||||
return 'Write anything. Enter "/" for commands';
|
return i18n.t('Write anything. Enter "/" for commands');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
@ -129,7 +133,6 @@ export const mainExtensions = [
|
|||||||
class: "comment-mark",
|
class: "comment-mark",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Table.configure({
|
Table.configure({
|
||||||
resizable: true,
|
resizable: true,
|
||||||
lastColumnResizable: false,
|
lastColumnResizable: false,
|
||||||
@ -138,7 +141,6 @@ export const mainExtensions = [
|
|||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
|
|
||||||
MathInline.configure({
|
MathInline.configure({
|
||||||
view: MathInlineView,
|
view: MathInlineView,
|
||||||
}),
|
}),
|
||||||
@ -149,6 +151,7 @@ export const mainExtensions = [
|
|||||||
DetailsSummary,
|
DetailsSummary,
|
||||||
DetailsContent,
|
DetailsContent,
|
||||||
Youtube.configure({
|
Youtube.configure({
|
||||||
|
addPasteHandler: false,
|
||||||
controls: true,
|
controls: true,
|
||||||
nocookie: true,
|
nocookie: true,
|
||||||
}),
|
}),
|
||||||
@ -179,6 +182,12 @@ export const mainExtensions = [
|
|||||||
Excalidraw.configure({
|
Excalidraw.configure({
|
||||||
view: ExcalidrawView,
|
view: ExcalidrawView,
|
||||||
}),
|
}),
|
||||||
|
Embed.configure({
|
||||||
|
view: EmbedView,
|
||||||
|
}),
|
||||||
|
MarkdownClipboard.configure({
|
||||||
|
transformPastedText: true,
|
||||||
|
}),
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
// adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT
|
||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { DOMParser } from "@tiptap/pm/model";
|
||||||
|
import { find } from "linkifyjs";
|
||||||
|
import { markdownToHtml } from "@docmost/editor-ext";
|
||||||
|
|
||||||
|
export const MarkdownClipboard = Extension.create({
|
||||||
|
name: "markdownClipboard",
|
||||||
|
priority: 50,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
transformPastedText: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey("markdownClipboard"),
|
||||||
|
props: {
|
||||||
|
clipboardTextParser: (text, context, plainText) => {
|
||||||
|
const link = find(text, {
|
||||||
|
defaultProtocol: "http",
|
||||||
|
}).find((item) => item.isLink && item.value === text);
|
||||||
|
|
||||||
|
if (plainText || !this.options.transformPastedText || link) {
|
||||||
|
// don't parse plaintext link to allow link paste handler to work
|
||||||
|
// pasting with shift key prevents formatting
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = markdownToHtml(text);
|
||||||
|
return DOMParser.fromSchema(this.editor.schema).parseSlice(
|
||||||
|
elementFromString(parsed),
|
||||||
|
{
|
||||||
|
preserveWhitespace: true,
|
||||||
|
context,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function elementFromString(value) {
|
||||||
|
// add a wrapper to preserve leading and trailing whitespace
|
||||||
|
const wrappedValue = `<body>${value}</body>`;
|
||||||
|
|
||||||
|
return new window.DOMParser().parseFromString(wrappedValue, "text/html").body;
|
||||||
|
}
|
||||||
@ -30,8 +30,7 @@ export function FullEditor({
|
|||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
fluid={fullPageWidth}
|
fluid={fullPageWidth}
|
||||||
{...(fullPageWidth && { mx: 80 })}
|
size={!fullPageWidth && 850}
|
||||||
size={850}
|
|
||||||
className={classes.editor}
|
className={classes.editor}
|
||||||
>
|
>
|
||||||
<MemoizedTitleEditor
|
<MemoizedTitleEditor
|
||||||
|
|||||||
@ -184,6 +184,7 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div onClick={() => editor.commands.focus('end')} style={{ paddingBottom: '20vh' }}></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EditorSkeleton />
|
<EditorSkeleton />
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
font-size: inherit;
|
font-size: var(--mantine-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code styling */
|
/* Code styling */
|
||||||
@ -103,12 +103,12 @@
|
|||||||
|
|
||||||
@mixin where-light {
|
@mixin where-light {
|
||||||
background-color: var(--code-bg, var(--mantine-color-gray-1));
|
background-color: var(--code-bg, var(--mantine-color-gray-1));
|
||||||
color: var(--mantine-color-black);
|
color: var(--mantine-color-pink-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin where-dark {
|
@mixin where-dark {
|
||||||
background-color: var(--mantine-color-dark-8);
|
background-color: var(--mantine-color-dark-8);
|
||||||
color: var(--mantine-color-gray-4);
|
color: var(--mantine-color-pink-7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,7 @@ import {
|
|||||||
pageEditorAtom,
|
pageEditorAtom,
|
||||||
titleEditorAtom,
|
titleEditorAtom,
|
||||||
} from "@/features/editor/atoms/editor-atoms";
|
} from "@/features/editor/atoms/editor-atoms";
|
||||||
import {
|
import { useUpdatePageMutation } from "@/features/page/queries/page-query";
|
||||||
usePageQuery,
|
|
||||||
useUpdatePageMutation,
|
|
||||||
} from "@/features/page/queries/page-query";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
||||||
@ -21,7 +18,8 @@ import { updateTreeNodeName } from "@/features/page/tree/utils";
|
|||||||
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||||
import { History } from "@tiptap/extension-history";
|
import { History } from "@tiptap/extension-history";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface TitleEditorProps {
|
export interface TitleEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -38,15 +36,20 @@ export function TitleEditor({
|
|||||||
spaceSlug,
|
spaceSlug,
|
||||||
editable,
|
editable,
|
||||||
}: TitleEditorProps) {
|
}: TitleEditorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
||||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000);
|
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
|
||||||
const updatePageMutation = useUpdatePageMutation();
|
const {
|
||||||
|
data: updatedPageData,
|
||||||
|
mutate: updatePageMutation,
|
||||||
|
status,
|
||||||
|
} = useUpdatePageMutation();
|
||||||
const pageEditor = useAtomValue(pageEditorAtom);
|
const pageEditor = useAtomValue(pageEditorAtom);
|
||||||
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
||||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||||
const emit = useQueryEmit();
|
const emit = useQueryEmit();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [activePageId, setActivePageId] = useState(pageId);
|
||||||
|
|
||||||
const titleEditor = useEditor({
|
const titleEditor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
@ -58,7 +61,7 @@ export function TitleEditor({
|
|||||||
}),
|
}),
|
||||||
Text,
|
Text,
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: "Untitled",
|
placeholder: t("Untitled"),
|
||||||
showOnlyWhenEditable: false,
|
showOnlyWhenEditable: false,
|
||||||
}),
|
}),
|
||||||
History.configure({
|
History.configure({
|
||||||
@ -74,6 +77,7 @@ export function TitleEditor({
|
|||||||
onUpdate({ editor }) {
|
onUpdate({ editor }) {
|
||||||
const currentTitle = editor.getText();
|
const currentTitle = editor.getText();
|
||||||
setDebouncedTitleState(currentTitle);
|
setDebouncedTitleState(currentTitle);
|
||||||
|
setActivePageId(pageId);
|
||||||
},
|
},
|
||||||
editable: editable,
|
editable: editable,
|
||||||
content: title,
|
content: title,
|
||||||
@ -85,25 +89,30 @@ export function TitleEditor({
|
|||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedTitle !== null) {
|
if (debouncedTitle !== null && activePageId === pageId) {
|
||||||
updatePageMutation.mutate({
|
updatePageMutation({
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
title: debouncedTitle,
|
title: debouncedTitle,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}, [debouncedTitle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === "success" && updatedPageData) {
|
||||||
|
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
|
||||||
|
setTreeData(newTreeData);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "updateOne",
|
operation: "updateOne",
|
||||||
|
spaceId: updatedPageData.spaceId,
|
||||||
entity: ["pages"],
|
entity: ["pages"],
|
||||||
id: pageId,
|
id: pageId,
|
||||||
payload: { title: debouncedTitle, slugId: slugId },
|
payload: { title: debouncedTitle, slugId: slugId },
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
|
|
||||||
setTreeData(newTreeData);
|
|
||||||
}
|
}
|
||||||
}, [debouncedTitle]);
|
}, [updatedPageData, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (titleEditor && title !== titleEditor.getText()) {
|
if (titleEditor && title !== titleEditor.getText()) {
|
||||||
|
|||||||
@ -4,8 +4,10 @@ import React, { useState } from "react";
|
|||||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts";
|
import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function AddGroupMemberModal() {
|
export default function AddGroupMemberModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [userIds, setUserIds] = useState<string[]>([]);
|
const [userIds, setUserIds] = useState<string[]>([]);
|
||||||
@ -27,19 +29,19 @@ export default function AddGroupMemberModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={open}>Add group members</Button>
|
<Button onClick={open}>{t("Add group members")}</Button>
|
||||||
|
|
||||||
<Modal opened={opened} onClose={close} title="Add group members">
|
<Modal opened={opened} onClose={close} title={t("Add group members")}>
|
||||||
<Divider size="xs" mb="xs" />
|
<Divider size="xs" mb="xs" />
|
||||||
|
|
||||||
<MultiUserSelect
|
<MultiUserSelect
|
||||||
label={"Add group members"}
|
label={t("Add group members")}
|
||||||
onChange={handleMultiSelectChange}
|
onChange={handleMultiSelectChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button onClick={handleSubmit} type="submit">
|
<Button onClick={handleSubmit} type="submit">
|
||||||
Add
|
{t("Add")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -5,15 +5,17 @@ import { useForm, zodResolver } from "@mantine/form";
|
|||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().trim().min(2).max(50),
|
||||||
description: z.string().max(500),
|
description: z.string().max(500),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export function CreateGroupForm() {
|
export function CreateGroupForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const createGroupMutation = useCreateGroupMutation();
|
const createGroupMutation = useCreateGroupMutation();
|
||||||
const [userIds, setUserIds] = useState<string[]>([]);
|
const [userIds, setUserIds] = useState<string[]>([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -52,16 +54,16 @@ export function CreateGroupForm() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
withAsterisk
|
withAsterisk
|
||||||
id="name"
|
id="name"
|
||||||
label="Group name"
|
label={t("Group name")}
|
||||||
placeholder="e.g Developers"
|
placeholder={t("e.g Developers")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
label="Group description"
|
label={t("Group description")}
|
||||||
placeholder="e.g Group for developers"
|
placeholder={t("e.g Group for developers")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
autosize
|
autosize
|
||||||
minRows={2}
|
minRows={2}
|
||||||
@ -70,13 +72,13 @@ export function CreateGroupForm() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MultiUserSelect
|
<MultiUserSelect
|
||||||
label={"Add group members"}
|
label={t("Add group members")}
|
||||||
onChange={handleMultiSelectChange}
|
onChange={handleMultiSelectChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button type="submit">Create</Button>
|
<Button type="submit">{t("Create")}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { Button, Divider, Modal } from "@mantine/core";
|
import { Button, Divider, Modal } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { CreateGroupForm } from "@/features/group/components/create-group-form.tsx";
|
import { CreateGroupForm } from "@/features/group/components/create-group-form.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function CreateGroupModal() {
|
export default function CreateGroupModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={open}>Create group</Button>
|
<Button onClick={open}>{t("Create group")}</Button>
|
||||||
|
|
||||||
<Modal opened={opened} onClose={close} title="Create group">
|
<Modal opened={opened} onClose={close} title={t("Create group")}>
|
||||||
<Divider size="xs" mb="xs" />
|
<Divider size="xs" mb="xs" />
|
||||||
<CreateGroupForm />
|
<CreateGroupForm />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import { useForm, zodResolver } from "@mantine/form";
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().min(2).max(50),
|
||||||
@ -18,6 +19,7 @@ interface EditGroupFormProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const updateGroupMutation = useUpdateGroupMutation();
|
const updateGroupMutation = useUpdateGroupMutation();
|
||||||
const { isSuccess } = updateGroupMutation;
|
const { isSuccess } = updateGroupMutation;
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
@ -60,16 +62,16 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
withAsterisk
|
withAsterisk
|
||||||
id="name"
|
id="name"
|
||||||
label="Group name"
|
label={t("Group name")}
|
||||||
placeholder="e.g Developers"
|
placeholder={t("e.g Developers")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
label="Group description"
|
label={t("Group description")}
|
||||||
placeholder="e.g Group for developers"
|
placeholder={t("e.g Group for developers")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
autosize
|
autosize
|
||||||
minRows={2}
|
minRows={2}
|
||||||
@ -79,7 +81,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button type="submit">Edit</Button>
|
<Button type="submit">{t("Save")}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Divider, Modal } from "@mantine/core";
|
import { Divider, Modal } from "@mantine/core";
|
||||||
import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx";
|
import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface EditGroupModalProps {
|
interface EditGroupModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@ -10,9 +11,11 @@ export default function EditGroupModal({
|
|||||||
opened,
|
opened,
|
||||||
onClose,
|
onClose,
|
||||||
}: EditGroupModalProps) {
|
}: EditGroupModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal opened={opened} onClose={onClose} title="Edit group">
|
<Modal opened={opened} onClose={onClose} title={t("Edit group")}>
|
||||||
<Divider size="xs" mb="xs" />
|
<Divider size="xs" mb="xs" />
|
||||||
<EditGroupForm onClose={onClose} />
|
<EditGroupForm onClose={onClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -9,8 +9,10 @@ import { IconDots, IconTrash } from "@tabler/icons-react";
|
|||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function GroupActionMenu() {
|
export default function GroupActionMenu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const { data: group, isLoading } = useGroupQuery(groupId);
|
const { data: group, isLoading } = useGroupQuery(groupId);
|
||||||
const deleteGroupMutation = useDeleteGroupMutation();
|
const deleteGroupMutation = useDeleteGroupMutation();
|
||||||
@ -24,15 +26,16 @@ export default function GroupActionMenu() {
|
|||||||
|
|
||||||
const openDeleteModal = () =>
|
const openDeleteModal = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: "Delete group",
|
title: t("Delete group"),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Are you sure you want to delete this group? Members will lose access
|
{t(
|
||||||
to resources this group has access to.
|
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: onDelete,
|
onConfirm: onDelete,
|
||||||
});
|
});
|
||||||
@ -57,7 +60,7 @@ export default function GroupActionMenu() {
|
|||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item onClick={open} disabled={group.isDefault}>
|
<Menu.Item onClick={open} disabled={group.isDefault}>
|
||||||
Edit group
|
{t("Edit group")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -66,7 +69,7 @@ export default function GroupActionMenu() {
|
|||||||
disabled={group.isDefault}
|
disabled={group.isDefault}
|
||||||
leftSection={<IconTrash size={16} stroke={2} />}
|
leftSection={<IconTrash size={16} stroke={2} />}
|
||||||
>
|
>
|
||||||
Delete group
|
{t("Delete group")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useDisclosure } from "@mantine/hooks";
|
|||||||
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
import EditGroupModal from "@/features/group/components/edit-group-modal.tsx";
|
||||||
import GroupActionMenu from "@/features/group/components/group-action-menu.tsx";
|
import GroupActionMenu from "@/features/group/components/group-action-menu.tsx";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function GroupDetails() {
|
export default function GroupDetails() {
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
|
|||||||
@ -3,23 +3,28 @@ import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
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";
|
||||||
|
|
||||||
export default function GroupList() {
|
export default function GroupList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useGetGroupsQuery();
|
const { data, isLoading } = useGetGroupsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
|
<Table.ScrollContainer minWidth={400}>
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>Group</Table.Th>
|
<Table.Th>{t("Group")}</Table.Th>
|
||||||
<Table.Th>Members</Table.Th>
|
<Table.Th>{t("Members")}</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((group, index) => (
|
{data?.items.map((group: IGroup, index: number) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Anchor
|
<Anchor
|
||||||
@ -32,20 +37,19 @@ export default function GroupList() {
|
|||||||
component={Link}
|
component={Link}
|
||||||
to={`/settings/groups/${group.id}`}
|
to={`/settings/groups/${group.id}`}
|
||||||
>
|
>
|
||||||
<Group gap="sm">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<IconGroupCircle />
|
<IconGroupCircle />
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
{group.name}
|
{group.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed">
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
{group.description}
|
{group.description}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -53,17 +57,19 @@ export default function GroupList() {
|
|||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: "var(--mantine-color-text)",
|
color: "var(--mantine-color-text)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/settings/groups/${group.id}`}
|
to={`/settings/groups/${group.id}`}
|
||||||
>
|
>
|
||||||
{group.memberCount} members
|
{formatMemberCount(group.memberCount, t)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,8 +9,11 @@ import { IconDots } from "@tabler/icons-react";
|
|||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
|
||||||
export default function GroupMembersList() {
|
export default function GroupMembersList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const { data, isLoading } = useGroupMembersQuery(groupId);
|
const { data, isLoading } = useGroupMembersQuery(groupId);
|
||||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||||
@ -26,15 +29,16 @@ export default function GroupMembersList() {
|
|||||||
|
|
||||||
const openRemoveModal = (userId: string) =>
|
const openRemoveModal = (userId: string) =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: "Remove group member",
|
title: t("Remove group member"),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Are you sure you want to remove this user from the group? The user
|
{t(
|
||||||
will lose access to resources this group has access to.
|
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
centered: true,
|
centered: true,
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => onRemove(userId),
|
onConfirm: () => onRemove(userId),
|
||||||
});
|
});
|
||||||
@ -42,21 +46,25 @@ export default function GroupMembersList() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
{data && (
|
||||||
|
<Table.ScrollContainer minWidth={500}>
|
||||||
<Table verticalSpacing="sm">
|
<Table verticalSpacing="sm">
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>User</Table.Th>
|
<Table.Th>{t("User")}</Table.Th>
|
||||||
<Table.Th>Status</Table.Th>
|
<Table.Th>{t("Status")}</Table.Th>
|
||||||
<Table.Th></Table.Th>
|
<Table.Th></Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{data?.items.map((user, index) => (
|
{data?.items.map((user: IUser, index: number) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
|
<CustomAvatar
|
||||||
|
avatarUrl={user.avatarUrl}
|
||||||
|
name={user.name}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500}>
|
||||||
{user.name}
|
{user.name}
|
||||||
@ -67,11 +75,9 @@ export default function GroupMembersList() {
|
|||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge variant="light">Active</Badge>
|
<Badge variant="light">{t("Active")}</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Menu
|
<Menu
|
||||||
@ -87,10 +93,9 @@ export default function GroupMembersList() {
|
|||||||
<IconDots size={20} stroke={2} />
|
<IconDots size={20} stroke={2} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item onClick={() => openRemoveModal(user.id)}>
|
<Menu.Item onClick={() => openRemoveModal(user.id)}>
|
||||||
Remove group member
|
{t("Remove group member")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
@ -100,6 +105,7 @@ export default function GroupMembersList() {
|
|||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Group, MultiSelect, MultiSelectProps, Text } from "@mantine/core";
|
|||||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query.ts";
|
import { useGetGroupsQuery } from "@/features/group/queries/group-query.ts";
|
||||||
import { IGroup } from "@/features/group/types/group.types.ts";
|
import { IGroup } from "@/features/group/types/group.types.ts";
|
||||||
import { IconUsersGroup } from "@tabler/icons-react";
|
import { IconUsersGroup } from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface MultiGroupSelectProps {
|
interface MultiGroupSelectProps {
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
@ -29,6 +30,7 @@ export function MultiGroupSelect({
|
|||||||
description,
|
description,
|
||||||
mt,
|
mt,
|
||||||
}: MultiGroupSelectProps) {
|
}: MultiGroupSelectProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||||
const { data: groups, isLoading } = useGetGroupsQuery({
|
const { data: groups, isLoading } = useGetGroupsQuery({
|
||||||
@ -39,17 +41,19 @@ export function MultiGroupSelect({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groups) {
|
if (groups) {
|
||||||
const groupsData = groups?.items.map((group: IGroup) => {
|
const groupsData = groups?.items
|
||||||
|
.filter((group: IGroup) => group.name.toLowerCase() !== 'everyone')
|
||||||
|
.map((group: IGroup) => {
|
||||||
return {
|
return {
|
||||||
value: group.id,
|
value: group.id,
|
||||||
label: group.name,
|
label: group.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter out existing users by their ids
|
// Filter out existing groups by their ids
|
||||||
const filteredGroupData = groupsData.filter(
|
const filteredGroupData = groupsData.filter(
|
||||||
(user) =>
|
(group) =>
|
||||||
!data.find((existingUser) => existingUser.value === user.value),
|
!data.find((existingGroup) => existingGroup.value === group.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine existing data with new search data
|
// Combine existing data with new search data
|
||||||
@ -64,8 +68,8 @@ export function MultiGroupSelect({
|
|||||||
hidePickedOptions
|
hidePickedOptions
|
||||||
maxDropdownHeight={300}
|
maxDropdownHeight={300}
|
||||||
description={description}
|
description={description}
|
||||||
label={label || "Add groups"}
|
label={label || t("Add groups")}
|
||||||
placeholder="Search for groups"
|
placeholder={t("Search for groups")}
|
||||||
mt={mt}
|
mt={mt}
|
||||||
searchable
|
searchable
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
@ -73,7 +77,7 @@ export function MultiGroupSelect({
|
|||||||
clearable
|
clearable
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
nothingFoundMessage="No group found"
|
nothingFoundMessage={t("No group found")}
|
||||||
maxValues={50}
|
maxValues={50}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace
|
|||||||
import { IUser } from "@/features/user/types/user.types.ts";
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
import { Group, MultiSelect, MultiSelectProps, Text } from "@mantine/core";
|
import { Group, MultiSelect, MultiSelectProps, Text } from "@mantine/core";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface MultiUserSelectProps {
|
interface MultiUserSelectProps {
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
@ -29,6 +30,7 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
|
export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||||
const { data: users, isLoading } = useWorkspaceMembersQuery({
|
const { data: users, isLoading } = useWorkspaceMembersQuery({
|
||||||
@ -65,15 +67,15 @@ export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
|
|||||||
renderOption={renderMultiSelectOption}
|
renderOption={renderMultiSelectOption}
|
||||||
hidePickedOptions
|
hidePickedOptions
|
||||||
maxDropdownHeight={300}
|
maxDropdownHeight={300}
|
||||||
label={label || "Add members"}
|
label={label || t("Add members")}
|
||||||
placeholder="Search for users"
|
placeholder={t("Search for users")}
|
||||||
searchable
|
searchable
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={setSearchValue}
|
onSearchChange={setSearchValue}
|
||||||
clearable
|
clearable
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
nothingFoundMessage="No user found"
|
nothingFoundMessage={t("No user found")}
|
||||||
maxValues={50}
|
maxValues={50}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,24 +29,22 @@ export function useGetGroupsQuery(
|
|||||||
|
|
||||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['groups', groupId],
|
queryKey: ['group', groupId],
|
||||||
queryFn: () => getGroupById(groupId),
|
queryFn: () => getGroupById(groupId),
|
||||||
enabled: !!groupId,
|
enabled: !!groupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupMembersQuery(groupId: string) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['groupMembers', groupId],
|
|
||||||
queryFn: () => getGroupMembers(groupId),
|
|
||||||
enabled: !!groupId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateGroupMutation() {
|
export function useCreateGroupMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<IGroup, Error, Partial<IGroup>>({
|
return useMutation<IGroup, Error, Partial<IGroup>>({
|
||||||
mutationFn: (data) => createGroup(data),
|
mutationFn: (data) => createGroup(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['groups'],
|
||||||
|
});
|
||||||
|
|
||||||
notifications.show({ message: 'Group created successfully' });
|
notifications.show({ message: 'Group created successfully' });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@ -96,6 +94,14 @@ export function useDeleteGroupMutation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGroupMembersQuery(groupId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['groupMembers', groupId],
|
||||||
|
queryFn: () => getGroupMembers(groupId),
|
||||||
|
enabled: !!groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useAddGroupMemberMutation() {
|
export function useAddGroupMemberMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import { Text, Tabs, Space } from "@mantine/core";
|
import { Text, Tabs, Space } from "@mantine/core";
|
||||||
import { IconClockHour3 } from "@tabler/icons-react";
|
import { IconClockHour3 } from "@tabler/icons-react";
|
||||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function HomeTabs() {
|
export default function HomeTabs() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="recent">
|
<Tabs defaultValue="recent">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
Recently updated
|
{t("Recently updated")}
|
||||||
</Text>
|
</Text>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|||||||
@ -16,12 +16,14 @@ import {
|
|||||||
} from "@/features/editor/atoms/editor-atoms";
|
} from "@/features/editor/atoms/editor-atoms";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryList({ pageId }: Props) {
|
function HistoryList({ pageId }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
||||||
const {
|
const {
|
||||||
data: pageHistoryList,
|
data: pageHistoryList,
|
||||||
@ -36,14 +38,15 @@ function HistoryList({ pageId }: Props) {
|
|||||||
|
|
||||||
const confirmModal = () =>
|
const confirmModal = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: "Please confirm your action",
|
title: t("Please confirm your action"),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Are you sure you want to restore this version? Any changes not
|
{t(
|
||||||
versioned will be lost.
|
"Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||||
onConfirm: handleRestore,
|
onConfirm: handleRestore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,7 +63,7 @@ function HistoryList({ pageId }: Props) {
|
|||||||
.setContent(activeHistoryData.content)
|
.setContent(activeHistoryData.content)
|
||||||
.run();
|
.run();
|
||||||
setHistoryModalOpen(false);
|
setHistoryModalOpen(false);
|
||||||
notifications.show({ message: "Successfully restored" });
|
notifications.show({ message: t("Successfully restored") });
|
||||||
}
|
}
|
||||||
}, [activeHistoryData]);
|
}, [activeHistoryData]);
|
||||||
|
|
||||||
@ -79,11 +82,11 @@ function HistoryList({ pageId }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <div>Error loading page history.</div>;
|
return <div>{t("Error loading page history.")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pageHistoryList || pageHistoryList.items.length === 0) {
|
if (!pageHistoryList || pageHistoryList.items.length === 0) {
|
||||||
return <>No page history saved yet.</>;
|
return <>{t("No page history saved yet.")}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -104,14 +107,14 @@ function HistoryList({ pageId }: Props) {
|
|||||||
|
|
||||||
<Group p="xs" wrap="nowrap">
|
<Group p="xs" wrap="nowrap">
|
||||||
<Button size="compact-md" onClick={confirmModal}>
|
<Button size="compact-md" onClick={confirmModal}>
|
||||||
Restore
|
{t("Restore")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="compact-md"
|
size="compact-md"
|
||||||
onClick={() => setHistoryModalOpen(false)}
|
onClick={() => setHistoryModalOpen(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
{t("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { Modal, Text } from "@mantine/core";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms";
|
import { historyAtoms } from "@/features/page-history/atoms/history-atoms";
|
||||||
import HistoryModalBody from "@/features/page-history/components/history-modal-body";
|
import HistoryModalBody from "@/features/page-history/components/history-modal-body";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
export default function HistoryModal({ pageId }: Props) {
|
export default function HistoryModal({ pageId }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isModalOpen, setModalOpen] = useAtom(historyAtoms);
|
const [isModalOpen, setModalOpen] = useAtom(historyAtoms);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -21,7 +23,7 @@ export default function HistoryModal({ pageId }: Props) {
|
|||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
<Modal.Title>
|
<Modal.Title>
|
||||||
<Text size="md" fw={500}>
|
<Text size="md" fw={500}>
|
||||||
Page history
|
{t("Page history")}
|
||||||
</Text>
|
</Text>
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
<Modal.CloseButton />
|
<Modal.CloseButton />
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { usePageHistoryQuery } from '@/features/page-history/queries/page-history-query';
|
import { usePageHistoryQuery } from "@/features/page-history/queries/page-history-query";
|
||||||
import { HistoryEditor } from '@/features/page-history/components/history-editor';
|
import { HistoryEditor } from "@/features/page-history/components/history-editor";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface HistoryProps {
|
interface HistoryProps {
|
||||||
historyId: string;
|
historyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryView({ historyId }: HistoryProps) {
|
function HistoryView({ historyId }: HistoryProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { data, isLoading, isError } = usePageHistoryQuery(historyId);
|
const { data, isLoading, isError } = usePageHistoryQuery(historyId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -13,13 +15,15 @@ function HistoryView({ historyId }: HistoryProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return <div>Error fetching page data.</div>;
|
return <div>{t("Error fetching page data.")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (data &&
|
return (
|
||||||
|
data && (
|
||||||
<div>
|
<div>
|
||||||
<HistoryEditor content={data.content} title={data.title} />
|
<HistoryEditor content={data.content} title={data.title} />
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--mantine-color-default-color);
|
color: var(--mantine-color-default-color);
|
||||||
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mantine-Breadcrumbs-breadcrumb {
|
.mantine-Breadcrumbs-breadcrumb {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
|
|||||||
import {
|
import {
|
||||||
IconArrowsHorizontal,
|
IconArrowsHorizontal,
|
||||||
IconDots,
|
IconDots,
|
||||||
IconDownload,
|
IconFileExport,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconMessage,
|
IconMessage,
|
||||||
@ -24,6 +24,8 @@ import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
|||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ExportModal from "@/components/common/export-modal";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -52,6 +54,7 @@ interface PageActionMenuProps {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
const { pageSlug, spaceSlug } = useParams();
|
const { pageSlug, spaceSlug } = useParams();
|
||||||
@ -68,7 +71,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title);
|
getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title);
|
||||||
|
|
||||||
clipboard.copy(pageUrl);
|
clipboard.copy(pageUrl);
|
||||||
notifications.show({ message: "Link copied" });
|
notifications.show({ message: t("Link copied") });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrint = () => {
|
const handlePrint = () => {
|
||||||
@ -106,13 +109,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
leftSection={<IconLink size={16} />}
|
leftSection={<IconLink size={16} />}
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
>
|
>
|
||||||
Copy link
|
{t("Copy link")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
|
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
<PageWidthToggle label="Full width" />
|
<PageWidthToggle label={t("Full width")} />
|
||||||
</Group>
|
</Group>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
@ -120,23 +123,23 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
leftSection={<IconHistory size={16} />}
|
leftSection={<IconHistory size={16} />}
|
||||||
onClick={openHistoryModal}
|
onClick={openHistoryModal}
|
||||||
>
|
>
|
||||||
Page history
|
{t("Page history")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconDownload size={16} />}
|
leftSection={<IconFileExport size={16} />}
|
||||||
onClick={openExportModal}
|
onClick={openExportModal}
|
||||||
>
|
>
|
||||||
Export
|
{t("Export")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconPrinter size={16} />}
|
leftSection={<IconPrinter size={16} />}
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
>
|
>
|
||||||
Print PDF
|
{t("Print PDF")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
@ -147,15 +150,16 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
leftSection={<IconTrash size={16} />}
|
leftSection={<IconTrash size={16} />}
|
||||||
onClick={handleDeletePage}
|
onClick={handleDeletePage}
|
||||||
>
|
>
|
||||||
Delete
|
{t("Delete")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<PageExportModal
|
<ExportModal
|
||||||
pageId={page.id}
|
type="page"
|
||||||
|
id={page.id}
|
||||||
open={exportOpened}
|
open={exportOpened}
|
||||||
onClose={closeExportModal}
|
onClose={closeExportModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Modal, Button, Group, Text, Select } from "@mantine/core";
|
import { Modal, Button, Group, Text, Select, Switch } from "@mantine/core";
|
||||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface PageExportModalProps {
|
interface PageExportModalProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -16,6 +17,7 @@ export default function PageExportModal({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: PageExportModalProps) {
|
}: PageExportModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
@ -24,7 +26,7 @@ export default function PageExportModal({
|
|||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: "Export failed:" + err.response?.data.message,
|
message: t("Export failed:") + err.response?.data.message,
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
console.error("export error", err);
|
console.error("export error", err);
|
||||||
@ -48,22 +50,29 @@ export default function PageExportModal({
|
|||||||
<Modal.Overlay />
|
<Modal.Overlay />
|
||||||
<Modal.Content style={{ overflow: "hidden" }}>
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
<Modal.Header py={0}>
|
<Modal.Header py={0}>
|
||||||
<Modal.Title fw={500}>Export page</Modal.Title>
|
<Modal.Title fw={500}>{t("Export page")}</Modal.Title>
|
||||||
<Modal.CloseButton />
|
<Modal.CloseButton />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<div>
|
<div>
|
||||||
<Text size="md">Format</Text>
|
<Text size="md">{t("Format")}</Text>
|
||||||
</div>
|
</div>
|
||||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" pt="md">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Include subpages")}</Text>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group justify="center" mt="md">
|
<Group justify="center" mt="md">
|
||||||
<Button onClick={onClose} variant="default">
|
<Button onClick={onClose} variant="default">
|
||||||
Cancel
|
{t("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleExport}>Export</Button>
|
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
@ -76,6 +85,8 @@ interface ExportFormatSelection {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data={[
|
data={[
|
||||||
@ -88,7 +99,7 @@ function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
|||||||
comboboxProps={{ width: "120" }}
|
comboboxProps={{ width: "120" }}
|
||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
withCheckIcon={false}
|
withCheckIcon={false}
|
||||||
aria-label="Select export format"
|
aria-label={t("Select export format")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user