Compare commits

..

34 Commits

Author SHA1 Message Date
Philipinho 7778482377 upgrade to latest slufigy and nanoid versions 2025-04-19 19:31:15 +01:00
Philipinho 674769df02 use non-esm nanoid version 2025-04-19 15:54:11 +01:00
Philipinho de57d05199 0.10.2 2025-04-15 12:48:40 +01:00
Philipinho 89ec990232 sync ee 2025-04-15 12:46:28 +01:00
Philipinho 49d0f1cc9a Add click handler 2025-04-11 13:41:43 +01:00
Philipinho 268001ae26 v0.10.1 2025-04-11 13:23:42 +01:00
Philip Okugbe 27fa45a769 fix local attachment paths in exports (#1013) 2025-04-11 13:18:44 +01:00
Philipinho f9711918a3 fix comment editor padding 2025-04-11 12:32:54 +01:00
Philipinho 29bb52db0c v0.10.0 2025-04-09 19:14:51 +01:00
Philipinho f2241db5ee remove beta message 2025-04-09 19:14:33 +01:00
Philipinho 58d1855a36 fix hash check 2025-04-09 19:03:27 +01:00
Philipinho 7fe3c5f177 * time ago hook 2025-04-09 18:47:39 +01:00
Philipinho 5fd477d074 collapse by default in node-edit mode 2025-04-09 15:46:29 +01:00
Philip Okugbe 4aa5d7e326 hide history action menu for can-view role (#1001) 2025-04-09 15:42:29 +01:00
Philipinho 7f7f2bccd0 fix toggle node in non-edit mode 2025-04-09 15:37:18 +01:00
Philip Okugbe a9f370660b New Crowdin updates (#1005)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-04-08 17:28:33 +01:00
Philipinho 117c7049ff fix 2025-04-08 17:15:09 +01:00
Philipinho cd10365f71 new translations 2025-04-08 17:10:48 +01:00
Philip Okugbe ee30d9d0f2 New Crowdin updates (#1003)
* New translations translation.json (French)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-04-08 17:10:08 +01:00
Philipinho 276ececbf2 cleanup 2025-04-08 17:06:32 +01:00
Philipinho fa194a497c cleanup 2025-04-08 17:04:43 +01:00
Philip Okugbe 1eaba6e77f fix: bug fixes (#1000)
* sort by groups first

* add scroll area

* fix group members pagination

* move pagination to the right
2025-04-08 13:34:00 +01:00
Philipinho 651e5f6153 null check 2025-04-08 11:59:47 +01:00
Philip Okugbe 7431804a46 feat: delete workspace member (#987)
* add delete user endpoint (server)

* delete user (UI)

* prevent token generation

* more checks
2025-04-07 19:26:03 +01:00
Philipinho 3559358d14 fix pagination issue where user is not part of any space 2025-04-07 19:09:02 +01:00
Philipinho 06270ff747 - fixes
- allow mail from address override
- queue cloud emails
2025-04-07 19:07:10 +01:00
sanua356 233536314f feat: add Table of contents (#981)
* chore: add table of contents module

* refactor

* lint

* null check

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-05 19:03:42 +01:00
Philip Okugbe 17ce3bab8a feat: move page between spaces (#988)
* feat: Move the page to another space

- The ability to move a page to another space has been added

* feat: Move the page to another space
* feat: Move the page to another space

- Correction of the visibility attribute of elements that extend beyond the boundaries of the space selection modal window

* feat: Move the page to another space

- Added removal of query keys when moving pages

* feat: Move the page to another space

- Fix locales

* feat: Move the page to another space
* feat: Move the page to another space

- Fix docker compose

* feat: Move the page to another space

* feat: Move the page to another space

- Some refactor

* feat: Move the page to another space

- Attachments update

* feat: Move the page to another space

- The function of searching for attachments by page ID and updating attachments has been combined

* feat: Move the page to another space

- Fix variable name

* feat: Move the page to another space

- Move current space to parameter of component SpaceSelectionModal

* refactor ui

---------

Co-authored-by: plekhanov <astecom@mail.ru>
2025-04-04 23:44:18 +01:00
Philip Okugbe b27d1708b0 queue trial ended job (#992) 2025-04-04 23:35:08 +01:00
Philip Okugbe 64f0531093 feat: keep track of page contributors (#959)
* WIP

* feat: store and retrieve page contributors
2025-04-04 13:03:57 +01:00
fuscodev 8aa604637e feat: nested toggle block (#671)
* feat: nested toggle block

* fix: md export

* fix detailsButton icon alignment

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-04 13:01:39 +01:00
Philipinho 7ca2b437d4 sync 2025-04-03 14:08:06 +01:00
Philip Okugbe 595bd1dc81 Fix editor connection loop (#986)
* fix editor connection loop

* remove query refresh
2025-04-03 14:05:34 +01:00
Philipinho a74d3feae4 fix: make collab ready reliable on tab return 2025-03-27 14:39:43 +00:00
79 changed files with 1482 additions and 653 deletions
-3
View File
@@ -9,9 +9,6 @@
</div> </div>
<br /> <br />
> [!NOTE]
> Docmost is currently in **beta**. We value your feedback as we progress towards a stable release.
## Getting started ## Getting started
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs). To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.9.0", "version": "0.10.2",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -34,6 +34,7 @@
"jotai": "^2.12.1", "jotai": "^2.12.1",
"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",
"katex": "0.16.21", "katex": "0.16.21",
"lowlight": "^3.2.0", "lowlight": "^3.2.0",
"mermaid": "^11.4.1", "mermaid": "^11.4.1",
@@ -351,5 +351,16 @@
"Created at: {{time}}": "Erstellt am: {{time}}", "Created at: {{time}}": "Erstellt am: {{time}}",
"Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}", "Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}",
"Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}", "Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}",
"Character count: {{characterCount}}": "Zeichenzahl: {{characterCount}}" "Character count: {{characterCount}}": "Zeichenzahl: {{characterCount}}",
"New update": "Neues Update",
"{{latestVersion}} is available": "{{latestVersion}} ist verfügbar",
"Delete member": "Mitglied löschen",
"Member deleted successfully": "Mitglied erfolgreich gelöscht",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Sind Sie sicher, dass Sie dieses Arbeitsbereichsmitglied löschen möchten? Diese Aktion ist unwiderruflich.",
"Move": "Verschieben",
"Move page": "Seite verschieben",
"Move page to a different space.": "Seite in einen anderen Bereich verschieben.",
"Real-time editor connection lost. Retrying...": "Echtzeit-Editor-Verbindung verloren. Wiederholen...",
"Table of contents": "Inhaltsverzeichnis",
"Add headings (H1, H2, H3) to generate a table of contents.": "Fügen Sie Überschriften (H1, H2, H3) hinzu, um ein Inhaltsverzeichnis zu erstellen."
} }
@@ -353,5 +353,14 @@
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "Word count: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}", "Character count: {{characterCount}}": "Character count: {{characterCount}}",
"New update": "New update", "New update": "New update",
"{{latestVersion}} is available": "{{latestVersion}} is available" "{{latestVersion}} is available": "{{latestVersion}} is available",
"Delete member": "Delete member",
"Member deleted successfully": "Member deleted successfully",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Are you sure you want to delete this workspace member? This action is irreversible.",
"Move": "Move",
"Move page": "Move page",
"Move page to a different space.": "Move page to a different space.",
"Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...",
"Table of contents": "Table of contents",
"Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents."
} }
@@ -351,5 +351,16 @@
"Created at: {{time}}": "Creado a: {{time}}", "Created at: {{time}}": "Creado a: {{time}}",
"Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}", "Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}",
"Word count: {{wordCount}}": "Conteo de palabras: {{wordCount}}", "Word count: {{wordCount}}": "Conteo de palabras: {{wordCount}}",
"Character count: {{characterCount}}": "Recuento de caracteres: {{characterCount}}" "Character count: {{characterCount}}": "Recuento de caracteres: {{characterCount}}",
"New update": "Nueva actualización",
"{{latestVersion}} is available": "{{latestVersion}} está disponible",
"Delete member": "Eliminar miembro",
"Member deleted successfully": "Miembro eliminado con éxito",
"Are you sure you want to delete this workspace member? This action is irreversible.": "¿Está seguro que desea eliminar este miembro del área de trabajo? Esta acción es irreversible.",
"Move": "Mover",
"Move page": "Mover página",
"Move page to a different space.": "Mover página a un espacio diferente.",
"Real-time editor connection lost. Retrying...": "Conexión del editor en tiempo real perdida. Reintentando...",
"Table of contents": "Índice de contenidos",
"Add headings (H1, H2, H3) to generate a table of contents.": "Añadir encabezados (H1, H2, H3) para generar un índice de contenidos."
} }
@@ -21,7 +21,7 @@
"Can view": "Peut voir", "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.", "Can view pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
"Cancel": "Annuler", "Cancel": "Annuler",
"Change email": "Changer l'email", "Change email": "Changer le courriel",
"Change password": "Changer le mot de passe", "Change password": "Changer le mot de passe",
"Change photo": "Changer la photo", "Change photo": "Changer la photo",
"Choose a role": "Choisir un rôle", "Choose a role": "Choisir un rôle",
@@ -351,5 +351,16 @@
"Created at: {{time}}": "Créé à : {{time}}", "Created at: {{time}}": "Créé à : {{time}}",
"Edited by {{name}} {{time}}": "Modifié par {{name}} {{time}}", "Edited by {{name}} {{time}}": "Modifié par {{name}} {{time}}",
"Word count: {{wordCount}}": "Nombre de mots : {{wordCount}}", "Word count: {{wordCount}}": "Nombre de mots : {{wordCount}}",
"Character count: {{characterCount}}": "Nombre de caractères : {{characterCount}}" "Character count: {{characterCount}}": "Nombre de caractères : {{characterCount}}",
"New update": "Nouvelle mise à jour",
"{{latestVersion}} is available": "{{latestVersion}} est disponible",
"Delete member": "Supprimer le membre",
"Member deleted successfully": "Membre supprimé avec succès",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Êtes-vous sûr de vouloir supprimer ce membre de l'espace de travail? Cette action est irréversible.",
"Move": "Déplacer",
"Move page": "Déplacer la page",
"Move page to a different space.": "Déplacer la page vers un autre espace.",
"Real-time editor connection lost. Retrying...": "Connexion avec l'éditeur en temps réel perdue. Nouvelle tentative...",
"Table of contents": "",
"Add headings (H1, H2, H3) to generate a table of contents.": "Ajoutez des titres (H1, H2, H3) pour générer une table des matières."
} }
@@ -347,9 +347,20 @@
"Members added successfully": "Membri aggiunti con successo", "Members added successfully": "Membri aggiunti con successo",
"Member removed successfully": "Membro rimosso con successo", "Member removed successfully": "Membro rimosso con successo",
"Member role updated successfully": "Ruolo del membro aggiornato con successo", "Member role updated successfully": "Ruolo del membro aggiornato con successo",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "Creato da: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "Creato il: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "Modificato da {{name}} il {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "Conteggio parole: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "Conteggio caratteri: {{characterCount}}",
"New update": "Nuovo aggiornamento",
"{{latestVersion}} is available": "{{latestVersion}} è disponibile",
"Delete member": "Elimina membro",
"Member deleted successfully": "Membro eliminato con successo",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Sei sicuro di voler eliminare questo membro del workspace? Questa azione è irreversibile.",
"Move": "Sposta",
"Move page": "Sposta pagina",
"Move page to a different space.": "Sposta la pagina in un altro spazio.",
"Real-time editor connection lost. Retrying...": "Connessione all'editor in tempo reale persa. Riprovo...",
"Table of contents": "Indice dei contenuti",
"Add headings (H1, H2, H3) to generate a table of contents.": "Aggiungi intestazioni (H1, H2, H3) per generare un sommario."
} }
@@ -347,9 +347,20 @@
"Members added successfully": "メンバーを追加しました", "Members added successfully": "メンバーを追加しました",
"Member removed successfully": "メンバーが削除されました", "Member removed successfully": "メンバーが削除されました",
"Member role updated successfully": "メンバーのロールを更新しました", "Member role updated successfully": "メンバーのロールを更新しました",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "作成者: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "が作成しました:{{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "最終編集: {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "ワード数: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "文字数: {{characterCount}}",
"New update": "新規更新",
"{{latestVersion}} is available": "{{latestVersion}}は利用可能です",
"Delete member": "メンバーを削除する",
"Member deleted successfully": "メンバーが削除されました",
"Are you sure you want to delete this workspace member? This action is irreversible.": "ワークスペースメンバーを削除してもよろしいですか?この操作は元に戻せません。",
"Move": "移動",
"Move page": "ページを移動",
"Move page to a different space.": "ページを別のスペースに移動します。",
"Real-time editor connection lost. Retrying...": "リアルタイムエディターの接続が失われました。再試行しています…",
"Table of contents": "目次",
"Add headings (H1, H2, H3) to generate a table of contents.": "見出し(H1、H2、H3)を追加して目次を生成します。"
} }
@@ -148,7 +148,7 @@
"Select role to assign to all invited members": "초대된 모든 사용자에게 할당할 역할 선택", "Select role to assign to all invited members": "초대된 모든 사용자에게 할당할 역할 선택",
"Select theme": "배경 선택", "Select theme": "배경 선택",
"Send invitation": "초대 보내기", "Send invitation": "초대 보내기",
"Invitation sent": "Invitation sent", "Invitation sent": "초대 발송 완료",
"Settings": "설정", "Settings": "설정",
"Setup workspace": "Workspace 설정", "Setup workspace": "Workspace 설정",
"Sign In": "로그인", "Sign In": "로그인",
@@ -245,7 +245,7 @@
"Align left": "왼쪽 정렬", "Align left": "왼쪽 정렬",
"Align right": "오른쪽 정렬", "Align right": "오른쪽 정렬",
"Align center": "가운데 정렬", "Align center": "가운데 정렬",
"Justify": "Justify", "Justify": "정렬",
"Merge cells": "셀 병합", "Merge cells": "셀 병합",
"Split cell": "셀 분할", "Split cell": "셀 분할",
"Delete column": "열 삭제", "Delete column": "열 삭제",
@@ -341,15 +341,26 @@
"Names do not match": "이름이 일치하지 않습니다", "Names do not match": "이름이 일치하지 않습니다",
"Today, {{time}}": "오늘, {{time}}", "Today, {{time}}": "오늘, {{time}}",
"Yesterday, {{time}}": "어제, {{time}}", "Yesterday, {{time}}": "어제, {{time}}",
"Space created successfully": "Space created successfully", "Space created successfully": "공간 생성 완료",
"Space updated successfully": "Space updated successfully", "Space updated successfully": "공간이 성공적으로 업데이트되었습니다",
"Space deleted successfully": "Space deleted successfully", "Space deleted successfully": "스페이스 삭제 완료",
"Members added successfully": "Members added successfully", "Members added successfully": "회원 추가 완료",
"Member removed successfully": "Member removed successfully", "Member removed successfully": "멤버가 성공적으로 제거되었습니다",
"Member role updated successfully": "Member role updated successfully", "Member role updated successfully": "회원 역할이 성공적으로 업데이트되었습니다",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "작성자: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "생성 날짜: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "{{name}}님이 편집함 {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "단어 수: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "문자 수: {{characterCount}}",
"New update": "새로운 업데이트",
"{{latestVersion}} is available": "{{latestVersion}}이 사용 가능합니다",
"Delete member": "회원 삭제",
"Member deleted successfully": "멤버가 성공적으로 제거되었습니다",
"Are you sure you want to delete this workspace member? This action is irreversible.": "이 워크스페이스 멤버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"Move": "이동",
"Move page": "페이지 이동",
"Move page to a different space.": "페이지를 다른 공간으로 이동합니다.",
"Real-time editor connection lost. Retrying...": "실시간 편집기 연결이 끊어졌습니다. 재시도 중...",
"Table of contents": "목차",
"Add headings (H1, H2, H3) to generate a table of contents.": "목차를 생성하려면 제목 (H1, H2, H3)을 추가하세요."
} }
@@ -351,5 +351,16 @@
"Created at: {{time}}": "Aangemaakt op: {{time}}", "Created at: {{time}}": "Aangemaakt op: {{time}}",
"Edited by {{name}} {{time}}": "Bewerkt door {{name}} {{time}}", "Edited by {{name}} {{time}}": "Bewerkt door {{name}} {{time}}",
"Word count: {{wordCount}}": "Aantal woorden: {{wordCount}}", "Word count: {{wordCount}}": "Aantal woorden: {{wordCount}}",
"Character count: {{characterCount}}": "Aantal tekens: {{characterCount}}" "Character count: {{characterCount}}": "Aantal tekens: {{characterCount}}",
"New update": "Nieuwe update",
"{{latestVersion}} is available": "{{latestVersion}} is beschikbaar",
"Delete member": "Verwijder lid",
"Member deleted successfully": "Lid succesvol verwijderd",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Weet u zeker dat u dit lid van de werkruimte wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden.",
"Move": "Verplaatsen",
"Move page": "Pagina verplaatsen",
"Move page to a different space.": "Verplaats pagina naar een andere ruimte.",
"Real-time editor connection lost. Retrying...": "Realtime editorverbinding verloren. Opnieuw proberen...",
"Table of contents": "Inhoudsopgave",
"Add headings (H1, H2, H3) to generate a table of contents.": "Voeg koppen (H1, H2, H3) toe om een inhoudsopgave te genereren."
} }
@@ -148,7 +148,7 @@
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados", "Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
"Select theme": "Selecionar tema", "Select theme": "Selecionar tema",
"Send invitation": "Enviar convite", "Send invitation": "Enviar convite",
"Invitation sent": "Invitation sent", "Invitation sent": "Convite enviado",
"Settings": "Configurações", "Settings": "Configurações",
"Setup workspace": "Configurar workspace", "Setup workspace": "Configurar workspace",
"Sign In": "Entrar", "Sign In": "Entrar",
@@ -245,7 +245,7 @@
"Align left": "Alinhar à esquerda", "Align left": "Alinhar à esquerda",
"Align right": "Alinhar à direita", "Align right": "Alinhar à direita",
"Align center": "Alinhar ao centro", "Align center": "Alinhar ao centro",
"Justify": "Justify", "Justify": "Justificar",
"Merge cells": "Mesclar células", "Merge cells": "Mesclar células",
"Split cell": "Dividir célula", "Split cell": "Dividir célula",
"Delete column": "Excluir coluna", "Delete column": "Excluir coluna",
@@ -341,15 +341,26 @@
"Names do not match": "Os nomes não coincidem", "Names do not match": "Os nomes não coincidem",
"Today, {{time}}": "Hoje, {{time}}", "Today, {{time}}": "Hoje, {{time}}",
"Yesterday, {{time}}": "Ontem, {{time}}", "Yesterday, {{time}}": "Ontem, {{time}}",
"Space created successfully": "Space created successfully", "Space created successfully": "Espaço criado com sucesso",
"Space updated successfully": "Space updated successfully", "Space updated successfully": "Espaço atualizado com sucesso",
"Space deleted successfully": "Space deleted successfully", "Space deleted successfully": "Espaço excluído com sucesso",
"Members added successfully": "Members added successfully", "Members added successfully": "Membros adicionados com sucesso",
"Member removed successfully": "Member removed successfully", "Member removed successfully": "Membro removido com sucesso",
"Member role updated successfully": "Member role updated successfully", "Member role updated successfully": "Função do membro atualizada com sucesso",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "Criado por: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "Criado em: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "Contagem de palavras: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "Contagem de caracteres: {{characterCount}}",
"New update": "Nova atualização",
"{{latestVersion}} is available": "{{latestVersion}} está disponível",
"Delete member": "Excluir membro",
"Member deleted successfully": "Membro removido com sucesso",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Você tem certeza que deseja deletar este membro do workspace? Esta ação é irreversível.",
"Move": "Mover",
"Move page": "Mover página",
"Move page to a different space.": "Mover página para um espaço diferente.",
"Real-time editor connection lost. Retrying...": "Conexão do editor em tempo real perdida. Tentando novamente...",
"Table of contents": "Tabela de conteúdos",
"Add headings (H1, H2, H3) to generate a table of contents.": "Adicionar títulos (H1, H2, H3) para gerar uma tabela de conteúdo."
} }
@@ -148,7 +148,7 @@
"Select role to assign to all invited members": "Выберите роль для всех приглашённых участников", "Select role to assign to all invited members": "Выберите роль для всех приглашённых участников",
"Select theme": "Выберите тему", "Select theme": "Выберите тему",
"Send invitation": "Отправить приглашение", "Send invitation": "Отправить приглашение",
"Invitation sent": "Invitation sent", "Invitation sent": "Приглашение отправлено",
"Settings": "Настройки", "Settings": "Настройки",
"Setup workspace": "Настроить рабочее пространство", "Setup workspace": "Настроить рабочее пространство",
"Sign In": "Вход", "Sign In": "Вход",
@@ -245,7 +245,7 @@
"Align left": "По левому краю", "Align left": "По левому краю",
"Align right": "По правому краю", "Align right": "По правому краю",
"Align center": "По центру", "Align center": "По центру",
"Justify": "Justify", "Justify": "По ширине",
"Merge cells": "Объединить ячейки", "Merge cells": "Объединить ячейки",
"Split cell": "Разделить ячейку", "Split cell": "Разделить ячейку",
"Delete column": "Удалить столбец", "Delete column": "Удалить столбец",
@@ -331,25 +331,36 @@
"Insert math equation": "Вставить математическое выражение", "Insert math equation": "Вставить математическое выражение",
"Mermaid diagram": "Диаграмма Mermaid", "Mermaid diagram": "Диаграмма Mermaid",
"Insert mermaid diagram": "Вставить диаграмму Mermaid", "Insert mermaid diagram": "Вставить диаграмму Mermaid",
"Insert and design Drawio diagrams": "Вставьте и редактируйте диаграммы Draw.io", "Insert and design Drawio diagrams": "Вставить и рисовать диаграммы Draw.io",
"Insert current date": "Вставить текущую дату", "Insert current date": "Вставить текущую дату",
"Draw and sketch excalidraw diagrams": "Создайте и рисуйте диаграммы Excalidraw", "Draw and sketch excalidraw diagrams": "Вставить и рисовать диаграммы Excalidraw",
"Multiple": "Несколько", "Multiple": "Несколько",
"Heading {{level}}": "Заголовок {{level}}", "Heading {{level}}": "Заголовок {{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}}": "Сегодня, {{time}}", "Today, {{time}}": "Сегодня, {{time}}",
"Yesterday, {{time}}": "Вчера, {{time}}", "Yesterday, {{time}}": "Вчера, {{time}}",
"Space created successfully": "Space created successfully", "Space created successfully": "Пространство успешно создано",
"Space updated successfully": "Space updated successfully", "Space updated successfully": "Пространство успешно обновлено",
"Space deleted successfully": "Space deleted successfully", "Space deleted successfully": "Пространство успешно удалено",
"Members added successfully": "Members added successfully", "Members added successfully": "Участники успешно добавлены",
"Member removed successfully": "Member removed successfully", "Member removed successfully": "Участник успешно удален",
"Member role updated successfully": "Member role updated successfully", "Member role updated successfully": "Роль участника успешно обновлена",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "Автор: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "Дата создания: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "Изменено {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "Количество слов: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "Количество символов: {{characterCount}}",
"New update": "Новое обновление",
"{{latestVersion}} is available": "Доступна новая версия {{latestVersion}}",
"Delete member": "Удалить участника",
"Member deleted successfully": "Участник успешно удален",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Вы уверены, что хотите удалить этого участника рабочей области? Это действие необратимо.",
"Move": "Переместить",
"Move page": "Переместить страницу",
"Move page to a different space.": "Переместите страницу в другое пространство.",
"Real-time editor connection lost. Retrying...": "Соединение с редактором в реальном времени потеряно. Повторная попытка...",
"Table of contents": "Содержание",
"Add headings (H1, H2, H3) to generate a table of contents.": "Добавьте заголовки (H1, H2, H3), чтобы создать оглавление."
} }
@@ -148,7 +148,7 @@
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色", "Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
"Select theme": "选择主题", "Select theme": "选择主题",
"Send invitation": "发送邀请", "Send invitation": "发送邀请",
"Invitation sent": "Invitation sent", "Invitation sent": "邀请邮件已发送",
"Settings": "设置", "Settings": "设置",
"Setup workspace": "设置工作空间", "Setup workspace": "设置工作空间",
"Sign In": "登录", "Sign In": "登录",
@@ -245,7 +245,7 @@
"Align left": "靠左对齐", "Align left": "靠左对齐",
"Align right": "靠右对齐", "Align right": "靠右对齐",
"Align center": "居中对齐", "Align center": "居中对齐",
"Justify": "Justify", "Justify": "两端对齐",
"Merge cells": "合并单元格", "Merge cells": "合并单元格",
"Split cell": "分割单元格", "Split cell": "分割单元格",
"Delete column": "删除整列", "Delete column": "删除整列",
@@ -341,15 +341,26 @@
"Names do not match": "名称不匹配", "Names do not match": "名称不匹配",
"Today, {{time}}": "今天,{{time}}", "Today, {{time}}": "今天,{{time}}",
"Yesterday, {{time}}": "昨天,{{time}}", "Yesterday, {{time}}": "昨天,{{time}}",
"Space created successfully": "Space created successfully", "Space created successfully": "空间创建成功",
"Space updated successfully": "Space updated successfully", "Space updated successfully": "空间更新成功",
"Space deleted successfully": "Space deleted successfully", "Space deleted successfully": "空间已成功删除",
"Members added successfully": "Members added successfully", "Members added successfully": "成员添加成功",
"Member removed successfully": "Member removed successfully", "Member removed successfully": "成员移除成功",
"Member role updated successfully": "Member role updated successfully", "Member role updated successfully": "成员角色更新成功",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "创建者:<b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}", "Created at: {{time}}": "创建于:{{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}", "Edited by {{name}} {{time}}": "{{name}} 编辑于 {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}", "Word count: {{wordCount}}": "字数:{{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}" "Character count: {{characterCount}}": "字符数:{{characterCount}}",
"New update": "新更新",
"{{latestVersion}} is available": "{{latestVersion}} 已经可以使用",
"Delete member": "删除成员",
"Member deleted successfully": "成员删除成功",
"Are you sure you want to delete this workspace member? This action is irreversible.": "您确定要删除此工作区成员吗?此操作不可逆。",
"Move": "移动",
"Move page": "移动页面",
"Move page to a different space.": "将页面移动到不同的空间。",
"Real-time editor connection lost. Retrying...": "实时编辑器连接丢失。重试中……",
"Table of contents": "目录",
"Add headings (H1, H2, H3) to generate a table of contents.": "添加标题(H1H2H3)以生成目录。"
} }
@@ -65,11 +65,12 @@ export default function ExportModal({
yOffset="10vh" yOffset="10vh"
xOffset={0} xOffset={0}
mah={400} mah={400}
onClick={(e) => e.stopPropagation()}
> >
<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 {type}</Modal.Title> <Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title>
<Modal.CloseButton /> <Modal.CloseButton />
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
@@ -21,7 +21,7 @@ export default function Paginate({
} }
return ( return (
<Group mt="md"> <Group mt="md" justify="flex-end">
<Button <Button
variant="default" variant="default"
size="compact-sm" size="compact-sm"
@@ -4,10 +4,14 @@ 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"; import { useTranslation } from "react-i18next";
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
import { useAtomValue } from "jotai";
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
export default function Aside() { export default function Aside() {
const [{ tab }] = useAtom(asideStateAtom); const [{ tab }] = useAtom(asideStateAtom);
const { t } = useTranslation(); const { t } = useTranslation();
const pageEditor = useAtomValue(pageEditorAtom);
let title: string; let title: string;
let component: ReactNode; let component: ReactNode;
@@ -17,6 +21,10 @@ export default function Aside() {
component = <CommentList />; component = <CommentList />;
title = "Comments"; title = "Comments";
break; break;
case "toc":
component = <TableOfContents editor={pageEditor} />;
title = "Table of contents";
break;
default: default:
component = null; component = null;
title = null; title = null;
@@ -20,4 +20,4 @@ export const asideStateAtom = atom<AsideStateType>({
isAsideOpen: false, isAsideOpen: false,
}); });
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300); export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
@@ -35,6 +35,12 @@ export default function AppVersion() {
position="middle-end" position="middle-end"
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
disabled={!hasUpdate} disabled={!hasUpdate}
onClick={() => {
window.open(
"https://github.com/docmost/docmost/releases",
"_blank",
);
}}
> >
<Text <Text
size="sm" size="sm"
@@ -19,8 +19,8 @@ export function useCollabToken(): UseQueryResult<ICollabToken, Error> {
queryKey: ["collab-token"], queryKey: ["collab-token"],
queryFn: () => getCollabToken(), queryFn: () => getCollabToken(),
staleTime: 20 * 60 * 60 * 1000, //20hrs staleTime: 20 * 60 * 60 * 1000, //20hrs
refetchInterval: 12 * 60 * 60 * 1000, // 12hrs //refetchInterval: 12 * 60 * 60 * 1000, // 12hrs
refetchIntervalInBackground: true, //refetchIntervalInBackground: true,
refetchOnMount: true, refetchOnMount: true,
//@ts-ignore //@ts-ignore
retry: (failureCount, error) => { retry: (failureCount, error) => {
@@ -19,8 +19,7 @@
box-shadow: 0 0 0 2px var(--mantine-color-blue-3); box-shadow: 0 0 0 2px var(--mantine-color-blue-3);
} }
.ProseMirror { .ProseMirror :global(.ProseMirror){
width: 100%;
max-width: 100%; max-width: 100%;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@@ -29,7 +28,6 @@
padding-right: 6px; padding-right: 6px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 2px; margin-bottom: 2px;
font-size: 14px;
overflow: hidden auto; overflow: hidden auto;
} }
@@ -247,7 +247,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
searchTerms: ["collapsible", "block", "toggle", "details", "expand"], searchTerms: ["collapsible", "block", "toggle", "details", "expand"],
icon: IconCaretRightFilled, icon: IconCaretRightFilled,
command: ({ editor, range }: CommandProps) => command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleDetails().run(), editor.chain().focus().deleteRange(range).setDetails().run(),
}, },
{ {
title: "Callout", title: "Callout",
@@ -0,0 +1,54 @@
.headerPadding {
display: none;
top: calc(
var(--app-shell-header-offset, 0rem) + var(--app-shell-header-height, 0rem)
);
}
.link {
outline: none;
cursor: pointer;
display: block;
width: 100%;
text-align: start;
word-wrap: break-word;
background-color: transparent;
color: var(--mantine-color-text);
font-size: var(--mantine-font-size-sm);
line-height: var(--mantine-line-height-sm);
padding: 6px;
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
border: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-2),
var(--mantine-color-dark-6)
);
}
@media (max-width: $mantine-breakpoint-sm) {
& {
border: none !important;
padding-left: 0px;
}
}
}
.linkActive {
font-weight: 500;
border-left-color: light-dark(
var(--mantine-color-grey-5),
var(--mantine-color-grey-3)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
&,
&:hover {
background-color: light-dark(
var(--mantine-color-gray-3),
var(--mantine-color-dark-5)
) !important;
}
}
@@ -0,0 +1,165 @@
import { NodePos, useEditor } from "@tiptap/react";
import { TextSelection } from "@tiptap/pm/state";
import React, { FC, useEffect, useRef, useState } from "react";
import classes from "./table-of-contents.module.css";
import clsx from "clsx";
import { Box, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
type TableOfContentsProps = {
editor: ReturnType<typeof useEditor>;
};
export type HeadingLink = {
label: string;
level: number;
element: HTMLElement;
position: number;
};
const recalculateLinks = (nodePos: NodePos[]) => {
const nodes: HTMLElement[] = [];
const links: HeadingLink[] = Array.from(nodePos).reduce<HeadingLink[]>(
(acc, item) => {
const label = item.node.textContent;
const level = Number(item.node.attrs.level);
if (label.length && level <= 3) {
acc.push({
label,
level,
element: item.element,
//@ts-ignore
position: item.resolvedPos.pos,
});
nodes.push(item.element);
}
return acc;
},
[],
);
return { links, nodes };
};
export const TableOfContents: FC<TableOfContentsProps> = (props) => {
const { t } = useTranslation();
const [links, setLinks] = useState<HeadingLink[]>([]);
const [headingDOMNodes, setHeadingDOMNodes] = useState<HTMLElement[]>([]);
const [activeElement, setActiveElement] = useState<HTMLElement | null>(null);
const headerPaddingRef = useRef<HTMLDivElement | null>(null);
const handleScrollToHeading = (position: number) => {
const { view } = props.editor;
const headerOffset = parseInt(
window.getComputedStyle(headerPaddingRef.current).getPropertyValue("top"),
);
const { node } = view.domAtPos(position);
const element = node as HTMLElement;
const scrollPosition =
element.getBoundingClientRect().top + window.scrollY - headerOffset;
window.scrollTo({
top: scrollPosition,
behavior: "smooth",
});
const tr = view.state.tr;
tr.setSelection(new TextSelection(tr.doc.resolve(position)));
view.dispatch(tr);
view.focus();
};
const handleUpdate = () => {
const result = recalculateLinks(props.editor?.$nodes("heading"));
setLinks(result.links);
setHeadingDOMNodes(result.nodes);
};
useEffect(() => {
props.editor?.on("update", handleUpdate);
return () => {
props.editor?.off("update", handleUpdate);
};
}, [props.editor]);
useEffect(() => {
handleUpdate();
}, []);
useEffect(() => {
try {
const observeHandler = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveElement(entry.target as HTMLElement);
}
});
};
let headerOffset = 0;
if (headerPaddingRef.current) {
headerOffset = parseInt(
window
.getComputedStyle(headerPaddingRef.current)
.getPropertyValue("top"),
);
}
const observerOptions: IntersectionObserverInit = {
rootMargin: `-${headerOffset}px 0px -85% 0px`,
threshold: 0,
root: null,
};
const observer = new IntersectionObserver(
observeHandler,
observerOptions,
);
headingDOMNodes.forEach((heading) => {
observer.observe(heading);
});
return () => {
headingDOMNodes.forEach((heading) => {
observer.unobserve(heading);
});
};
} catch (err) {
console.log(err);
}
}, [headingDOMNodes, props.editor]);
if (!links.length) {
return (
<>
<Text size="sm">
{t("Add headings (H1, H2, H3) to generate a table of contents.")}
</Text>
</>
);
}
return (
<>
<div>
{links.map((item, idx) => (
<Box<"button">
component="button"
onClick={() => handleScrollToHeading(item.position)}
key={idx}
className={clsx(classes.link, {
[classes.linkActive]: item.element === activeElement,
})}
style={{
paddingLeft: `calc(${item.level} * var(--mantine-spacing-md))`,
}}
>
{item.label}
</Box>
))}
</div>
<div ref={headerPaddingRef} className={classes.headerPadding} />
</>
);
};
@@ -228,4 +228,4 @@ export const collabExtensions: CollabExtensions = (provider, user) => [
color: randomElement(userColors), color: randomElement(userColors),
}, },
}), }),
]; ];
+14 -19
View File
@@ -52,6 +52,7 @@ import { IPage } from "@/features/page/types/page.types.ts";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { extractPageSlugId } from "@/lib"; import { extractPageSlugId } from "@/lib";
import { FIVE_MINUTES } from "@/lib/constants.ts"; import { FIVE_MINUTES } from "@/lib/constants.ts";
import { jwtDecode } from "jwt-decode";
interface PageEditorProps { interface PageEditorProps {
pageId: string; pageId: string;
@@ -83,7 +84,6 @@ export default function PageEditor({
const documentState = useDocumentVisibility(); const documentState = useDocumentVisibility();
const [isCollabReady, setIsCollabReady] = useState(false); const [isCollabReady, setIsCollabReady] = useState(false);
const { pageSlug } = useParams(); const { pageSlug } = useParams();
const collabRetryCount = useRef(0);
const slugId = extractPageSlugId(pageSlug); const slugId = extractPageSlugId(pageSlug);
const localProvider = useMemo(() => { const localProvider = useMemo(() => {
@@ -105,13 +105,11 @@ export default function PageEditor({
connect: false, connect: false,
preserveConnection: false, preserveConnection: false,
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => { onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
collabRetryCount.current = collabRetryCount.current + 1; const payload = jwtDecode(collabQuery?.token);
refetchCollabToken().then(() => { const now = Date.now().valueOf() / 1000;
collabRetryCount.current = 0; const isTokenExpired = now >= payload.exp;
}); if (isTokenExpired) {
refetchCollabToken();
if (collabRetryCount.current > 20) {
window.location.reload();
} }
}, },
onStatus: (status) => { onStatus: (status) => {
@@ -211,6 +209,7 @@ export default function PageEditor({
queryClient.setQueryData(["pages", slugId], { queryClient.setQueryData(["pages", slugId], {
...pageData, ...pageData,
content: newContent, content: newContent,
updatedAt: new Date(),
}); });
} }
}, 3000); }, 3000);
@@ -265,17 +264,13 @@ export default function PageEditor({
documentState === "visible" && documentState === "visible" &&
remoteProvider?.status === WebSocketStatus.Disconnected remoteProvider?.status === WebSocketStatus.Disconnected
) { ) {
const reconnectTimeout = setTimeout( resetIdle();
() => { remoteProvider.connect();
remoteProvider.connect(); setTimeout(() => {
resetIdle(); setIsCollabReady(true);
}, }, 600);
collabRetryCount.current > 2 ? 3000 : 0,
);
return () => clearTimeout(reconnectTimeout);
} }
}, [isIdle, documentState, remoteProvider?.status]); }, [isIdle, documentState, remoteProvider]);
const isSynced = isLocalSynced && isRemoteSynced; const isSynced = isLocalSynced && isRemoteSynced;
@@ -284,7 +279,7 @@ export default function PageEditor({
if ( if (
!isCollabReady && !isCollabReady &&
isSynced && isSynced &&
remoteProvider.status === WebSocketStatus.Connected remoteProvider?.status === WebSocketStatus.Connected
) { ) {
setIsCollabReady(true); setIsCollabReady(true);
} }
@@ -53,24 +53,22 @@
padding: 4px; padding: 4px;
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
[data-type="detailsContent"] {
display: none;
}
}
&[open] {
[data-type="detailsButton"] {
.ProseMirror-icon {
transform: rotateZ(90deg);
}
}
[data-type="detailsContainer"] {
[data-type="detailsContent"] {
display: block;
}
}
} }
} }
}
[data-type="details"] > [data-type="detailsContainer"] > [data-type="detailsContent"]{
display: none;
}
[data-type="details"][open] > [data-type="detailsContainer"] > [data-type="detailsContent"]{
display: block;
}
[data-type="details"][open] > [data-type="detailsButton"] {
align-items: start;
}
[data-type="details"][open] > [data-type="detailsButton"] .ProseMirror-icon{
transform: rotateZ(90deg);
}
}
@@ -33,7 +33,7 @@ export async function getGroupMembers(
groupId: string, groupId: string,
params?: QueryParams, params?: QueryParams,
): Promise<IPagination<IUser>> { ): Promise<IPagination<IUser>> {
const req = await api.post("/groups/members", { groupId, params }); const req = await api.post("/groups/members", { groupId, ...params });
return req.data; return req.data;
} }
@@ -17,6 +17,13 @@ import {
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"; import { useTranslation } from "react-i18next";
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
import { useParams } from "react-router-dom";
import {
SpaceCaslAction,
SpaceCaslSubject,
} from "@/features/space/permissions/permissions.type.ts";
interface Props { interface Props {
pageId: string; pageId: string;
@@ -36,6 +43,11 @@ function HistoryList({ pageId }: Props) {
const [mainEditorTitle] = useAtom(titleEditorAtom); const [mainEditorTitle] = useAtom(titleEditorAtom);
const [, setHistoryModalOpen] = useAtom(historyAtoms); const [, setHistoryModalOpen] = useAtom(historyAtoms);
const { spaceSlug } = useParams();
const { data: space } = useSpaceQuery(spaceSlug);
const spaceRules = space?.membership?.permissions;
const spaceAbility = useSpaceAbility(spaceRules);
const confirmModal = () => const confirmModal = () =>
modals.openConfirmModal({ modals.openConfirmModal({
title: t("Please confirm your action"), title: t("Please confirm your action"),
@@ -103,20 +115,26 @@ function HistoryList({ pageId }: Props) {
))} ))}
</ScrollArea> </ScrollArea>
<Divider /> {spaceAbility.cannot(
SpaceCaslAction.Manage,
<Group p="xs" wrap="nowrap"> SpaceCaslSubject.Page,
<Button size="compact-md" onClick={confirmModal}> ) ? null : (
{t("Restore")} <>
</Button> <Divider />
<Button <Group p="xs" wrap="nowrap">
variant="default" <Button size="compact-md" onClick={confirmModal}>
size="compact-md" {t("Restore")}
onClick={() => setHistoryModalOpen(false)} </Button>
> <Button
{t("Cancel")} variant="default"
</Button> size="compact-md"
</Group> onClick={() => setHistoryModalOpen(false)}
>
{t("Cancel")}
</Button>
</Group>
</>
)}
</div> </div>
); );
} }
@@ -1,16 +1,18 @@
import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core";
import { import {
IconArrowRight,
IconArrowsHorizontal, IconArrowsHorizontal,
IconDots, IconDots,
IconFileExport, IconFileExport,
IconHistory, IconHistory,
IconLink, IconLink,
IconList,
IconMessage, IconMessage,
IconPrinter, IconPrinter,
IconTrash, IconTrash,
IconWifiOff, IconWifiOff,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import React from "react"; import React, { useEffect } from "react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx"; import useToggleAside from "@/hooks/use-toggle-aside.tsx";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
@@ -31,11 +33,14 @@ import {
yjsConnectionStatusAtom, yjsConnectionStatusAtom,
} from "@/features/editor/atoms/editor-atoms.ts"; } from "@/features/editor/atoms/editor-atoms.ts";
import { formattedDate, timeAgo } from "@/lib/time.ts"; import { formattedDate, timeAgo } from "@/lib/time.ts";
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
interface PageHeaderMenuProps { interface PageHeaderMenuProps {
readOnly?: boolean; readOnly?: boolean;
} }
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
const { t } = useTranslation();
const toggleAside = useToggleAside(); const toggleAside = useToggleAside();
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom); const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
@@ -43,7 +48,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
<> <>
{yjsConnectionStatus === "disconnected" && ( {yjsConnectionStatus === "disconnected" && (
<Tooltip <Tooltip
label="Real-time editor connection lost. Retrying..." label={t("Real-time editor connection lost. Retrying...")}
openDelay={250} openDelay={250}
withArrow withArrow
> >
@@ -53,7 +58,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
</Tooltip> </Tooltip>
)} )}
<Tooltip label="Comments" openDelay={250} withArrow> <Tooltip label={t("Comments")} openDelay={250} withArrow>
<ActionIcon <ActionIcon
variant="default" variant="default"
style={{ border: "none" }} style={{ border: "none" }}
@@ -63,6 +68,16 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("Table of contents")} openDelay={250} withArrow>
<ActionIcon
variant="default"
style={{ border: "none" }}
onClick={() => toggleAside("toc")}
>
<IconList size={20} stroke={2} />
</ActionIcon>
</Tooltip>
<PageActionMenu readOnly={readOnly} /> <PageActionMenu readOnly={readOnly} />
</> </>
); );
@@ -83,7 +98,12 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
const [tree] = useAtom(treeApiAtom); const [tree] = useAtom(treeApiAtom);
const [exportOpened, { open: openExportModal, close: closeExportModal }] = const [exportOpened, { open: openExportModal, close: closeExportModal }] =
useDisclosure(false); useDisclosure(false);
const [
movePageModalOpened,
{ open: openMovePageModal, close: closeMoveSpaceModal },
] = useDisclosure(false);
const [pageEditor] = useAtom(pageEditorAtom); const [pageEditor] = useAtom(pageEditorAtom);
const pageUpdatedAt = useTimeAgo(page.updatedAt);
const handleCopyLink = () => { const handleCopyLink = () => {
const pageUrl = const pageUrl =
@@ -147,6 +167,15 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
<Menu.Divider /> <Menu.Divider />
{!readOnly && (
<Menu.Item
leftSection={<IconArrowRight size={16} />}
onClick={openMovePageModal}
>
{t("Move")}
</Menu.Item>
)}
<Menu.Item <Menu.Item
leftSection={<IconFileExport size={16} />} leftSection={<IconFileExport size={16} />}
onClick={openExportModal} onClick={openExportModal}
@@ -181,7 +210,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
<Tooltip <Tooltip
label={t("Edited by {{name}} {{time}}", { label={t("Edited by {{name}} {{time}}", {
name: page.lastUpdatedBy.name, name: page.lastUpdatedBy.name,
time: timeAgo(page.updatedAt), time: pageUpdatedAt,
})} })}
position="left-start" position="left-start"
> >
@@ -217,6 +246,14 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
open={exportOpened} open={exportOpened}
onClose={closeExportModal} onClose={closeExportModal}
/> />
<MovePageModal
pageId={page.id}
slugId={page.slugId}
currentSpaceSlug={spaceSlug}
onClose={closeMoveSpaceModal}
open={movePageModalOpened}
/>
</> </>
); );
} }
@@ -0,0 +1,98 @@
import { Modal, Button, Group, Text } from "@mantine/core";
import { movePageToSpace } from "@/features/page/services/page-service.ts";
import { useState } from "react";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import { ISpace } from "@/features/space/types/space.types.ts";
import { queryClient } from "@/main.tsx";
import { SpaceSelect } from "@/features/space/components/sidebar/space-select.tsx";
import { useNavigate } from "react-router-dom";
import { buildPageUrl } from "@/features/page/page.utils.ts";
interface MovePageModalProps {
pageId: string;
slugId: string;
currentSpaceSlug: string;
open: boolean;
onClose: () => void;
}
export default function MovePageModal({
pageId,
slugId,
currentSpaceSlug,
open,
onClose,
}: MovePageModalProps) {
const { t } = useTranslation();
const [targetSpace, setTargetSpace] = useState<ISpace>(null);
const navigate = useNavigate();
const handlePageMove = async () => {
if (!targetSpace) return;
try {
await movePageToSpace({ pageId, spaceId: targetSpace.id });
queryClient.removeQueries({
predicate: (item) =>
["pages", "sidebar-pages", "root-sidebar-pages"].includes(
item.queryKey[0] as string,
),
});
const pageUrl = buildPageUrl(targetSpace.slug, slugId, undefined);
navigate(pageUrl);
notifications.show({
message: t("Page moved successfully"),
});
onClose();
} catch (err) {
notifications.show({
message: err.response?.data.message || "An error occurred",
color: "red",
});
console.log(err);
}
setTargetSpace(null);
};
const handleChange = (space: ISpace) => {
setTargetSpace(space);
};
return (
<Modal.Root
opened={open}
onClose={onClose}
size={500}
padding="xl"
yOffset="10vh"
xOffset={0}
mah={400}
onClick={e => e.stopPropagation()}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{t("Move page")}</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<Text mb="xs" c="dimmed" size="sm">{t("Move page to a different space.")}</Text>
<SpaceSelect
value={currentSpaceSlug}
clearable={false}
onChange={handleChange}
/>
<Group justify="end" mt="md">
<Button onClick={onClose} variant="default">
{t("Cancel")}
</Button>
<Button onClick={handlePageMove}>{t("Move")}</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
@@ -2,6 +2,7 @@ import api from "@/lib/api-client";
import { import {
IExportPageParams, IExportPageParams,
IMovePage, IMovePage,
IMovePageToSpace,
IPage, IPage,
IPageInput, IPageInput,
SidebarPagesParams, SidebarPagesParams,
@@ -34,6 +35,10 @@ export async function movePage(data: IMovePage): Promise<void> {
await api.post<void>("/pages/move", data); await api.post<void>("/pages/move", data);
} }
export async function movePageToSpace(data: IMovePageToSpace): Promise<void> {
await api.post<void>("/pages/move-to-space", data);
}
export async function getSidebarPages( export async function getSidebarPages(
params: SidebarPagesParams, params: SidebarPagesParams,
): Promise<IPagination<IPage>> { ): Promise<IPagination<IPage>> {
@@ -7,11 +7,12 @@ import {
usePageQuery, usePageQuery,
useUpdatePageMutation, useUpdatePageMutation,
} from "@/features/page/queries/page-query.ts"; } from "@/features/page/queries/page-query.ts";
import React, { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import classes from "@/features/page/tree/styles/tree.module.css"; import classes from "@/features/page/tree/styles/tree.module.css";
import { ActionIcon, Menu, rem } from "@mantine/core"; import { ActionIcon, Menu, rem } from "@mantine/core";
import { import {
IconArrowRight,
IconChevronDown, IconChevronDown,
IconChevronRight, IconChevronRight,
IconDotsVertical, IconDotsVertical,
@@ -56,6 +57,7 @@ import { extractPageSlugId } from "@/lib";
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ExportModal from "@/components/common/export-modal"; import ExportModal from "@/components/common/export-modal";
import MovePageModal from "../../components/move-page-modal.tsx";
interface SpaceTreeProps { interface SpaceTreeProps {
spaceId: string; spaceId: string;
@@ -234,6 +236,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
const emit = useQueryEmit(); const emit = useQueryEmit();
const { spaceSlug } = useParams(); const { spaceSlug } = useParams();
const timerRef = useRef(null); const timerRef = useRef(null);
const { t } = useTranslation();
const prefetchPage = () => { const prefetchPage = () => {
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
@@ -369,7 +372,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
/> />
</div> </div>
<span className={classes.text}>{node.data.name || "untitled"}</span> <span className={classes.text}>{node.data.name || t("untitled")}</span>
<div className={classes.actions}> <div className={classes.actions}>
<NodeMenu node={node} treeApi={tree} /> <NodeMenu node={node} treeApi={tree} />
@@ -434,6 +437,10 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
const { openDeleteModal } = useDeletePageModal(); const { openDeleteModal } = useDeletePageModal();
const [exportOpened, { open: openExportModal, close: closeExportModal }] = const [exportOpened, { open: openExportModal, close: closeExportModal }] =
useDisclosure(false); useDisclosure(false);
const [
movePageModalOpened,
{ open: openMovePageModal, close: closeMoveSpaceModal },
] = useDisclosure(false);
const handleCopyLink = () => { const handleCopyLink = () => {
const pageUrl = const pageUrl =
@@ -486,8 +493,18 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
{!(treeApi.props.disableEdit as boolean) && ( {!(treeApi.props.disableEdit as boolean) && (
<> <>
<Menu.Divider /> <Menu.Item
leftSection={<IconArrowRight size={16} />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openMovePageModal();
}}
>
{t("Move")}
</Menu.Item>
<Menu.Divider />
<Menu.Item <Menu.Item
c="red" c="red"
leftSection={<IconTrash size={16} />} leftSection={<IconTrash size={16} />}
@@ -504,6 +521,14 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
<MovePageModal
pageId={node.id}
slugId={node.data.slugId}
currentSpaceSlug={spaceSlug}
onClose={closeMoveSpaceModal}
open={movePageModalOpened}
/>
<ExportModal <ExportModal
type="page" type="page"
id={node.id} id={node.id}
@@ -42,6 +42,11 @@ export interface IMovePage {
parentPageId?: string; parentPageId?: string;
} }
export interface IMovePageToSpace {
pageId: string;
spaceId: string;
}
export interface SidebarPagesParams { export interface SidebarPagesParams {
spaceId: string; spaceId: string;
pageId?: string; pageId?: string;
@@ -6,21 +6,33 @@ import { ISpace } from "../../types/space.types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface SpaceSelectProps { interface SpaceSelectProps {
onChange: (value: string) => void; onChange: (value: ISpace) => void;
value?: string; value?: string;
label?: string; label?: string;
width?: number;
opened?: boolean;
clearable?: boolean;
} }
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => ( const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
<Avatar color="initials" variant="filled" name={option.label} size={20} /> <Avatar color="initials" variant="filled" name={option.label} size={20} />
<div> <div>
<Text size="sm" lineClamp={1}>{option.label}</Text> <Text size="sm" lineClamp={1}>
{option.label}
</Text>
</div> </div>
</Group> </Group>
); );
export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) { export function SpaceSelect({
onChange,
label,
value,
width,
opened,
clearable,
}: SpaceSelectProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [debouncedQuery] = useDebouncedValue(searchValue, 500); const [debouncedQuery] = useDebouncedValue(searchValue, 500);
@@ -42,8 +54,8 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
}); });
const filteredSpaceData = spaceData.filter( const filteredSpaceData = spaceData.filter(
(user) => (space) =>
!data.find((existingUser) => existingUser.value === user.value), !data.find((existingSpace) => existingSpace.value === space.value),
); );
setData((prevData) => [...prevData, ...filteredSpaceData]); setData((prevData) => [...prevData, ...filteredSpaceData]);
} }
@@ -59,14 +71,18 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
searchable searchable
searchValue={searchValue} searchValue={searchValue}
onSearchChange={setSearchValue} onSearchChange={setSearchValue}
clearable clearable={clearable}
variant="filled" variant="filled"
onChange={onChange} onChange={(slug) =>
onChange(spaces.items?.find((item) => item.slug === slug))
}
// duct tape
onClick={(e) => e.stopPropagation()}
nothingFoundMessage={t("No space found")} nothingFoundMessage={t("No space found")}
limit={50} limit={50}
checkIconPosition="right" checkIconPosition="right"
comboboxProps={{ width: 300, withinPortal: false }} comboboxProps={{ width, withinPortal: false }}
dropdownOpened dropdownOpened={opened}
/> />
); );
} }
@@ -55,7 +55,9 @@ export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) {
<SpaceSelect <SpaceSelect
label={spaceName} label={spaceName}
value={spaceSlug} value={spaceSlug}
onChange={handleSelect} onChange={space => handleSelect(space.slug)}
width={300}
opened={true}
/> />
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -1,4 +1,11 @@
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core"; import {
Group,
Table,
Text,
Menu,
ActionIcon,
ScrollArea,
} from "@mantine/core";
import React from "react"; import React from "react";
import { IconDots } from "@tabler/icons-react"; import { IconDots } from "@tabler/icons-react";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
@@ -106,93 +113,95 @@ export default function SpaceMembersList({
return ( return (
<> <>
<SearchInput onSearch={handleSearch} /> <SearchInput onSearch={handleSearch} />
<Table.ScrollContainer minWidth={500}> <ScrollArea h={400}>
<Table highlightOnHover verticalSpacing={8}> <Table.ScrollContainer minWidth={500}>
<Table.Thead> <Table highlightOnHover verticalSpacing={8}>
<Table.Tr> <Table.Thead>
<Table.Th>{t("Member")}</Table.Th> <Table.Tr>
<Table.Th>{t("Role")}</Table.Th> <Table.Th>{t("Member")}</Table.Th>
<Table.Th></Table.Th> <Table.Th>{t("Role")}</Table.Th>
</Table.Tr> <Table.Th></Table.Th>
</Table.Thead>
<Table.Tbody>
{data?.items.map((member, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm" wrap="nowrap">
{member.type === "user" && (
<CustomAvatar
avatarUrl={member?.avatarUrl}
name={member.name}
/>
)}
{member.type === "group" && <IconGroupCircle />}
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{member?.name}
</Text>
<Text fz="xs" c="dimmed">
{member.type == "user" && member?.email}
{member.type == "group" &&
`${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={spaceRoleData}
roleName={getSpaceRoleLabel(member.role)}
onChange={(newRole) =>
handleRoleChange(
member.id,
member.type,
newRole,
member.role,
)
}
disabled={readOnly}
/>
</Table.Td>
<Table.Td>
{!readOnly && (
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() =>
openRemoveModal(member.id, member.type)
}
>
{t("Remove space member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
</Table.Tr> </Table.Tr>
))} </Table.Thead>
</Table.Tbody>
</Table> <Table.Tbody>
</Table.ScrollContainer> {data?.items.map((member, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm" wrap="nowrap">
{member.type === "user" && (
<CustomAvatar
avatarUrl={member?.avatarUrl}
name={member.name}
/>
)}
{member.type === "group" && <IconGroupCircle />}
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{member?.name}
</Text>
<Text fz="xs" c="dimmed">
{member.type == "user" && member?.email}
{member.type == "group" &&
`${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={spaceRoleData}
roleName={getSpaceRoleLabel(member.role)}
onChange={(newRole) =>
handleRoleChange(
member.id,
member.type,
newRole,
member.role,
)
}
disabled={readOnly}
/>
</Table.Td>
<Table.Td>
{!readOnly && (
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() =>
openRemoveModal(member.id, member.type)
}
>
{t("Remove space member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</ScrollArea>
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
@@ -70,7 +70,6 @@ function ChangeEmailForm() {
function handleSubmit(data: FormValues) { function handleSubmit(data: FormValues) {
setIsLoading(true); setIsLoading(true);
console.log(data);
} }
return ( return (
@@ -1,7 +1,7 @@
import { Group, Text, Switch, MantineSize } from "@mantine/core";
import { useAtom } from "jotai/index";
import { userAtom } from "@/features/user/atoms/current-user-atom.ts"; import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts"; import { updateUser } from "@/features/user/services/user-service.ts";
import { Group, MantineSize, Switch, Text } from "@mantine/core";
import { useAtom } from "jotai/index";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -26,6 +26,7 @@ interface PageWidthToggleProps {
size?: MantineSize; size?: MantineSize;
label?: string; label?: string;
} }
export function PageWidthToggle({ size, label }: PageWidthToggleProps) { export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [user, setUser] = useAtom(userAtom); const [user, setUser] = useAtom(userAtom);
@@ -50,4 +51,4 @@ export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
aria-label={t("Toggle full page width")} aria-label={t("Toggle full page width")}
/> />
); );
} }
@@ -30,4 +30,4 @@ export interface IUserSettings {
preferences: { preferences: {
fullPageWidth: boolean; fullPageWidth: boolean;
}; };
} }
@@ -0,0 +1,66 @@
import { Menu, ActionIcon, Text } from "@mantine/core";
import React from "react";
import { IconDots, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useDeleteWorkspaceMemberMutation } from "@/features/workspace/queries/workspace-query.ts";
import { useTranslation } from "react-i18next";
import useUserRole from "@/hooks/use-user-role.tsx";
interface Props {
userId: string;
}
export default function MemberActionMenu({ userId }: Props) {
const { t } = useTranslation();
const deleteWorkspaceMemberMutation = useDeleteWorkspaceMemberMutation();
const { isAdmin } = useUserRole();
const onRevoke = async () => {
await deleteWorkspaceMemberMutation.mutateAsync({ userId });
};
const openRevokeModal = () =>
modals.openConfirmModal({
title: t("Delete member"),
children: (
<Text size="sm">
{t(
"Are you sure you want to delete this workspace member? This action is irreversible.",
)}
</Text>
),
centered: true,
labels: { confirm: t("Delete"), cancel: t("Don't") },
confirmProps: { color: "red" },
onConfirm: onRevoke,
});
return (
<>
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
c="red"
onClick={openRevokeModal}
leftSection={<IconTrash size={16} />}
disabled={!isAdmin}
>
{t("Delete member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</>
);
}
@@ -17,6 +17,7 @@ import Paginate from "@/components/common/paginate.tsx";
import { SearchInput } from "@/components/common/search-input.tsx"; import { SearchInput } from "@/components/common/search-input.tsx";
import NoTableResults from "@/components/common/no-table-results.tsx"; import NoTableResults from "@/components/common/no-table-results.tsx";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx"; import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx";
import MemberActionMenu from "@/features/workspace/components/members/components/members-action-menu.tsx";
export default function WorkspaceMembersTable() { export default function WorkspaceMembersTable() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -96,6 +97,9 @@ export default function WorkspaceMembersTable() {
disabled={!isAdmin} disabled={!isAdmin}
/> />
</Table.Td> </Table.Td>
<Table.Td>
{isAdmin && <MemberActionMenu userId={user.id} />}
</Table.Td>
</Table.Tr> </Table.Tr>
)) ))
) : ( ) : (
@@ -16,6 +16,7 @@ import {
getWorkspace, getWorkspace,
getWorkspacePublicData, getWorkspacePublicData,
getAppVersion, getAppVersion,
deleteWorkspaceMember,
} from "@/features/workspace/services/workspace-service"; } from "@/features/workspace/services/workspace-service";
import { IPagination, QueryParams } from "@/lib/types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
@@ -56,6 +57,30 @@ export function useWorkspaceMembersQuery(
}); });
} }
export function useDeleteWorkspaceMemberMutation() {
const queryClient = useQueryClient();
return useMutation<
void,
Error,
{
userId: string;
}
>({
mutationFn: (data) => deleteWorkspaceMember(data),
onSuccess: (data, variables) => {
notifications.show({ message: "Member deleted successfully" });
queryClient.invalidateQueries({
queryKey: ["workspaceMembers"],
});
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({ message: errorMessage, color: "red" });
},
});
}
export function useChangeMemberRoleMutation() { export function useChangeMemberRoleMutation() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -36,6 +36,12 @@ export async function getWorkspaceMembers(
return req.data; return req.data;
} }
export async function deleteWorkspaceMember(data: {
userId: string;
}): Promise<void> {
await api.post("/workspace/members/delete", data);
}
export async function updateWorkspace(data: Partial<IWorkspace>) { export async function updateWorkspace(data: Partial<IWorkspace>) {
const req = await api.post<IWorkspace>("/workspace/update", data); const req = await api.post<IWorkspace>("/workspace/update", data);
return req.data; return req.data;
+16
View File
@@ -0,0 +1,16 @@
import { timeAgo } from "@/lib/time.ts";
import { useEffect, useState } from "react";
export function useTimeAgo(date: Date | string) {
const [value, setValue] = useState(() => timeAgo(new Date(date)));
useEffect(() => {
const interval = setInterval(() => {
setValue(timeAgo(new Date(date)));
}, 5 * 1000);
return () => clearInterval(interval);
}, [date]);
return value;
}
@@ -2,8 +2,8 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountLanguage from "@/features/user/components/account-language.tsx"; import AccountLanguage from "@/features/user/components/account-language.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx"; import AccountTheme from "@/features/user/components/account-theme.tsx";
import PageWidthPref from "@/features/user/components/page-width-pref.tsx"; import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
import { Divider } from "@mantine/core";
import { getAppName } from "@/lib/config.ts"; import { getAppName } from "@/lib/config.ts";
import { Divider } from "@mantine/core";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.9.0", "version": "0.10.2",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -59,14 +59,13 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"fix-esm": "^1.0.1",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"happy-dom": "^15.11.6", "happy-dom": "^15.11.6",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"kysely": "^0.27.5", "kysely": "^0.27.5",
"kysely-migration-cli": "^0.4.2", "kysely-migration-cli": "^0.4.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"nanoid": "^5.1.0", "nanoid": "^5.1.5",
"nestjs-kysely": "^1.1.0", "nestjs-kysely": "^1.1.0",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"openid-client": "^5.7.1", "openid-client": "^5.7.1",
@@ -1,5 +1,7 @@
import { import {
afterUnloadDocumentPayload,
Extension, Extension,
onChangePayload,
onLoadDocumentPayload, onLoadDocumentPayload,
onStoreDocumentPayload, onStoreDocumentPayload,
} from '@hocuspocus/server'; } from '@hocuspocus/server';
@@ -26,6 +28,7 @@ import { Page } from '@docmost/db/types/entity.types';
@Injectable() @Injectable()
export class PersistenceExtension implements Extension { export class PersistenceExtension implements Extension {
private readonly logger = new Logger(PersistenceExtension.name); private readonly logger = new Logger(PersistenceExtension.name);
private contributors: Map<string, Set<string>> = new Map();
constructor( constructor(
private readonly pageRepo: PageRepo, private readonly pageRepo: PageRepo,
@@ -116,12 +119,27 @@ export class PersistenceExtension implements Extension {
return; return;
} }
let contributorIds = undefined;
try {
const existingContributors = page.contributorIds || [];
const contributorSet = this.contributors.get(documentName);
contributorSet.add(page.creatorId);
const newContributors = [...contributorSet];
contributorIds = Array.from(
new Set([...existingContributors, ...newContributors]),
);
this.contributors.delete(documentName);
} catch (err) {
this.logger.log('Contributors error:' + err?.['message']);
}
await this.pageRepo.updatePage( await this.pageRepo.updatePage(
{ {
content: tiptapJson, content: tiptapJson,
textContent: textContent, textContent: textContent,
ydoc: ydocState, ydoc: ydocState,
lastUpdatedById: context.user.id, lastUpdatedById: context.user.id,
contributorIds: contributorIds,
}, },
pageId, pageId,
trx, trx,
@@ -152,4 +170,21 @@ export class PersistenceExtension implements Extension {
} as IPageBacklinkJob); } as IPageBacklinkJob);
} }
} }
async onChange(data: onChangePayload) {
const documentName = data.documentName;
const userId = data.context?.user.id;
if (!userId) return;
if (!this.contributors.has(documentName)) {
this.contributors.set(documentName, new Set());
}
this.contributors.get(documentName).add(userId);
}
async afterUnloadDocument(data: afterUnloadDocumentPayload) {
const documentName = data.documentName;
this.contributors.delete(documentName);
}
} }
@@ -1,9 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports import { customAlphabet } from 'nanoid';
const { customAlphabet } = require('fix-esm').require('nanoid');
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
export const nanoIdGen = customAlphabet(alphabet, 10); export const nanoIdGen = customAlphabet(alphabet, 10);
const slugIdAlphabet = const slugIdAlphabet =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
export const generateSlugId = customAlphabet(slugIdAlphabet, 10); export const generateSlugId = customAlphabet(slugIdAlphabet, 10);
@@ -17,6 +17,9 @@ export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
if (job.name === QueueJob.DELETE_SPACE_ATTACHMENTS) { if (job.name === QueueJob.DELETE_SPACE_ATTACHMENTS) {
await this.attachmentService.handleDeleteSpaceAttachments(job.data.id); await this.attachmentService.handleDeleteSpaceAttachments(job.data.id);
} }
if (job.name === QueueJob.DELETE_USER_AVATARS) {
await this.attachmentService.handleDeleteUserAvatars(job.data.id);
}
} catch (err) { } catch (err) {
throw err; throw err;
} }
@@ -281,10 +281,42 @@ export class AttachmentService {
}), }),
); );
if(failedDeletions.length === attachments.length){ if (failedDeletions.length === attachments.length) {
throw new Error(`Failed to delete any attachments for spaceId: ${spaceId}`); throw new Error(
`Failed to delete any attachments for spaceId: ${spaceId}`,
);
}
} catch (err) {
throw err;
}
}
async handleDeleteUserAvatars(userId: string) {
try {
const userAvatars = await this.db
.selectFrom('attachments')
.select(['id', 'filePath'])
.where('creatorId', '=', userId)
.where('type', '=', AttachmentType.Avatar)
.execute();
if (!userAvatars || userAvatars.length === 0) {
return;
} }
await Promise.all(
userAvatars.map(async (attachment) => {
try {
await this.storageService.delete(attachment.filePath);
await this.attachmentRepo.deleteAttachmentById(attachment.id);
} catch (err) {
this.logger.log(
`DeleteUserAvatar: failed to delete user avatar ${attachment.id}:`,
err,
);
}
}),
);
} catch (err) { } catch (err) {
throw err; throw err;
} }
@@ -43,19 +43,22 @@ export class AuthService {
) {} ) {}
async login(loginDto: LoginDto, workspaceId: string) { async login(loginDto: LoginDto, workspaceId: string) {
const user = await this.userRepo.findByEmail( const user = await this.userRepo.findByEmail(loginDto.email, workspaceId, {
loginDto.email, includePassword: true,
workspaceId, });
{
includePassword: true const errorMessage = 'email or password does not match';
} if (!user || user?.deletedAt) {
throw new UnauthorizedException(errorMessage);
}
const isPasswordMatch = await comparePasswordHash(
loginDto.password,
user.password,
); );
if ( if (!isPasswordMatch) {
!user || throw new UnauthorizedException(errorMessage);
!(await comparePasswordHash(loginDto.password, user.password))
) {
throw new UnauthorizedException('email or password does not match');
} }
user.lastLoginAt = new Date(); user.lastLoginAt = new Date();
@@ -86,7 +89,7 @@ export class AuthService {
includePassword: true, includePassword: true,
}); });
if (!user) { if (!user || user.deletedAt) {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
} }
@@ -125,7 +128,7 @@ export class AuthService {
workspace.id, workspace.id,
); );
if (!user) { if (!user || user.deletedAt) {
return; return;
} }
@@ -168,7 +171,7 @@ export class AuthService {
} }
const user = await this.userRepo.findById(userToken.userId, workspaceId); const user = await this.userRepo.findById(userToken.userId, workspaceId);
if (!user) { if (!user || user.deletedAt) {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
} }
@@ -1,4 +1,8 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'; import {
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { import {
@@ -17,6 +21,10 @@ export class TokenService {
) {} ) {}
async generateAccessToken(user: User): Promise<string> { async generateAccessToken(user: User): Promise<string> {
if (user.deletedAt) {
throw new ForbiddenException();
}
const payload: JwtPayload = { const payload: JwtPayload = {
sub: user.id, sub: user.id,
email: user.email, email: user.email,
@@ -1,9 +1,4 @@
import { import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
BadRequestException,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt'; import { Strategy } from 'passport-jwt';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
@@ -47,7 +42,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
} }
const user = await this.userRepo.findById(payload.sub, payload.workspaceId); const user = await this.userRepo.findById(payload.sub, payload.workspaceId);
if (!user) { if (!user || user.deletedAt) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
@@ -13,3 +13,11 @@ export class MovePageDto {
@IsString() @IsString()
parentPageId?: string | null; parentPageId?: string | null;
} }
export class MovePageToSpaceDto {
@IsString()
pageId: string;
@IsString()
spaceId: string;
}
+34 -6
View File
@@ -7,11 +7,12 @@ import {
UseGuards, UseGuards,
ForbiddenException, ForbiddenException,
NotFoundException, NotFoundException,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { PageService } from './services/page.service'; import { PageService } from './services/page.service';
import { CreatePageDto } from './dto/create-page.dto'; import { CreatePageDto } from './dto/create-page.dto';
import { UpdatePageDto } from './dto/update-page.dto'; import { UpdatePageDto } from './dto/update-page.dto';
import { MovePageDto } from './dto/move-page.dto'; import { MovePageDto, MovePageToSpaceDto } from './dto/move-page.dto';
import { PageHistoryIdDto, PageIdDto, PageInfoDto } from './dto/page.dto'; import { PageHistoryIdDto, PageIdDto, PageInfoDto } from './dto/page.dto';
import { PageHistoryService } from './services/page-history.service'; import { PageHistoryService } from './services/page-history.service';
import { AuthUser } from '../../common/decorators/auth-user.decorator'; import { AuthUser } from '../../common/decorators/auth-user.decorator';
@@ -46,6 +47,7 @@ export class PageController {
includeContent: true, includeContent: true,
includeCreator: true, includeCreator: true,
includeLastUpdatedBy: true, includeLastUpdatedBy: true,
includeContributors: true,
}); });
if (!page) { if (!page) {
@@ -92,11 +94,7 @@ export class PageController {
throw new ForbiddenException(); throw new ForbiddenException();
} }
return this.pageService.update( return this.pageService.update(page, updatePageDto, user.id);
updatePageDto.pageId,
updatePageDto,
user.id,
);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@@ -209,6 +207,36 @@ export class PageController {
return this.pageService.getSidebarPages(dto.spaceId, pagination, pageId); return this.pageService.getSidebarPages(dto.spaceId, pagination, pageId);
} }
@HttpCode(HttpStatus.OK)
@Post('move-to-space')
async movePageToSpace(
@Body() dto: MovePageToSpaceDto,
@AuthUser() user: User,
) {
const movedPage = await this.pageRepo.findById(dto.pageId);
if (!movedPage) {
throw new NotFoundException('Page to move not found');
}
if (movedPage.spaceId === dto.spaceId) {
throw new BadRequestException('Page is already in this space');
}
const abilities = await Promise.all([
this.spaceAbility.createForUser(user, movedPage.spaceId),
this.spaceAbility.createForUser(user, dto.spaceId),
]);
if (
abilities.some((ability) =>
ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page),
)
) {
throw new ForbiddenException();
}
return this.pageService.movePageToSpace(movedPage, dto.spaceId);
}
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('move') @Post('move')
async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) { async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) {
@@ -19,11 +19,14 @@ import { MovePageDto } from '../dto/move-page.dto';
import { ExpressionBuilder } from 'kysely'; import { ExpressionBuilder } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
import { generateSlugId } from '../../../common/helpers'; import { generateSlugId } from '../../../common/helpers';
import { executeTx } from '@docmost/db/utils';
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
@Injectable() @Injectable()
export class PageService { export class PageService {
constructor( constructor(
private pageRepo: PageRepo, private pageRepo: PageRepo,
private attachmentRepo: AttachmentRepo,
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
) {} ) {}
@@ -60,12 +63,31 @@ export class PageService {
parentPageId = parentPage.id; parentPageId = parentPage.id;
} }
const createdPage = await this.pageRepo.insertPage({
slugId: generateSlugId(),
title: createPageDto.title,
position: await this.nextPagePosition(
createPageDto.spaceId,
parentPageId,
),
icon: createPageDto.icon,
parentPageId: parentPageId,
spaceId: createPageDto.spaceId,
creatorId: userId,
workspaceId: workspaceId,
lastUpdatedById: userId,
});
return createdPage;
}
async nextPagePosition(spaceId: string, parentPageId?: string) {
let pagePosition: string; let pagePosition: string;
const lastPageQuery = this.db const lastPageQuery = this.db
.selectFrom('pages') .selectFrom('pages')
.select(['id', 'position']) .select(['position'])
.where('spaceId', '=', createPageDto.spaceId) .where('spaceId', '=', spaceId)
.orderBy('position', 'desc') .orderBy('position', 'desc')
.limit(1); .limit(1);
@@ -96,37 +118,36 @@ export class PageService {
} }
} }
const createdPage = await this.pageRepo.insertPage({ return pagePosition;
slugId: generateSlugId(),
title: createPageDto.title,
position: pagePosition,
icon: createPageDto.icon,
parentPageId: parentPageId,
spaceId: createPageDto.spaceId,
creatorId: userId,
workspaceId: workspaceId,
lastUpdatedById: userId,
});
return createdPage;
} }
async update( async update(
pageId: string, page: Page,
updatePageDto: UpdatePageDto, updatePageDto: UpdatePageDto,
userId: string, userId: string,
): Promise<Page> { ): Promise<Page> {
const contributors = new Set<string>(page.contributorIds);
contributors.add(userId);
const contributorIds = Array.from(contributors);
await this.pageRepo.updatePage( await this.pageRepo.updatePage(
{ {
title: updatePageDto.title, title: updatePageDto.title,
icon: updatePageDto.icon, icon: updatePageDto.icon,
lastUpdatedById: userId, lastUpdatedById: userId,
updatedAt: new Date(), updatedAt: new Date(),
contributorIds: contributorIds,
}, },
pageId, page.id,
); );
return await this.pageRepo.findById(pageId); return await this.pageRepo.findById(page.id, {
includeSpace: true,
includeContent: true,
includeCreator: true,
includeLastUpdatedBy: true,
includeContributors: true,
});
} }
withHasChildren(eb: ExpressionBuilder<DB, 'pages'>) { withHasChildren(eb: ExpressionBuilder<DB, 'pages'>) {
@@ -181,6 +202,36 @@ export class PageService {
return result; return result;
} }
async movePageToSpace(rootPage: Page, spaceId: string) {
await executeTx(this.db, async (trx) => {
// Update root page
const nextPosition = await this.nextPagePosition(spaceId);
await this.pageRepo.updatePage(
{ spaceId, parentPageId: null, position: nextPosition },
rootPage.id,
trx,
);
const pageIds = await this.pageRepo
.getPageAndDescendants(rootPage.id)
.then((pages) => pages.map((page) => page.id));
// The first id is the root page id
if (pageIds.length > 1) {
// Update sub pages
await this.pageRepo.updatePages(
{ spaceId },
pageIds.filter((id) => id !== rootPage.id),
trx,
);
}
// Update attachments
await this.attachmentRepo.updateAttachmentsByPageId(
{ spaceId },
pageIds,
trx,
);
});
}
async movePage(dto: MovePageDto, movedPage: Page) { async movePage(dto: MovePageDto, movedPage: Page) {
// validate position value by attempting to generate a key // validate position value by attempting to generate a key
try { try {
@@ -84,6 +84,7 @@ export class SearchService {
.select(['id', 'name', 'avatarUrl']) .select(['id', 'name', 'avatarUrl'])
.where((eb) => eb(sql`LOWER(users.name)`, 'like', `%${query}%`)) .where((eb) => eb(sql`LOWER(users.name)`, 'like', `%${query}%`))
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId)
.where('deletedAt', 'is', null)
.limit(limit) .limit(limit)
.execute(); .execute();
} }
@@ -1,6 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types'; import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from '../../auth/dto/create-user.dto';
import { IsBoolean, IsOptional, IsString } from 'class-validator'; import { IsBoolean, IsOptional, IsString } from 'class-validator';
import { CreateUserDto } from '../../auth/dto/create-user.dto';
export class UpdateUserDto extends PartialType( export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const), OmitType(CreateUserDto, ['password'] as const),
+3 -10
View File
@@ -1,10 +1,10 @@
import { UserRepo } from '@docmost/db/repos/user/user.repo';
import { import {
BadRequestException, BadRequestException,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { UserRepo } from '@docmost/db/repos/user/user.repo';
@Injectable() @Injectable()
export class UserService { export class UserService {
@@ -27,8 +27,9 @@ export class UserService {
// preference update // preference update
if (typeof updateUserDto.fullPageWidth !== 'undefined') { if (typeof updateUserDto.fullPageWidth !== 'undefined') {
return this.updateUserPageWidthPreference( return this.userRepo.updatePreference(
userId, userId,
'fullPageWidth',
updateUserDto.fullPageWidth, updateUserDto.fullPageWidth,
); );
} }
@@ -55,12 +56,4 @@ export class UserService {
await this.userRepo.updateUser(updateUserDto, userId, workspaceId); await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
return user; return user;
} }
async updateUserPageWidthPreference(userId: string, fullPageWidth: boolean) {
return this.userRepo.updatePreference(
userId,
'fullPageWidth',
fullPageWidth,
);
}
} }
@@ -34,6 +34,7 @@ import { addDays } from 'date-fns';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { CheckHostnameDto } from '../dto/check-hostname.dto'; import { CheckHostnameDto } from '../dto/check-hostname.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('workspace') @Controller('workspace')
@@ -120,6 +121,22 @@ export class WorkspaceController {
} }
} }
@HttpCode(HttpStatus.OK)
@Post('members/delete')
async deleteWorkspaceMember(
@Body() dto: RemoveWorkspaceUserDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
) {
throw new ForbiddenException();
}
await this.workspaceService.deleteUser(user, dto.userId, workspace.id);
}
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('members/change-role') @Post('members/change-role')
async updateWorkspaceMemberRole( async updateWorkspaceMemberRole(
@@ -2,6 +2,7 @@ import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
Injectable, Injectable,
Logger,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto'; import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
@@ -26,9 +27,16 @@ import { DomainService } from '../../../integrations/environment/domain.service'
import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants'; import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants';
import { v4 } from 'uuid';
import { AttachmentType } from 'src/core/attachment/attachment.constants';
import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq';
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
private readonly logger = new Logger(WorkspaceService.name);
constructor( constructor(
private workspaceRepo: WorkspaceRepo, private workspaceRepo: WorkspaceRepo,
private spaceService: SpaceService, private spaceService: SpaceService,
@@ -39,6 +47,8 @@ export class WorkspaceService {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private domainService: DomainService, private domainService: DomainService,
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
) {} ) {}
async findById(workspaceId: string) { async findById(workspaceId: string) {
@@ -91,13 +101,15 @@ export class WorkspaceService {
createWorkspaceDto: CreateWorkspaceDto, createWorkspaceDto: CreateWorkspaceDto,
trx?: KyselyTransaction, trx?: KyselyTransaction,
) { ) {
return await executeTx( let trialEndAt = undefined;
const createdWorkspace = await executeTx(
this.db, this.db,
async (trx) => { async (trx) => {
let hostname = undefined; let hostname = undefined;
let trialEndAt = undefined;
let status = undefined; let status = undefined;
let plan = undefined; let plan = undefined;
let billingEmail = undefined;
if (this.environmentService.isCloud()) { if (this.environmentService.isCloud()) {
// generate unique hostname // generate unique hostname
@@ -110,6 +122,7 @@ export class WorkspaceService {
); );
status = WorkspaceStatus.Active; status = WorkspaceStatus.Active;
plan = 'standard'; plan = 'standard';
billingEmail = user.email;
} }
// create workspace // create workspace
@@ -121,6 +134,7 @@ export class WorkspaceService {
status, status,
trialEndAt, trialEndAt,
plan, plan,
billingEmail,
}, },
trx, trx,
); );
@@ -195,6 +209,28 @@ export class WorkspaceService {
}, },
trx, trx,
); );
if (this.environmentService.isCloud() && trialEndAt) {
try {
const delay = trialEndAt.getTime() - Date.now();
await this.billingQueue.add(
QueueJob.TRIAL_ENDED,
{ workspaceId: createdWorkspace.id },
{ delay },
);
await this.billingQueue.add(
QueueJob.WELCOME_EMAIL,
{ userId: user.id },
{ delay: 60 * 1000 }, // 1m
);
} catch (err) {
this.logger.error(err);
}
}
return createdWorkspace;
} }
async addUserToWorkspace( async addUserToWorkspace(
@@ -386,4 +422,66 @@ export class WorkspaceService {
} }
return { hostname: this.domainService.getUrl(hostname) }; return { hostname: this.domainService.getUrl(hostname) };
} }
async deleteUser(
authUser: User,
userId: string,
workspaceId: string,
): Promise<void> {
const user = await this.userRepo.findById(userId, workspaceId);
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
);
if (user.role === UserRole.OWNER && workspaceOwnerCount === 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
}
if (authUser.id === userId) {
throw new BadRequestException('You cannot delete yourself');
}
if (authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
throw new BadRequestException('You cannot delete a user with owner role');
}
await executeTx(this.db, async (trx) => {
await this.userRepo.updateUser(
{
name: 'Deleted user',
email: v4() + '@deleted.docmost.com',
avatarUrl: null,
settings: null,
deletedAt: new Date(),
},
userId,
workspaceId,
trx,
);
await trx.deleteFrom('groupUsers').where('userId', '=', userId).execute();
await trx
.deleteFrom('spaceMembers')
.where('userId', '=', userId)
.execute();
await trx
.deleteFrom('authAccounts')
.where('userId', '=', userId)
.execute();
});
try {
await this.attachmentQueue.add(QueueJob.DELETE_USER_AVATARS, user);
} catch (err) {
// empty
}
}
} }
+1 -1
View File
@@ -47,7 +47,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
log: (event: LogEvent) => { log: (event: LogEvent) => {
if (environmentService.getNodeEnv() !== 'development') return; if (environmentService.getNodeEnv() !== 'development') return;
const logger = new Logger(DatabaseModule.name); const logger = new Logger(DatabaseModule.name);
if (event.level === 'query') { if (event.level) {
if (process.env.DEBUG_DB?.toLowerCase() === 'true') { if (process.env.DEBUG_DB?.toLowerCase() === 'true') {
logger.debug(event.query.sql); logger.debug(event.query.sql);
logger.debug('query time: ' + event.queryDurationMillis + ' ms'); logger.debug('query time: ' + event.queryDurationMillis + ' ms');
@@ -0,0 +1,12 @@
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('pages')
.addColumn('contributor_ids', sql`uuid[]`, (col) => col.defaultTo("{}"))
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.alterTable('pages').dropColumn('contributor_ids').execute();
}
@@ -18,6 +18,7 @@ export async function executeWithPagination<O, DB, TB extends keyof DB>(
perPage: number; perPage: number;
page: number; page: number;
experimental_deferredJoinPrimaryKey?: StringReference<DB, TB>; experimental_deferredJoinPrimaryKey?: StringReference<DB, TB>;
hasEmptyIds?: boolean; // in cases where we pass empty whereIn ids
}, },
): Promise<PaginationResult<O>> { ): Promise<PaginationResult<O>> {
if (opts.page < 1) { if (opts.page < 1) {
@@ -33,21 +34,20 @@ export async function executeWithPagination<O, DB, TB extends keyof DB>(
.select((eb) => eb.ref(deferredJoinPrimaryKey).as('primaryKey')) .select((eb) => eb.ref(deferredJoinPrimaryKey).as('primaryKey'))
.execute() .execute()
// @ts-expect-error TODO: Fix the type here later // @ts-expect-error TODO: Fix the type here later
.then((rows) => rows.map((row) => row.primaryKey)); .then((rows) => rows.map((row) => row.primaryKey));
qb = qb qb = qb
.where((eb) => .where((eb) =>
primaryKeys.length > 0 primaryKeys.length > 0
? ? eb(deferredJoinPrimaryKey, 'in', primaryKeys as any)
eb(deferredJoinPrimaryKey, 'in', primaryKeys as any)
: eb(sql`1`, '=', 0), : eb(sql`1`, '=', 0),
) )
.clearOffset() .clearOffset()
.clearLimit(); .clearLimit();
} }
const rows = await qb.execute(); const rows = opts.hasEmptyIds ? [] : await qb.execute();
const hasNextPage = rows.length > 0 ? rows.length > opts.perPage : false; const hasNextPage = rows.length > 0 ? rows.length > opts.perPage : false;
const hasPrevPage = rows.length > 0 ? opts.page > 1 : false; const hasPrevPage = rows.length > 0 ? opts.page > 1 : false;
@@ -55,6 +55,18 @@ export class AttachmentRepo {
.execute(); .execute();
} }
updateAttachmentsByPageId(
updatableAttachment: UpdatableAttachment,
pageIds: string[],
trx?: KyselyTransaction,
) {
return dbOrTx(this.db, trx)
.updateTable('attachments')
.set(updatableAttachment)
.where('pageId', 'in', pageIds)
.executeTakeFirst();
}
async updateAttachment( async updateAttachment(
updatableAttachment: UpdatableAttachment, updatableAttachment: UpdatableAttachment,
attachmentId: string, attachmentId: string,
@@ -10,9 +10,9 @@ import {
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
import { ExpressionBuilder } from 'kysely'; import { ExpressionBuilder, sql } from 'kysely';
import { DB } from '@docmost/db/types/db'; import { DB } from '@docmost/db/types/db';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
@Injectable() @Injectable()
@@ -38,6 +38,7 @@ export class PageRepo {
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'deletedAt', 'deletedAt',
'contributorIds',
]; ];
async findById( async findById(
@@ -48,6 +49,7 @@ export class PageRepo {
includeSpace?: boolean; includeSpace?: boolean;
includeCreator?: boolean; includeCreator?: boolean;
includeLastUpdatedBy?: boolean; includeLastUpdatedBy?: boolean;
includeContributors?: boolean;
withLock?: boolean; withLock?: boolean;
trx?: KyselyTransaction; trx?: KyselyTransaction;
}, },
@@ -68,6 +70,10 @@ export class PageRepo {
query = query.select((eb) => this.withLastUpdatedBy(eb)); query = query.select((eb) => this.withLastUpdatedBy(eb));
} }
if (opts?.includeContributors) {
query = query.select((eb) => this.withContributors(eb));
}
if (opts?.includeSpace) { if (opts?.includeSpace) {
query = query.select((eb) => this.withSpace(eb)); query = query.select((eb) => this.withSpace(eb));
} }
@@ -90,18 +96,23 @@ export class PageRepo {
pageId: string, pageId: string,
trx?: KyselyTransaction, trx?: KyselyTransaction,
) { ) {
const db = dbOrTx(this.db, trx); return this.updatePages(updatablePage, [pageId], trx);
let query = db }
async updatePages(
updatePageData: UpdatablePage,
pageIds: string[],
trx?: KyselyTransaction,
) {
return dbOrTx(this.db, trx)
.updateTable('pages') .updateTable('pages')
.set({ ...updatablePage, updatedAt: new Date() }); .set({ ...updatePageData, updatedAt: new Date() })
.where(
if (isValidUUID(pageId)) { pageIds.some((pageId) => !isValidUUID(pageId)) ? 'slugId' : 'id',
query = query.where('id', '=', pageId); 'in',
} else { pageIds,
query = query.where('slugId', '=', pageId); )
} .executeTakeFirst();
return query.executeTakeFirst();
} }
async insertPage( async insertPage(
@@ -154,9 +165,11 @@ export class PageRepo {
.where('spaceId', 'in', userSpaceIds) .where('spaceId', 'in', userSpaceIds)
.orderBy('updatedAt', 'desc'); .orderBy('updatedAt', 'desc');
const hasEmptyIds = userSpaceIds.length === 0;
const result = executeWithPagination(query, { const result = executeWithPagination(query, {
page: pagination.page, page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
hasEmptyIds,
}); });
return result; return result;
@@ -189,6 +202,15 @@ export class PageRepo {
).as('lastUpdatedBy'); ).as('lastUpdatedBy');
} }
withContributors(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonArrayFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', sql`ANY(${eb.ref('pages.contributorIds')})`),
).as('contributors');
}
async getPageAndDescendants(parentPageId: string) { async getPageAndDescendants(parentPageId: string) {
return this.db return this.db
.withRecursive('page_hierarchy', (db) => .withRecursive('page_hierarchy', (db) =>
@@ -114,6 +114,7 @@ export class SpaceMemberRepo {
]) ])
.select((eb) => this.groupRepo.withMemberCount(eb)) .select((eb) => this.groupRepo.withMemberCount(eb))
.where('spaceId', '=', spaceId) .where('spaceId', '=', spaceId)
.orderBy((eb) => eb('groups.id', 'is not', null), 'desc')
.orderBy('spaceMembers.createdAt', 'asc'); .orderBy('spaceMembers.createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
@@ -221,7 +222,7 @@ export class SpaceMemberRepo {
let query = this.db let query = this.db
.selectFrom('spaces') .selectFrom('spaces')
.selectAll('spaces') .selectAll()
.select((eb) => [this.spaceRepo.withMemberCount(eb)]) .select((eb) => [this.spaceRepo.withMemberCount(eb)])
//.where('workspaceId', '=', workspaceId) //.where('workspaceId', '=', workspaceId)
.where('id', 'in', userSpaceIds) .where('id', 'in', userSpaceIds)
@@ -237,9 +238,12 @@ export class SpaceMemberRepo {
); );
} }
const hasEmptyIds = userSpaceIds.length === 0;
const result = executeWithPagination(query, { const result = executeWithPagination(query, {
page: pagination.page, page: pagination.page,
perPage: pagination.limit, perPage: pagination.limit,
hasEmptyIds,
}); });
return result; return result;
@@ -139,6 +139,7 @@ export class UserRepo {
.selectFrom('users') .selectFrom('users')
.select(this.baseFields) .select(this.baseFields)
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId)
.where('deletedAt', 'is', null)
.orderBy('createdAt', 'asc'); .orderBy('createdAt', 'asc');
if (pagination.query) { if (pagination.query) {
+1
View File
@@ -161,6 +161,7 @@ export interface PageHistory {
export interface Pages { export interface Pages {
content: Json | null; content: Json | null;
contributorIds: Generated<string[] | null>;
coverPhoto: string | null; coverPhoto: string | null;
createdAt: Generated<Timestamp>; createdAt: Generated<Timestamp>;
creatorId: string | null; creatorId: string | null;
@@ -21,13 +21,12 @@ import {
getProsemirrorContent, getProsemirrorContent,
PageExportTree, PageExportTree,
replaceInternalLinks, replaceInternalLinks,
updateAttachmentUrls, updateAttachmentUrlsToLocalPaths,
} from './utils'; } from './utils';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { Node } from '@tiptap/pm/model'; import { Node } from '@tiptap/pm/model';
import { EditorState } from '@tiptap/pm/state'; import { EditorState } from '@tiptap/pm/state';
// eslint-disable-next-line @typescript-eslint/no-require-imports import slugify from '@sindresorhus/slugify';
import slugify = require('@sindresorhus/slugify');
import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentService } from '../environment/environment.service';
@Injectable() @Injectable()
@@ -193,7 +192,7 @@ export class ExportService {
if (includeAttachments) { if (includeAttachments) {
await this.zipAttachments(updatedJsonContent, page.spaceId, folder); await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
updatedJsonContent = updateAttachmentUrls(updatedJsonContent); updatedJsonContent = updateAttachmentUrlsToLocalPaths(updatedJsonContent);
} }
const pageTitle = getPageTitle(page.title); const pageTitle = getPageTitle(page.title);
@@ -79,16 +79,18 @@ function preserveDetail(turndownService: TurndownService) {
return node.nodeName === 'DETAILS'; return node.nodeName === 'DETAILS';
}, },
replacement: function (content: any, node: HTMLInputElement) { replacement: function (content: any, node: HTMLInputElement) {
// TODO: preserve summary of nested details
const summary = node.querySelector(':scope > summary'); const summary = node.querySelector(':scope > summary');
let detailSummary = ''; let detailSummary = '';
if (summary) { if (summary) {
detailSummary = `<summary>${turndownService.turndown(summary.innerHTML)}</summary>`; detailSummary = `<summary>${turndownService.turndown(summary.innerHTML)}</summary>`;
summary.remove();
} }
const detailsContent = turndownService.turndown(node.innerHTML); const detailsContent = Array.from(node.childNodes)
.filter(child => child.nodeName !== 'SUMMARY')
.map(child => (child.nodeType === 1 ? turndownService.turndown((child as HTMLElement).outerHTML) : child.textContent))
.join('');
return `\n<details>\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`; return `\n<details>\n${detailSummary}\n\n${detailsContent}\n\n</details>\n`;
}, },
}); });
+20 -7
View File
@@ -62,17 +62,30 @@ export function isAttachmentNode(nodeType: string) {
return attachmentNodeTypes.includes(nodeType); return attachmentNodeTypes.includes(nodeType);
} }
export function updateAttachmentUrls(prosemirrorJson: any) { export function updateAttachmentUrlsToLocalPaths(prosemirrorJson: any) {
const doc = jsonToNode(prosemirrorJson); const doc = jsonToNode(prosemirrorJson);
if (!doc) return null;
// Helper function to replace specific URL prefixes
const replacePrefix = (url: string): string => {
const prefixes = ['/files', '/api/files'];
for (const prefix of prefixes) {
if (url.startsWith(prefix)) {
return url.replace(prefix, 'files');
}
}
return url;
};
doc?.descendants((node: Node) => { doc?.descendants((node: Node) => {
if (isAttachmentNode(node.type.name)) { if (isAttachmentNode(node.type.name)) {
if (node.attrs.src && node.attrs.src.startsWith('/files')) { if (node.attrs.src) {
//@ts-expect-error // @ts-ignore
node.attrs.src = node.attrs.src.replace('/files', 'files'); node.attrs.src = replacePrefix(node.attrs.src);
} else if (node.attrs.url && node.attrs.url.startsWith('/files')) { }
//@ts-expect-error if (node.attrs.url) {
node.attrs.url = node.attrs.url.replace('/files', 'files'); // @ts-ignore
node.attrs.url = replacePrefix(node.attrs.url);
} }
} }
}); });
@@ -19,18 +19,27 @@ export class MailService {
async sendEmail(message: MailMessage): Promise<void> { async sendEmail(message: MailMessage): Promise<void> {
if (message.template) { if (message.template) {
// in case this method is used directly. we do not send the tsx template from queue // in case this method is used directly. we do not send the tsx template from queue
message.html = await render(message.template, { pretty: true }); message.html = await render(message.template, {
pretty: true,
});
message.text = await render(message.template, { plainText: true }); message.text = await render(message.template, { plainText: true });
} }
const sender = `${this.environmentService.getMailFromName()} <${this.environmentService.getMailFromAddress()}> `; let from = this.environmentService.getMailFromAddress();
if (message.from) {
from = message.from;
}
const sender = `${this.environmentService.getMailFromName()} <${from}> `;
await this.mailDriver.sendMail({ from: sender, ...message }); await this.mailDriver.sendMail({ from: sender, ...message });
} }
async sendToQueue(message: MailMessage): Promise<void> { async sendToQueue(message: MailMessage): Promise<void> {
if (message.template) { if (message.template) {
// transform the React object because it gets lost when sent via the queue // transform the React object because it gets lost when sent via the queue
message.html = await render(message.template, { pretty: true }); message.html = await render(message.template, {
pretty: true,
});
message.text = await render(message.template, { message.text = await render(message.template, {
plainText: true, plainText: true,
}); });
@@ -11,7 +11,12 @@ export enum QueueJob {
DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments', DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments',
PAGE_CONTENT_UPDATE = 'page-content-update', PAGE_CONTENT_UPDATE = 'page-content-update',
DELETE_USER_AVATARS = 'delete-user-avatars',
PAGE_BACKLINKS = 'page-backlinks', PAGE_BACKLINKS = 'page-backlinks',
STRIPE_SEATS_SYNC = 'sync-stripe-seats', STRIPE_SEATS_SYNC = 'sync-stripe-seats',
TRIAL_ENDED = 'trial-ended',
WELCOME_EMAIL = 'welcome-email',
FIRST_PAYMENT_EMAIL = 'first-payment-email',
} }
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.9.0", "version": "0.10.2",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",
@@ -14,7 +14,7 @@
"client:dev": "nx run client:dev", "client:dev": "nx run client:dev",
"server:dev": "nx run server:start:dev", "server:dev": "nx run server:start:dev",
"server:start": "nx run server:start:prod", "server:start": "nx run server:start:prod",
"email:dev": "nx run @docmost/transactional:dev", "email:dev": "nx run server:email:dev",
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\"" "dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\""
}, },
"dependencies": { "dependencies": {
@@ -25,7 +25,7 @@
"@hocuspocus/transformer": "^2.15.2", "@hocuspocus/transformer": "^2.15.2",
"@joplin/turndown": "^4.0.74", "@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56", "@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "2.2.1",
"@tiptap/core": "^2.10.3", "@tiptap/core": "^2.10.3",
"@tiptap/extension-code-block": "^2.10.3", "@tiptap/extension-code-block": "^2.10.3",
"@tiptap/extension-code-block-lowlight": "^2.10.3", "@tiptap/extension-code-block-lowlight": "^2.10.3",
+1 -1
View File
@@ -16,4 +16,4 @@ export * from "./lib/drawio";
export * from "./lib/excalidraw"; export * from "./lib/excalidraw";
export * from "./lib/embed"; export * from "./lib/embed";
export * from "./lib/mention"; export * from "./lib/mention";
export * from "./lib/markdown"; export * from "./lib/markdown";
+12 -4
View File
@@ -78,10 +78,13 @@ export const Details = Node.create<DetailsOptions>({
dom.setAttribute("data-type", this.name); dom.setAttribute("data-type", this.name);
btn.setAttribute("data-type", `${this.name}Button`); btn.setAttribute("data-type", `${this.name}Button`);
div.setAttribute("data-type", `${this.name}Container`); div.setAttribute("data-type", `${this.name}Container`);
if (node.attrs.open) {
dom.setAttribute("open", "true"); if (editor.isEditable) {
} else { if (node.attrs.open) {
dom.removeAttribute("open"); dom.setAttribute("open", "true");
} else {
dom.removeAttribute("open");
}
} }
ico.innerHTML = icon("right-line"); ico.innerHTML = icon("right-line");
@@ -111,6 +114,7 @@ export const Details = Node.create<DetailsOptions>({
if (updatedNode.type !== this.type) { if (updatedNode.type !== this.type) {
return false; return false;
} }
if (!editor.isEditable) return true;
if (updatedNode.attrs.open) { if (updatedNode.attrs.open) {
dom.setAttribute("open", "true"); dom.setAttribute("open", "true");
} else { } else {
@@ -132,6 +136,10 @@ export const Details = Node.create<DetailsOptions>({
} }
const slice = state.doc.slice(range.start, range.end); const slice = state.doc.slice(range.start, range.end);
if (slice.content.firstChild.type.name === "detailsSummary")
return false;
if ( if (
!state.schema.nodes.detailsContent.contentMatch.matchFragment( !state.schema.nodes.detailsContent.contentMatch.matchFragment(
slice.content, slice.content,
+90 -293
View File
@@ -38,8 +38,8 @@ importers:
specifier: ^1.0.56 specifier: ^1.0.56
version: 1.0.56 version: 1.0.56
'@sindresorhus/slugify': '@sindresorhus/slugify':
specifier: 1.1.0 specifier: 2.2.1
version: 1.1.0 version: 2.2.1
'@tiptap/core': '@tiptap/core':
specifier: ^2.10.3 specifier: ^2.10.3
version: 2.10.3(@tiptap/pm@2.10.3) version: 2.10.3(@tiptap/pm@2.10.3)
@@ -272,6 +272,9 @@ importers:
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
jwt-decode:
specifier: ^4.0.0
version: 4.0.0
katex: katex:
specifier: 0.16.21 specifier: 0.16.21
version: 0.16.21 version: 0.16.21
@@ -483,9 +486,6 @@ importers:
cookie: cookie:
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2 version: 1.0.2
fix-esm:
specifier: ^1.0.1
version: 1.0.1
fs-extra: fs-extra:
specifier: ^11.3.0 specifier: ^11.3.0
version: 11.3.0 version: 11.3.0
@@ -505,8 +505,8 @@ importers:
specifier: ^2.1.35 specifier: ^2.1.35
version: 2.1.35 version: 2.1.35
nanoid: nanoid:
specifier: ^5.1.0 specifier: ^5.1.5
version: 5.1.0 version: 5.1.5
nestjs-kysely: nestjs-kysely:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(kysely@0.27.5)(reflect-metadata@0.2.2) version: 1.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(kysely@0.27.5)(reflect-metadata@0.2.2)
@@ -630,7 +630,7 @@ importers:
version: 7.0.0 version: 7.0.0
ts-jest: ts-jest:
specifier: ^29.2.5 specifier: ^29.2.5
version: 29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.13.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3) version: 29.2.5(@babel/core@7.24.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.5))(jest@29.7.0(@types/node@22.13.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3)
ts-loader: ts-loader:
specifier: ^9.5.2 specifier: ^9.5.2
version: 9.5.2(typescript@5.7.3)(webpack@5.98.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) version: 9.5.2(typescript@5.7.3)(webpack@5.98.0(@swc/core@1.5.25(@swc/helpers@0.5.5)))
@@ -872,18 +872,10 @@ packages:
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/compat-data@7.23.5':
resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.26.2': '@babel/compat-data@7.26.2':
resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/core@7.24.3':
resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==}
engines: {node: '>=6.9.0'}
'@babel/core@7.24.5': '@babel/core@7.24.5':
resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -916,10 +908,6 @@ packages:
resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.23.6':
resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.25.9': '@babel/helper-compilation-targets@7.25.9':
resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -949,18 +937,10 @@ packages:
resolution: {integrity: sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==} resolution: {integrity: sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-function-name@7.23.0':
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
engines: {node: '>=6.9.0'}
'@babel/helper-function-name@7.24.6': '@babel/helper-function-name@7.24.6':
resolution: {integrity: sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==} resolution: {integrity: sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-hoist-variables@7.22.5':
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
'@babel/helper-hoist-variables@7.24.6': '@babel/helper-hoist-variables@7.24.6':
resolution: {integrity: sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==} resolution: {integrity: sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1071,10 +1051,6 @@ packages:
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.23.5':
resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.25.9': '@babel/helper-validator-option@7.25.9':
resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1083,10 +1059,6 @@ packages:
resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helpers@7.24.1':
resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.24.6': '@babel/helpers@7.24.6':
resolution: {integrity: sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==} resolution: {integrity: sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1099,11 +1071,6 @@ packages:
resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/parser@7.24.1':
resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.24.5': '@babel/parser@7.24.5':
resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -1143,13 +1110,6 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
'@babel/plugin-proposal-export-namespace-from@7.18.9':
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1615,14 +1575,6 @@ packages:
resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/template@7.22.15':
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.0':
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.6': '@babel/template@7.24.6':
resolution: {integrity: sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==} resolution: {integrity: sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1631,10 +1583,6 @@ packages:
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/traverse@7.24.1':
resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.25.9': '@babel/traverse@7.25.9':
resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -3317,13 +3265,13 @@ packages:
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@sindresorhus/slugify@1.1.0': '@sindresorhus/slugify@2.2.1':
resolution: {integrity: sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw==} resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==}
engines: {node: '>=10'} engines: {node: '>=12'}
'@sindresorhus/transliterate@0.1.2': '@sindresorhus/transliterate@1.6.0':
resolution: {integrity: sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==} resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==}
engines: {node: '>=10'} engines: {node: '>=12'}
'@sinonjs/commons@3.0.1': '@sinonjs/commons@3.0.1':
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
@@ -4753,11 +4701,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
browserslist@4.23.0:
resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.24.2: browserslist@4.24.2:
resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -4819,9 +4762,6 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'} engines: {node: '>=10'}
caniuse-lite@1.0.30001600:
resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==}
caniuse-lite@1.0.30001684: caniuse-lite@1.0.30001684:
resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
@@ -5460,9 +5400,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
hasBin: true hasBin: true
electron-to-chromium@1.4.715:
resolution: {integrity: sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==}
electron-to-chromium@1.5.65: electron-to-chromium@1.5.65:
resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==}
@@ -5588,6 +5525,10 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'} engines: {node: '>=10'}
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-prettier@10.0.1: eslint-config-prettier@10.0.1:
resolution: {integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==} resolution: {integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==}
hasBin: true hasBin: true
@@ -5796,9 +5737,6 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
fix-esm@1.0.1:
resolution: {integrity: sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==}
flat-cache@4.0.1: flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -6612,6 +6550,10 @@ packages:
jws@3.2.2: jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
jwt-decode@4.0.0:
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
engines: {node: '>=18'}
katex@0.16.21: katex@0.16.21:
resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==} resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
hasBin: true hasBin: true
@@ -6762,9 +6704,6 @@ packages:
lodash.debounce@4.0.8: lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.deburr@4.1.0:
resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==}
lodash.defaults@4.2.0: lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
@@ -7013,18 +6952,18 @@ packages:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
engines: {node: ^18.17.0 || >=20.5.0} engines: {node: ^18.17.0 || >=20.5.0}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.7: nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@3.3.8: nanoid@5.1.5:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.0:
resolution: {integrity: sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw==}
engines: {node: ^18 || >=20} engines: {node: ^18 || >=20}
hasBin: true hasBin: true
@@ -7097,9 +7036,6 @@ packages:
node-machine-id@1.1.12: node-machine-id@1.1.12:
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
node-releases@2.0.18: node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
@@ -8646,12 +8582,6 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
update-browserslist-db@1.0.13:
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
update-browserslist-db@1.1.1: update-browserslist-db@1.1.1:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true hasBin: true
@@ -9672,30 +9602,8 @@ snapshots:
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.0.1 picocolors: 1.0.1
'@babel/compat-data@7.23.5': {}
'@babel/compat-data@7.26.2': {} '@babel/compat-data@7.26.2': {}
'@babel/core@7.24.3':
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.24.2
'@babel/generator': 7.24.1
'@babel/helper-compilation-targets': 7.23.6
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3)
'@babel/helpers': 7.24.1
'@babel/parser': 7.24.1
'@babel/template': 7.24.0
'@babel/traverse': 7.24.1
'@babel/types': 7.24.0
convert-source-map: 2.0.0
debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/core@7.24.5': '@babel/core@7.24.5':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
@@ -9786,14 +9694,6 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.26.0 '@babel/types': 7.26.0
'@babel/helper-compilation-targets@7.23.6':
dependencies:
'@babel/compat-data': 7.23.5
'@babel/helper-validator-option': 7.23.5
browserslist: 4.23.0
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-compilation-targets@7.25.9': '@babel/helper-compilation-targets@7.25.9':
dependencies: dependencies:
'@babel/compat-data': 7.26.2 '@babel/compat-data': 7.26.2
@@ -9837,20 +9737,11 @@ snapshots:
'@babel/helper-environment-visitor@7.24.6': {} '@babel/helper-environment-visitor@7.24.6': {}
'@babel/helper-function-name@7.23.0':
dependencies:
'@babel/template': 7.22.15
'@babel/types': 7.24.0
'@babel/helper-function-name@7.24.6': '@babel/helper-function-name@7.24.6':
dependencies: dependencies:
'@babel/template': 7.24.6 '@babel/template': 7.24.6
'@babel/types': 7.24.6 '@babel/types': 7.24.6
'@babel/helper-hoist-variables@7.22.5':
dependencies:
'@babel/types': 7.24.0
'@babel/helper-hoist-variables@7.24.6': '@babel/helper-hoist-variables@7.24.6':
dependencies: dependencies:
'@babel/types': 7.24.6 '@babel/types': 7.24.6
@@ -9874,15 +9765,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/helper-module-transforms@7.23.3(@babel/core@7.24.3)':
dependencies:
'@babel/core': 7.24.3
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-module-imports': 7.22.15
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
'@babel/helper-module-transforms@7.23.3(@babel/core@7.26.0)': '@babel/helper-module-transforms@7.23.3(@babel/core@7.26.0)':
dependencies: dependencies:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
@@ -9975,8 +9857,6 @@ snapshots:
'@babel/helper-validator-identifier@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-option@7.23.5': {}
'@babel/helper-validator-option@7.25.9': {} '@babel/helper-validator-option@7.25.9': {}
'@babel/helper-wrap-function@7.22.20': '@babel/helper-wrap-function@7.22.20':
@@ -9985,14 +9865,6 @@ snapshots:
'@babel/template': 7.25.9 '@babel/template': 7.25.9
'@babel/types': 7.26.0 '@babel/types': 7.26.0
'@babel/helpers@7.24.1':
dependencies:
'@babel/template': 7.24.0
'@babel/traverse': 7.24.1
'@babel/types': 7.24.0
transitivePeerDependencies:
- supports-color
'@babel/helpers@7.24.6': '@babel/helpers@7.24.6':
dependencies: dependencies:
'@babel/template': 7.25.9 '@babel/template': 7.25.9
@@ -10010,10 +9882,6 @@ snapshots:
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.0.0 picocolors: 1.0.0
'@babel/parser@7.24.1':
dependencies:
'@babel/types': 7.24.0
'@babel/parser@7.24.5': '@babel/parser@7.24.5':
dependencies: dependencies:
'@babel/types': 7.26.0 '@babel/types': 7.26.0
@@ -10051,19 +9919,13 @@ snapshots:
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-decorators': 7.23.3(@babel/core@7.26.0) '@babel/plugin-syntax-decorators': 7.23.3(@babel/core@7.26.0)
'@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.24.3)':
dependencies:
'@babel/core': 7.24.3
'@babel/helper-plugin-utils': 7.24.0
'@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3)
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)':
dependencies: dependencies:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.3)': '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10077,9 +9939,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10088,9 +9950,9 @@ snapshots:
'@babel/core': 7.24.6 '@babel/core': 7.24.6
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.3)': '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10119,11 +9981,6 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.3)':
dependencies:
'@babel/core': 7.24.3
'@babel/helper-plugin-utils': 7.24.0
'@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.26.0)': '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.26.0)':
dependencies: dependencies:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
@@ -10139,9 +9996,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.3)': '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10155,9 +10012,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10181,9 +10038,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.3)': '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10197,9 +10054,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10213,9 +10070,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.3)': '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10229,9 +10086,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10245,9 +10102,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10261,9 +10118,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.3)': '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10282,9 +10139,9 @@ snapshots:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.3)': '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5)':
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.25.9 '@babel/helper-plugin-utils': 7.25.9
optional: true optional: true
@@ -10454,13 +10311,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.24.3)':
dependencies:
'@babel/core': 7.24.3
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3)
'@babel/helper-plugin-utils': 7.24.0
'@babel/helper-simple-access': 7.22.5
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.26.0)': '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.26.0)':
dependencies: dependencies:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
@@ -10763,18 +10613,6 @@ snapshots:
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
'@babel/template@7.22.15':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.26.2
'@babel/types': 7.24.0
'@babel/template@7.24.0':
dependencies:
'@babel/code-frame': 7.24.2
'@babel/parser': 7.24.1
'@babel/types': 7.24.0
'@babel/template@7.24.6': '@babel/template@7.24.6':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@@ -10787,21 +10625,6 @@ snapshots:
'@babel/parser': 7.26.2 '@babel/parser': 7.26.2
'@babel/types': 7.26.0 '@babel/types': 7.26.0
'@babel/traverse@7.24.1':
dependencies:
'@babel/code-frame': 7.24.2
'@babel/generator': 7.24.1
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-function-name': 7.23.0
'@babel/helper-hoist-variables': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.24.1
'@babel/types': 7.24.0
debug: 4.3.7
globals: 11.12.0
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.25.9': '@babel/traverse@7.25.9':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@@ -12382,15 +12205,14 @@ snapshots:
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@sindresorhus/slugify@1.1.0': '@sindresorhus/slugify@2.2.1':
dependencies: dependencies:
'@sindresorhus/transliterate': 0.1.2 '@sindresorhus/transliterate': 1.6.0
escape-string-regexp: 4.0.0 escape-string-regexp: 5.0.0
'@sindresorhus/transliterate@0.1.2': '@sindresorhus/transliterate@1.6.0':
dependencies: dependencies:
escape-string-regexp: 2.0.0 escape-string-regexp: 5.0.0
lodash.deburr: 4.1.0
'@sinonjs/commons@3.0.1': '@sinonjs/commons@3.0.1':
dependencies: dependencies:
@@ -13996,13 +13818,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
babel-jest@29.7.0(@babel/core@7.24.3): babel-jest@29.7.0(@babel/core@7.24.5):
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@jest/transform': 29.7.0 '@jest/transform': 29.7.0
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1 babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.24.3) babel-preset-jest: 29.6.3(@babel/core@7.24.5)
chalk: 4.1.2 chalk: 4.1.2
graceful-fs: 4.2.11 graceful-fs: 4.2.11
slash: 3.0.0 slash: 3.0.0
@@ -14086,21 +13908,21 @@ snapshots:
optionalDependencies: optionalDependencies:
'@babel/traverse': 7.25.9 '@babel/traverse': 7.25.9
babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.3): babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.5):
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5)
optional: true optional: true
babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.6): babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.6):
@@ -14119,11 +13941,11 @@ snapshots:
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.6) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.6)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.6) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.6)
babel-preset-jest@29.6.3(@babel/core@7.24.3): babel-preset-jest@29.6.3(@babel/core@7.24.5):
dependencies: dependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
babel-plugin-jest-hoist: 29.6.3 babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.3) babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.5)
optional: true optional: true
babel-preset-jest@29.6.3(@babel/core@7.24.6): babel-preset-jest@29.6.3(@babel/core@7.24.6):
@@ -14188,13 +14010,6 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
browserslist@4.23.0:
dependencies:
caniuse-lite: 1.0.30001600
electron-to-chromium: 1.4.715
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
browserslist@4.24.2: browserslist@4.24.2:
dependencies: dependencies:
caniuse-lite: 1.0.30001684 caniuse-lite: 1.0.30001684
@@ -14266,8 +14081,6 @@ snapshots:
camelcase@6.3.0: {} camelcase@6.3.0: {}
caniuse-lite@1.0.30001600: {}
caniuse-lite@1.0.30001684: {} caniuse-lite@1.0.30001684: {}
chalk@2.4.2: chalk@2.4.2:
@@ -14914,8 +14727,6 @@ snapshots:
dependencies: dependencies:
jake: 10.8.7 jake: 10.8.7
electron-to-chromium@1.4.715: {}
electron-to-chromium@1.5.65: {} electron-to-chromium@1.5.65: {}
emittery@0.13.1: {} emittery@0.13.1: {}
@@ -15172,6 +14983,8 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
eslint-config-prettier@10.0.1(eslint@9.20.1(jiti@1.21.0)): eslint-config-prettier@10.0.1(eslint@9.20.1(jiti@1.21.0)):
dependencies: dependencies:
eslint: 9.20.1(jiti@1.21.0) eslint: 9.20.1(jiti@1.21.0)
@@ -15469,14 +15282,6 @@ snapshots:
locate-path: 6.0.0 locate-path: 6.0.0
path-exists: 4.0.0 path-exists: 4.0.0
fix-esm@1.0.1:
dependencies:
'@babel/core': 7.24.3
'@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.24.3)
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.3)
transitivePeerDependencies:
- supports-color
flat-cache@4.0.1: flat-cache@4.0.1:
dependencies: dependencies:
flatted: 3.2.9 flatted: 3.2.9
@@ -16534,6 +16339,8 @@ snapshots:
jwa: 1.4.1 jwa: 1.4.1
safe-buffer: 5.2.1 safe-buffer: 5.2.1
jwt-decode@4.0.0: {}
katex@0.16.21: katex@0.16.21:
dependencies: dependencies:
commander: 8.3.0 commander: 8.3.0
@@ -16661,8 +16468,6 @@ snapshots:
lodash.debounce@4.0.8: {} lodash.debounce@4.0.8: {}
lodash.deburr@4.1.0: {}
lodash.defaults@4.2.0: {} lodash.defaults@4.2.0: {}
lodash.flatten@4.4.0: {} lodash.flatten@4.4.0: {}
@@ -16904,11 +16709,11 @@ snapshots:
mute-stream@2.0.0: {} mute-stream@2.0.0: {}
nanoid@3.3.11: {}
nanoid@3.3.7: {} nanoid@3.3.7: {}
nanoid@3.3.8: {} nanoid@5.1.5: {}
nanoid@5.1.0: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -16974,8 +16779,6 @@ snapshots:
node-machine-id@1.1.12: {} node-machine-id@1.1.12: {}
node-releases@2.0.14: {}
node-releases@2.0.18: {} node-releases@2.0.18: {}
nodemailer@6.10.0: {} nodemailer@6.10.0: {}
@@ -17415,7 +17218,7 @@ snapshots:
postcss@8.4.31: postcss@8.4.31:
dependencies: dependencies:
nanoid: 3.3.8 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
@@ -17427,7 +17230,7 @@ snapshots:
postcss@8.5.2: postcss@8.5.2:
dependencies: dependencies:
nanoid: 3.3.8 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
@@ -18477,7 +18280,7 @@ snapshots:
ts-dedent@2.2.0: {} ts-dedent@2.2.0: {}
ts-jest@29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.13.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3): ts-jest@29.2.5(@babel/core@7.24.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.5))(jest@29.7.0(@types/node@22.13.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3):
dependencies: dependencies:
bs-logger: 0.2.6 bs-logger: 0.2.6
ejs: 3.1.10 ejs: 3.1.10
@@ -18491,10 +18294,10 @@ snapshots:
typescript: 5.7.3 typescript: 5.7.3
yargs-parser: 21.1.1 yargs-parser: 21.1.1
optionalDependencies: optionalDependencies:
'@babel/core': 7.24.3 '@babel/core': 7.24.5
'@jest/transform': 29.7.0 '@jest/transform': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.24.3) babel-jest: 29.7.0(@babel/core@7.24.5)
ts-loader@9.5.2(typescript@5.7.3)(webpack@5.98.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): ts-loader@9.5.2(typescript@5.7.3)(webpack@5.98.0(@swc/core@1.5.25(@swc/helpers@0.5.5))):
dependencies: dependencies:
@@ -18678,12 +18481,6 @@ snapshots:
universalify@2.0.1: {} universalify@2.0.1: {}
update-browserslist-db@1.0.13(browserslist@4.23.0):
dependencies:
browserslist: 4.23.0
escalade: 3.1.1
picocolors: 1.0.0
update-browserslist-db@1.1.1(browserslist@4.24.2): update-browserslist-db@1.1.1(browserslist@4.24.2):
dependencies: dependencies:
browserslist: 4.24.2 browserslist: 4.24.2