mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 20:22:05 +10:00
Compare commits
32 Commits
fix/editor
...
nanoid-esm
| Author | SHA1 | Date | |
|---|---|---|---|
| 674769df02 | |||
| de57d05199 | |||
| 89ec990232 | |||
| 49d0f1cc9a | |||
| 268001ae26 | |||
| 27fa45a769 | |||
| f9711918a3 | |||
| 29bb52db0c | |||
| f2241db5ee | |||
| 58d1855a36 | |||
| 7fe3c5f177 | |||
| 5fd477d074 | |||
| 4aa5d7e326 | |||
| 7f7f2bccd0 | |||
| a9f370660b | |||
| 117c7049ff | |||
| cd10365f71 | |||
| ee30d9d0f2 | |||
| 276ececbf2 | |||
| fa194a497c | |||
| 1eaba6e77f | |||
| 651e5f6153 | |||
| 7431804a46 | |||
| 3559358d14 | |||
| 06270ff747 | |||
| 233536314f | |||
| 17ce3bab8a | |||
| b27d1708b0 | |||
| 64f0531093 | |||
| 8aa604637e | |||
| 7ca2b437d4 | |||
| 595bd1dc81 |
@ -9,9 +9,6 @@
|
||||
</div>
|
||||
<br />
|
||||
|
||||
> [!NOTE]
|
||||
> Docmost is currently in **beta**. We value your feedback as we progress towards a stable release.
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@ -34,6 +34,7 @@
|
||||
"jotai": "^2.12.1",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "0.16.21",
|
||||
"lowlight": "^3.2.0",
|
||||
"mermaid": "^11.4.1",
|
||||
|
||||
@ -351,5 +351,16 @@
|
||||
"Created at: {{time}}": "Erstellt am: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}",
|
||||
"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}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}",
|
||||
"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}}",
|
||||
"Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}",
|
||||
"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 pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
|
||||
"Cancel": "Annuler",
|
||||
"Change email": "Changer l'email",
|
||||
"Change email": "Changer le courriel",
|
||||
"Change password": "Changer le mot de passe",
|
||||
"Change photo": "Changer la photo",
|
||||
"Choose a role": "Choisir un rôle",
|
||||
@ -351,5 +351,16 @@
|
||||
"Created at: {{time}}": "Créé à : {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Modifié par {{name}} {{time}}",
|
||||
"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",
|
||||
"Member removed successfully": "Membro rimosso con successo",
|
||||
"Member role updated successfully": "Ruolo del membro aggiornato con successo",
|
||||
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Created by: <b>{{creatorName}}</b>": "Creato da: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Creato il: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Modificato da {{name}} il {{time}}",
|
||||
"Word count: {{wordCount}}": "Conteggio parole: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Conteggio caratteri: {{characterCount}}",
|
||||
"New update": "Nuovo aggiornamento",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} è disponibile",
|
||||
"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": "メンバーを追加しました",
|
||||
"Member removed successfully": "メンバーが削除されました",
|
||||
"Member role updated successfully": "メンバーのロールを更新しました",
|
||||
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Created by: <b>{{creatorName}}</b>": "作成者: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "が作成しました:{{time}}",
|
||||
"Edited by {{name}} {{time}}": "最終編集: {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "ワード数: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "文字数: {{characterCount}}",
|
||||
"New update": "新規更新",
|
||||
"{{latestVersion}} is available": "{{latestVersion}}は利用可能です",
|
||||
"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 theme": "배경 선택",
|
||||
"Send invitation": "초대 보내기",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Invitation sent": "초대 발송 완료",
|
||||
"Settings": "설정",
|
||||
"Setup workspace": "Workspace 설정",
|
||||
"Sign In": "로그인",
|
||||
@ -245,7 +245,7 @@
|
||||
"Align left": "왼쪽 정렬",
|
||||
"Align right": "오른쪽 정렬",
|
||||
"Align center": "가운데 정렬",
|
||||
"Justify": "Justify",
|
||||
"Justify": "정렬",
|
||||
"Merge cells": "셀 병합",
|
||||
"Split cell": "셀 분할",
|
||||
"Delete column": "열 삭제",
|
||||
@ -341,15 +341,26 @@
|
||||
"Names do not match": "이름이 일치하지 않습니다",
|
||||
"Today, {{time}}": "오늘, {{time}}",
|
||||
"Yesterday, {{time}}": "어제, {{time}}",
|
||||
"Space created successfully": "Space created successfully",
|
||||
"Space updated successfully": "Space updated successfully",
|
||||
"Space deleted successfully": "Space deleted successfully",
|
||||
"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 at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Space created successfully": "공간 생성 완료",
|
||||
"Space updated successfully": "공간이 성공적으로 업데이트되었습니다",
|
||||
"Space deleted successfully": "스페이스 삭제 완료",
|
||||
"Members added successfully": "회원 추가 완료",
|
||||
"Member removed successfully": "멤버가 성공적으로 제거되었습니다",
|
||||
"Member role updated successfully": "회원 역할이 성공적으로 업데이트되었습니다",
|
||||
"Created by: <b>{{creatorName}}</b>": "작성자: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "생성 날짜: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "{{name}}님이 편집함 {{time}}",
|
||||
"Word count: {{wordCount}}": "단어 수: {{wordCount}}",
|
||||
"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}}",
|
||||
"Edited by {{name}} {{time}}": "Bewerkt door {{name}} {{time}}",
|
||||
"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 theme": "Selecionar tema",
|
||||
"Send invitation": "Enviar convite",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Invitation sent": "Convite enviado",
|
||||
"Settings": "Configurações",
|
||||
"Setup workspace": "Configurar workspace",
|
||||
"Sign In": "Entrar",
|
||||
@ -245,7 +245,7 @@
|
||||
"Align left": "Alinhar à esquerda",
|
||||
"Align right": "Alinhar à direita",
|
||||
"Align center": "Alinhar ao centro",
|
||||
"Justify": "Justify",
|
||||
"Justify": "Justificar",
|
||||
"Merge cells": "Mesclar células",
|
||||
"Split cell": "Dividir célula",
|
||||
"Delete column": "Excluir coluna",
|
||||
@ -341,15 +341,26 @@
|
||||
"Names do not match": "Os nomes não coincidem",
|
||||
"Today, {{time}}": "Hoje, {{time}}",
|
||||
"Yesterday, {{time}}": "Ontem, {{time}}",
|
||||
"Space created successfully": "Space created successfully",
|
||||
"Space updated successfully": "Space updated successfully",
|
||||
"Space deleted successfully": "Space deleted successfully",
|
||||
"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 at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Space created successfully": "Espaço criado com sucesso",
|
||||
"Space updated successfully": "Espaço atualizado com sucesso",
|
||||
"Space deleted successfully": "Espaço excluído com sucesso",
|
||||
"Members added successfully": "Membros adicionados com sucesso",
|
||||
"Member removed successfully": "Membro removido com sucesso",
|
||||
"Member role updated successfully": "Função do membro atualizada com sucesso",
|
||||
"Created by: <b>{{creatorName}}</b>": "Criado por: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Criado em: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Contagem de palavras: {{wordCount}}",
|
||||
"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 theme": "Выберите тему",
|
||||
"Send invitation": "Отправить приглашение",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Invitation sent": "Приглашение отправлено",
|
||||
"Settings": "Настройки",
|
||||
"Setup workspace": "Настроить рабочее пространство",
|
||||
"Sign In": "Вход",
|
||||
@ -245,7 +245,7 @@
|
||||
"Align left": "По левому краю",
|
||||
"Align right": "По правому краю",
|
||||
"Align center": "По центру",
|
||||
"Justify": "Justify",
|
||||
"Justify": "По ширине",
|
||||
"Merge cells": "Объединить ячейки",
|
||||
"Split cell": "Разделить ячейку",
|
||||
"Delete column": "Удалить столбец",
|
||||
@ -331,25 +331,36 @@
|
||||
"Insert math equation": "Вставить математическое выражение",
|
||||
"Mermaid diagram": "Диаграмма Mermaid",
|
||||
"Insert mermaid diagram": "Вставить диаграмму Mermaid",
|
||||
"Insert and design Drawio diagrams": "Вставьте и редактируйте диаграммы Draw.io",
|
||||
"Insert and design Drawio diagrams": "Вставить и рисовать диаграммы Draw.io",
|
||||
"Insert current date": "Вставить текущую дату",
|
||||
"Draw and sketch excalidraw diagrams": "Создайте и рисуйте диаграммы Excalidraw",
|
||||
"Draw and sketch excalidraw diagrams": "Вставить и рисовать диаграммы Excalidraw",
|
||||
"Multiple": "Несколько",
|
||||
"Heading {{level}}": "Заголовок {{level}}",
|
||||
"Toggle title": "Переключить заголовок",
|
||||
"Write anything. Enter \"/\" for commands": "Пишите что угодно. Введите \"/\" для выбора команд",
|
||||
"Write anything. Enter \"/\" for commands": "Начните писать. Введите \"/\" для списка команд",
|
||||
"Names do not match": "Названия не совпадают",
|
||||
"Today, {{time}}": "Сегодня, {{time}}",
|
||||
"Yesterday, {{time}}": "Вчера, {{time}}",
|
||||
"Space created successfully": "Space created successfully",
|
||||
"Space updated successfully": "Space updated successfully",
|
||||
"Space deleted successfully": "Space deleted successfully",
|
||||
"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 at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Space created successfully": "Пространство успешно создано",
|
||||
"Space updated successfully": "Пространство успешно обновлено",
|
||||
"Space deleted successfully": "Пространство успешно удалено",
|
||||
"Members added successfully": "Участники успешно добавлены",
|
||||
"Member removed successfully": "Участник успешно удален",
|
||||
"Member role updated successfully": "Роль участника успешно обновлена",
|
||||
"Created by: <b>{{creatorName}}</b>": "Автор: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Дата создания: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Изменено {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Количество слов: {{wordCount}}",
|
||||
"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 theme": "选择主题",
|
||||
"Send invitation": "发送邀请",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Invitation sent": "邀请邮件已发送",
|
||||
"Settings": "设置",
|
||||
"Setup workspace": "设置工作空间",
|
||||
"Sign In": "登录",
|
||||
@ -245,7 +245,7 @@
|
||||
"Align left": "靠左对齐",
|
||||
"Align right": "靠右对齐",
|
||||
"Align center": "居中对齐",
|
||||
"Justify": "Justify",
|
||||
"Justify": "两端对齐",
|
||||
"Merge cells": "合并单元格",
|
||||
"Split cell": "分割单元格",
|
||||
"Delete column": "删除整列",
|
||||
@ -341,15 +341,26 @@
|
||||
"Names do not match": "名称不匹配",
|
||||
"Today, {{time}}": "今天,{{time}}",
|
||||
"Yesterday, {{time}}": "昨天,{{time}}",
|
||||
"Space created successfully": "Space created successfully",
|
||||
"Space updated successfully": "Space updated successfully",
|
||||
"Space deleted successfully": "Space deleted successfully",
|
||||
"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 at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
"Space created successfully": "空间创建成功",
|
||||
"Space updated successfully": "空间更新成功",
|
||||
"Space deleted successfully": "空间已成功删除",
|
||||
"Members added successfully": "成员添加成功",
|
||||
"Member removed successfully": "成员移除成功",
|
||||
"Member role updated successfully": "成员角色更新成功",
|
||||
"Created by: <b>{{creatorName}}</b>": "创建者:<b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "创建于:{{time}}",
|
||||
"Edited by {{name}} {{time}}": "由{{name}} 编辑于 {{time}}",
|
||||
"Word count: {{wordCount}}": "字数:{{wordCount}}",
|
||||
"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)以生成目录。"
|
||||
}
|
||||
|
||||
@ -65,11 +65,12 @@ export default function ExportModal({
|
||||
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}>Export {type}</Modal.Title>
|
||||
<Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
||||
@ -21,7 +21,7 @@ export default function Paginate({
|
||||
}
|
||||
|
||||
return (
|
||||
<Group mt="md">
|
||||
<Group mt="md" justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
|
||||
@ -4,10 +4,14 @@ import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
const pageEditor = useAtomValue(pageEditorAtom);
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
@ -17,6 +21,10 @@ export default function Aside() {
|
||||
component = <CommentList />;
|
||||
title = "Comments";
|
||||
break;
|
||||
case "toc":
|
||||
component = <TableOfContents editor={pageEditor} />;
|
||||
title = "Table of contents";
|
||||
break;
|
||||
default:
|
||||
component = null;
|
||||
title = null;
|
||||
|
||||
@ -20,4 +20,4 @@ export const asideStateAtom = atom<AsideStateType>({
|
||||
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"
|
||||
style={{ cursor: "pointer" }}
|
||||
disabled={!hasUpdate}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://github.com/docmost/docmost/releases",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
|
||||
@ -19,8 +19,8 @@ export function useCollabToken(): UseQueryResult<ICollabToken, Error> {
|
||||
queryKey: ["collab-token"],
|
||||
queryFn: () => getCollabToken(),
|
||||
staleTime: 20 * 60 * 60 * 1000, //20hrs
|
||||
refetchInterval: 12 * 60 * 60 * 1000, // 12hrs
|
||||
refetchIntervalInBackground: true,
|
||||
//refetchInterval: 12 * 60 * 60 * 1000, // 12hrs
|
||||
//refetchIntervalInBackground: true,
|
||||
refetchOnMount: true,
|
||||
//@ts-ignore
|
||||
retry: (failureCount, error) => {
|
||||
|
||||
@ -19,8 +19,7 @@
|
||||
box-shadow: 0 0 0 2px var(--mantine-color-blue-3);
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
width: 100%;
|
||||
.ProseMirror :global(.ProseMirror){
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
@ -29,7 +28,6 @@
|
||||
padding-right: 6px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 14px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
searchTerms: ["collapsible", "block", "toggle", "details", "expand"],
|
||||
icon: IconCaretRightFilled,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).toggleDetails().run(),
|
||||
editor.chain().focus().deleteRange(range).setDetails().run(),
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
}),
|
||||
];
|
||||
];
|
||||
@ -52,6 +52,7 @@ import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
interface PageEditorProps {
|
||||
pageId: string;
|
||||
@ -83,7 +84,6 @@ export default function PageEditor({
|
||||
const documentState = useDocumentVisibility();
|
||||
const [isCollabReady, setIsCollabReady] = useState(false);
|
||||
const { pageSlug } = useParams();
|
||||
const collabRetryCount = useRef(0);
|
||||
const slugId = extractPageSlugId(pageSlug);
|
||||
|
||||
const localProvider = useMemo(() => {
|
||||
@ -105,13 +105,11 @@ export default function PageEditor({
|
||||
connect: false,
|
||||
preserveConnection: false,
|
||||
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
|
||||
collabRetryCount.current = collabRetryCount.current + 1;
|
||||
refetchCollabToken().then(() => {
|
||||
collabRetryCount.current = 0;
|
||||
});
|
||||
|
||||
if (collabRetryCount.current > 20) {
|
||||
window.location.reload();
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken();
|
||||
}
|
||||
},
|
||||
onStatus: (status) => {
|
||||
@ -211,6 +209,7 @@ export default function PageEditor({
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
...pageData,
|
||||
content: newContent,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
@ -265,19 +264,13 @@ export default function PageEditor({
|
||||
documentState === "visible" &&
|
||||
remoteProvider?.status === WebSocketStatus.Disconnected
|
||||
) {
|
||||
const reconnectTimeout = setTimeout(
|
||||
() => {
|
||||
remoteProvider.connect();
|
||||
resetIdle();
|
||||
},
|
||||
collabRetryCount.current > 2 ? 3000 : 0,
|
||||
);
|
||||
|
||||
setIsCollabReady(true);
|
||||
|
||||
return () => clearTimeout(reconnectTimeout);
|
||||
resetIdle();
|
||||
remoteProvider.connect();
|
||||
setTimeout(() => {
|
||||
setIsCollabReady(true);
|
||||
}, 600);
|
||||
}
|
||||
}, [isIdle, documentState, remoteProvider?.status]);
|
||||
}, [isIdle, documentState, remoteProvider]);
|
||||
|
||||
const isSynced = isLocalSynced && isRemoteSynced;
|
||||
|
||||
@ -286,7 +279,7 @@ export default function PageEditor({
|
||||
if (
|
||||
!isCollabReady &&
|
||||
isSynced &&
|
||||
remoteProvider.status === WebSocketStatus.Connected
|
||||
remoteProvider?.status === WebSocketStatus.Connected
|
||||
) {
|
||||
setIsCollabReady(true);
|
||||
}
|
||||
|
||||
@ -53,24 +53,22 @@
|
||||
padding: 4px;
|
||||
word-break: 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,
|
||||
params?: QueryParams,
|
||||
): Promise<IPagination<IUser>> {
|
||||
const req = await api.post("/groups/members", { groupId, params });
|
||||
const req = await api.post("/groups/members", { groupId, ...params });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,13 @@ import {
|
||||
import { modals } from "@mantine/modals";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
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 {
|
||||
pageId: string;
|
||||
@ -36,6 +43,11 @@ function HistoryList({ pageId }: Props) {
|
||||
const [mainEditorTitle] = useAtom(titleEditorAtom);
|
||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useSpaceQuery(spaceSlug);
|
||||
const spaceRules = space?.membership?.permissions;
|
||||
const spaceAbility = useSpaceAbility(spaceRules);
|
||||
|
||||
const confirmModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: t("Please confirm your action"),
|
||||
@ -103,20 +115,26 @@ function HistoryList({ pageId }: Props) {
|
||||
))}
|
||||
</ScrollArea>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group p="xs" wrap="nowrap">
|
||||
<Button size="compact-md" onClick={confirmModal}>
|
||||
{t("Restore")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-md"
|
||||
onClick={() => setHistoryModalOpen(false)}
|
||||
>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
</Group>
|
||||
{spaceAbility.cannot(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Page,
|
||||
) ? null : (
|
||||
<>
|
||||
<Divider />
|
||||
<Group p="xs" wrap="nowrap">
|
||||
<Button size="compact-md" onClick={confirmModal}>
|
||||
{t("Restore")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-md"
|
||||
onClick={() => setHistoryModalOpen(false)}
|
||||
>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconArrowsHorizontal,
|
||||
IconDots,
|
||||
IconFileExport,
|
||||
IconHistory,
|
||||
IconLink,
|
||||
IconList,
|
||||
IconMessage,
|
||||
IconPrinter,
|
||||
IconTrash,
|
||||
IconWifiOff,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||
@ -31,11 +33,14 @@ import {
|
||||
yjsConnectionStatusAtom,
|
||||
} from "@/features/editor/atoms/editor-atoms.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 {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const toggleAside = useToggleAside();
|
||||
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
|
||||
|
||||
@ -43,7 +48,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||
<>
|
||||
{yjsConnectionStatus === "disconnected" && (
|
||||
<Tooltip
|
||||
label="Real-time editor connection lost. Retrying..."
|
||||
label={t("Real-time editor connection lost. Retrying...")}
|
||||
openDelay={250}
|
||||
withArrow
|
||||
>
|
||||
@ -53,7 +58,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip label="Comments" openDelay={250} withArrow>
|
||||
<Tooltip label={t("Comments")} openDelay={250} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
@ -63,6 +68,16 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||
</ActionIcon>
|
||||
</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} />
|
||||
</>
|
||||
);
|
||||
@ -83,7 +98,12 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
const [tree] = useAtom(treeApiAtom);
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
const [
|
||||
movePageModalOpened,
|
||||
{ open: openMovePageModal, close: closeMoveSpaceModal },
|
||||
] = useDisclosure(false);
|
||||
const [pageEditor] = useAtom(pageEditorAtom);
|
||||
const pageUpdatedAt = useTimeAgo(page.updatedAt);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const pageUrl =
|
||||
@ -147,6 +167,15 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
{!readOnly && (
|
||||
<Menu.Item
|
||||
leftSection={<IconArrowRight size={16} />}
|
||||
onClick={openMovePageModal}
|
||||
>
|
||||
{t("Move")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
onClick={openExportModal}
|
||||
@ -181,7 +210,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
<Tooltip
|
||||
label={t("Edited by {{name}} {{time}}", {
|
||||
name: page.lastUpdatedBy.name,
|
||||
time: timeAgo(page.updatedAt),
|
||||
time: pageUpdatedAt,
|
||||
})}
|
||||
position="left-start"
|
||||
>
|
||||
@ -217,6 +246,14 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
open={exportOpened}
|
||||
onClose={closeExportModal}
|
||||
/>
|
||||
|
||||
<MovePageModal
|
||||
pageId={page.id}
|
||||
slugId={page.slugId}
|
||||
currentSpaceSlug={spaceSlug}
|
||||
onClose={closeMoveSpaceModal}
|
||||
open={movePageModalOpened}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
98
apps/client/src/features/page/components/move-page-modal.tsx
Normal file
98
apps/client/src/features/page/components/move-page-modal.tsx
Normal file
@ -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 {
|
||||
IExportPageParams,
|
||||
IMovePage,
|
||||
IMovePageToSpace,
|
||||
IPage,
|
||||
IPageInput,
|
||||
SidebarPagesParams,
|
||||
@ -34,6 +35,10 @@ export async function movePage(data: IMovePage): Promise<void> {
|
||||
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(
|
||||
params: SidebarPagesParams,
|
||||
): Promise<IPagination<IPage>> {
|
||||
|
||||
@ -7,11 +7,12 @@ import {
|
||||
usePageQuery,
|
||||
useUpdatePageMutation,
|
||||
} 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 classes from "@/features/page/tree/styles/tree.module.css";
|
||||
import { ActionIcon, Menu, rem } from "@mantine/core";
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconDotsVertical,
|
||||
@ -56,6 +57,7 @@ import { extractPageSlugId } from "@/lib";
|
||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
import MovePageModal from "../../components/move-page-modal.tsx";
|
||||
|
||||
interface SpaceTreeProps {
|
||||
spaceId: string;
|
||||
@ -234,6 +236,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
||||
const emit = useQueryEmit();
|
||||
const { spaceSlug } = useParams();
|
||||
const timerRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const prefetchPage = () => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
@ -369,7 +372,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className={classes.text}>{node.data.name || "untitled"}</span>
|
||||
<span className={classes.text}>{node.data.name || t("untitled")}</span>
|
||||
|
||||
<div className={classes.actions}>
|
||||
<NodeMenu node={node} treeApi={tree} />
|
||||
@ -434,6 +437,10 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
||||
const { openDeleteModal } = useDeletePageModal();
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
const [
|
||||
movePageModalOpened,
|
||||
{ open: openMovePageModal, close: closeMoveSpaceModal },
|
||||
] = useDisclosure(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const pageUrl =
|
||||
@ -486,8 +493,18 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
||||
|
||||
{!(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
|
||||
c="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
@ -504,6 +521,14 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<MovePageModal
|
||||
pageId={node.id}
|
||||
slugId={node.data.slugId}
|
||||
currentSpaceSlug={spaceSlug}
|
||||
onClose={closeMoveSpaceModal}
|
||||
open={movePageModalOpened}
|
||||
/>
|
||||
|
||||
<ExportModal
|
||||
type="page"
|
||||
id={node.id}
|
||||
|
||||
@ -42,6 +42,11 @@ export interface IMovePage {
|
||||
parentPageId?: string;
|
||||
}
|
||||
|
||||
export interface IMovePageToSpace {
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export interface SidebarPagesParams {
|
||||
spaceId: string;
|
||||
pageId?: string;
|
||||
|
||||
@ -6,21 +6,33 @@ import { ISpace } from "../../types/space.types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceSelectProps {
|
||||
onChange: (value: string) => void;
|
||||
onChange: (value: ISpace) => void;
|
||||
value?: string;
|
||||
label?: string;
|
||||
width?: number;
|
||||
opened?: boolean;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar color="initials" variant="filled" name={option.label} size={20} />
|
||||
<div>
|
||||
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
export function SpaceSelect({
|
||||
onChange,
|
||||
label,
|
||||
value,
|
||||
width,
|
||||
opened,
|
||||
clearable,
|
||||
}: SpaceSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
@ -42,8 +54,8 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
});
|
||||
|
||||
const filteredSpaceData = spaceData.filter(
|
||||
(user) =>
|
||||
!data.find((existingUser) => existingUser.value === user.value),
|
||||
(space) =>
|
||||
!data.find((existingSpace) => existingSpace.value === space.value),
|
||||
);
|
||||
setData((prevData) => [...prevData, ...filteredSpaceData]);
|
||||
}
|
||||
@ -59,14 +71,18 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
clearable
|
||||
clearable={clearable}
|
||||
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")}
|
||||
limit={50}
|
||||
checkIconPosition="right"
|
||||
comboboxProps={{ width: 300, withinPortal: false }}
|
||||
dropdownOpened
|
||||
comboboxProps={{ width, withinPortal: false }}
|
||||
dropdownOpened={opened}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -55,7 +55,9 @@ export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) {
|
||||
<SpaceSelect
|
||||
label={spaceName}
|
||||
value={spaceSlug}
|
||||
onChange={handleSelect}
|
||||
onChange={space => handleSelect(space.slug)}
|
||||
width={300}
|
||||
opened={true}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</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 { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
@ -106,93 +113,95 @@ export default function SpaceMembersList({
|
||||
return (
|
||||
<>
|
||||
<SearchInput onSearch={handleSearch} />
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("Member")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</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>
|
||||
<ScrollArea h={400}>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("Member")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</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.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</ScrollArea>
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
|
||||
@ -70,7 +70,6 @@ function ChangeEmailForm() {
|
||||
|
||||
function handleSubmit(data: FormValues) {
|
||||
setIsLoading(true);
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
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 { 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 { useTranslation } from "react-i18next";
|
||||
|
||||
@ -26,6 +26,7 @@ interface PageWidthToggleProps {
|
||||
size?: MantineSize;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
@ -50,4 +51,4 @@ export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
aria-label={t("Toggle full page width")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -30,4 +30,4 @@ export interface IUserSettings {
|
||||
preferences: {
|
||||
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 NoTableResults from "@/components/common/no-table-results.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() {
|
||||
const { t } = useTranslation();
|
||||
@ -96,6 +97,9 @@ export default function WorkspaceMembersTable() {
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{isAdmin && <MemberActionMenu userId={user.id} />}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
getWorkspace,
|
||||
getWorkspacePublicData,
|
||||
getAppVersion,
|
||||
deleteWorkspaceMember,
|
||||
} from "@/features/workspace/services/workspace-service";
|
||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||
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() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@ -36,6 +36,12 @@ export async function getWorkspaceMembers(
|
||||
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>) {
|
||||
const req = await api.post<IWorkspace>("/workspace/update", data);
|
||||
return req.data;
|
||||
|
||||
16
apps/client/src/hooks/use-time-ago.tsx
Normal file
16
apps/client/src/hooks/use-time-ago.tsx
Normal 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 AccountTheme from "@/features/user/components/account-theme.tsx";
|
||||
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
import { Divider } from "@mantine/core";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@ -59,14 +59,13 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie": "^1.0.2",
|
||||
"fix-esm": "^1.0.1",
|
||||
"fs-extra": "^11.3.0",
|
||||
"happy-dom": "^15.11.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.27.5",
|
||||
"kysely-migration-cli": "^0.4.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.1.0",
|
||||
"nanoid": "3.3.11",
|
||||
"nestjs-kysely": "^1.1.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
"openid-client": "^5.7.1",
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import {
|
||||
afterUnloadDocumentPayload,
|
||||
Extension,
|
||||
onChangePayload,
|
||||
onLoadDocumentPayload,
|
||||
onStoreDocumentPayload,
|
||||
} from '@hocuspocus/server';
|
||||
@ -26,6 +28,7 @@ import { Page } from '@docmost/db/types/entity.types';
|
||||
@Injectable()
|
||||
export class PersistenceExtension implements Extension {
|
||||
private readonly logger = new Logger(PersistenceExtension.name);
|
||||
private contributors: Map<string, Set<string>> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly pageRepo: PageRepo,
|
||||
@ -116,12 +119,27 @@ export class PersistenceExtension implements Extension {
|
||||
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(
|
||||
{
|
||||
content: tiptapJson,
|
||||
textContent: textContent,
|
||||
ydoc: ydocState,
|
||||
lastUpdatedById: context.user.id,
|
||||
contributorIds: contributorIds,
|
||||
},
|
||||
pageId,
|
||||
trx,
|
||||
@ -152,4 +170,21 @@ export class PersistenceExtension implements Extension {
|
||||
} 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
|
||||
const { customAlphabet } = require('fix-esm').require('nanoid');
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
export const nanoIdGen = customAlphabet(alphabet, 10);
|
||||
|
||||
const slugIdAlphabet =
|
||||
'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) {
|
||||
await this.attachmentService.handleDeleteSpaceAttachments(job.data.id);
|
||||
}
|
||||
if (job.name === QueueJob.DELETE_USER_AVATARS) {
|
||||
await this.attachmentService.handleDeleteUserAvatars(job.data.id);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -281,10 +281,42 @@ export class AttachmentService {
|
||||
}),
|
||||
);
|
||||
|
||||
if(failedDeletions.length === attachments.length){
|
||||
throw new Error(`Failed to delete any attachments for spaceId: ${spaceId}`);
|
||||
if (failedDeletions.length === attachments.length) {
|
||||
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) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -43,19 +43,22 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
async login(loginDto: LoginDto, workspaceId: string) {
|
||||
const user = await this.userRepo.findByEmail(
|
||||
loginDto.email,
|
||||
workspaceId,
|
||||
{
|
||||
includePassword: true
|
||||
}
|
||||
const user = await this.userRepo.findByEmail(loginDto.email, 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 (
|
||||
!user ||
|
||||
!(await comparePasswordHash(loginDto.password, user.password))
|
||||
) {
|
||||
throw new UnauthorizedException('email or password does not match');
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException(errorMessage);
|
||||
}
|
||||
|
||||
user.lastLoginAt = new Date();
|
||||
@ -86,7 +89,7 @@ export class AuthService {
|
||||
includePassword: true,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!user || user.deletedAt) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
@ -125,7 +128,7 @@ export class AuthService {
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
if (!user || user.deletedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -168,7 +171,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const user = await this.userRepo.findById(userToken.userId, workspaceId);
|
||||
if (!user) {
|
||||
if (!user || user.deletedAt) {
|
||||
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 { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import {
|
||||
@ -17,6 +21,10 @@ export class TokenService {
|
||||
) {}
|
||||
|
||||
async generateAccessToken(user: User): Promise<string> {
|
||||
if (user.deletedAt) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-jwt';
|
||||
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);
|
||||
|
||||
if (!user) {
|
||||
if (!user || user.deletedAt) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
|
||||
@ -13,3 +13,11 @@ export class MovePageDto {
|
||||
@IsString()
|
||||
parentPageId?: string | null;
|
||||
}
|
||||
|
||||
export class MovePageToSpaceDto {
|
||||
@IsString()
|
||||
pageId: string;
|
||||
|
||||
@IsString()
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
@ -7,11 +7,12 @@ import {
|
||||
UseGuards,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { PageService } from './services/page.service';
|
||||
import { CreatePageDto } from './dto/create-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 { PageHistoryService } from './services/page-history.service';
|
||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||
@ -46,6 +47,7 @@ export class PageController {
|
||||
includeContent: true,
|
||||
includeCreator: true,
|
||||
includeLastUpdatedBy: true,
|
||||
includeContributors: true,
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
@ -92,11 +94,7 @@ export class PageController {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.pageService.update(
|
||||
updatePageDto.pageId,
|
||||
updatePageDto,
|
||||
user.id,
|
||||
);
|
||||
return this.pageService.update(page, updatePageDto, user.id);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -209,6 +207,36 @@ export class PageController {
|
||||
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)
|
||||
@Post('move')
|
||||
async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) {
|
||||
|
||||
@ -19,11 +19,14 @@ import { MovePageDto } from '../dto/move-page.dto';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { generateSlugId } from '../../../common/helpers';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
||||
|
||||
@Injectable()
|
||||
export class PageService {
|
||||
constructor(
|
||||
private pageRepo: PageRepo,
|
||||
private attachmentRepo: AttachmentRepo,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
) {}
|
||||
|
||||
@ -60,12 +63,31 @@ export class PageService {
|
||||
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;
|
||||
|
||||
const lastPageQuery = this.db
|
||||
.selectFrom('pages')
|
||||
.select(['id', 'position'])
|
||||
.where('spaceId', '=', createPageDto.spaceId)
|
||||
.select(['position'])
|
||||
.where('spaceId', '=', spaceId)
|
||||
.orderBy('position', 'desc')
|
||||
.limit(1);
|
||||
|
||||
@ -96,37 +118,36 @@ export class PageService {
|
||||
}
|
||||
}
|
||||
|
||||
const createdPage = await this.pageRepo.insertPage({
|
||||
slugId: generateSlugId(),
|
||||
title: createPageDto.title,
|
||||
position: pagePosition,
|
||||
icon: createPageDto.icon,
|
||||
parentPageId: parentPageId,
|
||||
spaceId: createPageDto.spaceId,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
lastUpdatedById: userId,
|
||||
});
|
||||
|
||||
return createdPage;
|
||||
return pagePosition;
|
||||
}
|
||||
|
||||
async update(
|
||||
pageId: string,
|
||||
page: Page,
|
||||
updatePageDto: UpdatePageDto,
|
||||
userId: string,
|
||||
): Promise<Page> {
|
||||
const contributors = new Set<string>(page.contributorIds);
|
||||
contributors.add(userId);
|
||||
const contributorIds = Array.from(contributors);
|
||||
|
||||
await this.pageRepo.updatePage(
|
||||
{
|
||||
title: updatePageDto.title,
|
||||
icon: updatePageDto.icon,
|
||||
lastUpdatedById: userId,
|
||||
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'>) {
|
||||
@ -181,6 +202,36 @@ export class PageService {
|
||||
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) {
|
||||
// validate position value by attempting to generate a key
|
||||
try {
|
||||
|
||||
@ -84,6 +84,7 @@ export class SearchService {
|
||||
.select(['id', 'name', 'avatarUrl'])
|
||||
.where((eb) => eb(sql`LOWER(users.name)`, 'like', `%${query}%`))
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('deletedAt', 'is', null)
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(
|
||||
OmitType(CreateUserDto, ['password'] as const),
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@ -27,8 +27,9 @@ export class UserService {
|
||||
|
||||
// preference update
|
||||
if (typeof updateUserDto.fullPageWidth !== 'undefined') {
|
||||
return this.updateUserPageWidthPreference(
|
||||
return this.userRepo.updatePreference(
|
||||
userId,
|
||||
'fullPageWidth',
|
||||
updateUserDto.fullPageWidth,
|
||||
);
|
||||
}
|
||||
@ -55,12 +56,4 @@ export class UserService {
|
||||
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
||||
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 { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { CheckHostnameDto } from '../dto/check-hostname.dto';
|
||||
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@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)
|
||||
@Post('members/change-role')
|
||||
async updateWorkspaceMemberRole(
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
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 { addDays } from 'date-fns';
|
||||
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()
|
||||
export class WorkspaceService {
|
||||
private readonly logger = new Logger(WorkspaceService.name);
|
||||
|
||||
constructor(
|
||||
private workspaceRepo: WorkspaceRepo,
|
||||
private spaceService: SpaceService,
|
||||
@ -39,6 +47,8 @@ export class WorkspaceService {
|
||||
private environmentService: EnvironmentService,
|
||||
private domainService: DomainService,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
|
||||
) {}
|
||||
|
||||
async findById(workspaceId: string) {
|
||||
@ -91,13 +101,15 @@ export class WorkspaceService {
|
||||
createWorkspaceDto: CreateWorkspaceDto,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return await executeTx(
|
||||
let trialEndAt = undefined;
|
||||
|
||||
const createdWorkspace = await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
let hostname = undefined;
|
||||
let trialEndAt = undefined;
|
||||
let status = undefined;
|
||||
let plan = undefined;
|
||||
let billingEmail = undefined;
|
||||
|
||||
if (this.environmentService.isCloud()) {
|
||||
// generate unique hostname
|
||||
@ -110,6 +122,7 @@ export class WorkspaceService {
|
||||
);
|
||||
status = WorkspaceStatus.Active;
|
||||
plan = 'standard';
|
||||
billingEmail = user.email;
|
||||
}
|
||||
|
||||
// create workspace
|
||||
@ -121,6 +134,7 @@ export class WorkspaceService {
|
||||
status,
|
||||
trialEndAt,
|
||||
plan,
|
||||
billingEmail,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
@ -195,6 +209,28 @@ export class WorkspaceService {
|
||||
},
|
||||
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(
|
||||
@ -386,4 +422,66 @@ export class WorkspaceService {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
|
||||
log: (event: LogEvent) => {
|
||||
if (environmentService.getNodeEnv() !== 'development') return;
|
||||
const logger = new Logger(DatabaseModule.name);
|
||||
if (event.level === 'query') {
|
||||
if (event.level) {
|
||||
if (process.env.DEBUG_DB?.toLowerCase() === 'true') {
|
||||
logger.debug(event.query.sql);
|
||||
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;
|
||||
page: number;
|
||||
experimental_deferredJoinPrimaryKey?: StringReference<DB, TB>;
|
||||
hasEmptyIds?: boolean; // in cases where we pass empty whereIn ids
|
||||
},
|
||||
): Promise<PaginationResult<O>> {
|
||||
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'))
|
||||
.execute()
|
||||
// @ts-expect-error TODO: Fix the type here later
|
||||
|
||||
|
||||
.then((rows) => rows.map((row) => row.primaryKey));
|
||||
|
||||
qb = qb
|
||||
.where((eb) =>
|
||||
primaryKeys.length > 0
|
||||
?
|
||||
eb(deferredJoinPrimaryKey, 'in', primaryKeys as any)
|
||||
? eb(deferredJoinPrimaryKey, 'in', primaryKeys as any)
|
||||
: eb(sql`1`, '=', 0),
|
||||
)
|
||||
.clearOffset()
|
||||
.clearLimit();
|
||||
}
|
||||
|
||||
const rows = await qb.execute();
|
||||
const rows = opts.hasEmptyIds ? [] : await qb.execute();
|
||||
const hasNextPage = rows.length > 0 ? rows.length > opts.perPage : false;
|
||||
const hasPrevPage = rows.length > 0 ? opts.page > 1 : false;
|
||||
|
||||
|
||||
@ -55,6 +55,18 @@ export class AttachmentRepo {
|
||||
.execute();
|
||||
}
|
||||
|
||||
updateAttachmentsByPageId(
|
||||
updatableAttachment: UpdatableAttachment,
|
||||
pageIds: string[],
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return dbOrTx(this.db, trx)
|
||||
.updateTable('attachments')
|
||||
.set(updatableAttachment)
|
||||
.where('pageId', 'in', pageIds)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateAttachment(
|
||||
updatableAttachment: UpdatableAttachment,
|
||||
attachmentId: string,
|
||||
|
||||
@ -10,9 +10,9 @@ import {
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
@ -38,6 +38,7 @@ export class PageRepo {
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'contributorIds',
|
||||
];
|
||||
|
||||
async findById(
|
||||
@ -48,6 +49,7 @@ export class PageRepo {
|
||||
includeSpace?: boolean;
|
||||
includeCreator?: boolean;
|
||||
includeLastUpdatedBy?: boolean;
|
||||
includeContributors?: boolean;
|
||||
withLock?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
@ -68,6 +70,10 @@ export class PageRepo {
|
||||
query = query.select((eb) => this.withLastUpdatedBy(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeContributors) {
|
||||
query = query.select((eb) => this.withContributors(eb));
|
||||
}
|
||||
|
||||
if (opts?.includeSpace) {
|
||||
query = query.select((eb) => this.withSpace(eb));
|
||||
}
|
||||
@ -90,18 +96,23 @@ export class PageRepo {
|
||||
pageId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
let query = db
|
||||
return this.updatePages(updatablePage, [pageId], trx);
|
||||
}
|
||||
|
||||
async updatePages(
|
||||
updatePageData: UpdatablePage,
|
||||
pageIds: string[],
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return dbOrTx(this.db, trx)
|
||||
.updateTable('pages')
|
||||
.set({ ...updatablePage, updatedAt: new Date() });
|
||||
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
return query.executeTakeFirst();
|
||||
.set({ ...updatePageData, updatedAt: new Date() })
|
||||
.where(
|
||||
pageIds.some((pageId) => !isValidUUID(pageId)) ? 'slugId' : 'id',
|
||||
'in',
|
||||
pageIds,
|
||||
)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertPage(
|
||||
@ -154,9 +165,11 @@ export class PageRepo {
|
||||
.where('spaceId', 'in', userSpaceIds)
|
||||
.orderBy('updatedAt', 'desc');
|
||||
|
||||
const hasEmptyIds = userSpaceIds.length === 0;
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
hasEmptyIds,
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -189,6 +202,15 @@ export class PageRepo {
|
||||
).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) {
|
||||
return this.db
|
||||
.withRecursive('page_hierarchy', (db) =>
|
||||
|
||||
@ -114,6 +114,7 @@ export class SpaceMemberRepo {
|
||||
])
|
||||
.select((eb) => this.groupRepo.withMemberCount(eb))
|
||||
.where('spaceId', '=', spaceId)
|
||||
.orderBy((eb) => eb('groups.id', 'is not', null), 'desc')
|
||||
.orderBy('spaceMembers.createdAt', 'asc');
|
||||
|
||||
if (pagination.query) {
|
||||
@ -221,7 +222,7 @@ export class SpaceMemberRepo {
|
||||
|
||||
let query = this.db
|
||||
.selectFrom('spaces')
|
||||
.selectAll('spaces')
|
||||
.selectAll()
|
||||
.select((eb) => [this.spaceRepo.withMemberCount(eb)])
|
||||
//.where('workspaceId', '=', workspaceId)
|
||||
.where('id', 'in', userSpaceIds)
|
||||
@ -237,9 +238,12 @@ export class SpaceMemberRepo {
|
||||
);
|
||||
}
|
||||
|
||||
const hasEmptyIds = userSpaceIds.length === 0;
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
hasEmptyIds,
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@ -139,6 +139,7 @@ export class UserRepo {
|
||||
.selectFrom('users')
|
||||
.select(this.baseFields)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
if (pagination.query) {
|
||||
|
||||
1
apps/server/src/database/types/db.d.ts
vendored
1
apps/server/src/database/types/db.d.ts
vendored
@ -161,6 +161,7 @@ export interface PageHistory {
|
||||
|
||||
export interface Pages {
|
||||
content: Json | null;
|
||||
contributorIds: Generated<string[] | null>;
|
||||
coverPhoto: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
creatorId: string | null;
|
||||
|
||||
Submodule apps/server/src/ee updated: 337854ce96...d3095f2d8b
@ -21,7 +21,7 @@ import {
|
||||
getProsemirrorContent,
|
||||
PageExportTree,
|
||||
replaceInternalLinks,
|
||||
updateAttachmentUrls,
|
||||
updateAttachmentUrlsToLocalPaths,
|
||||
} from './utils';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { Node } from '@tiptap/pm/model';
|
||||
@ -193,7 +193,7 @@ export class ExportService {
|
||||
|
||||
if (includeAttachments) {
|
||||
await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
|
||||
updatedJsonContent = updateAttachmentUrls(updatedJsonContent);
|
||||
updatedJsonContent = updateAttachmentUrlsToLocalPaths(updatedJsonContent);
|
||||
}
|
||||
|
||||
const pageTitle = getPageTitle(page.title);
|
||||
|
||||
@ -79,16 +79,18 @@ function preserveDetail(turndownService: TurndownService) {
|
||||
return node.nodeName === 'DETAILS';
|
||||
},
|
||||
replacement: function (content: any, node: HTMLInputElement) {
|
||||
// TODO: preserve summary of nested details
|
||||
const summary = node.querySelector(':scope > summary');
|
||||
let detailSummary = '';
|
||||
|
||||
if (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`;
|
||||
},
|
||||
});
|
||||
|
||||
@ -62,17 +62,30 @@ export function isAttachmentNode(nodeType: string) {
|
||||
return attachmentNodeTypes.includes(nodeType);
|
||||
}
|
||||
|
||||
export function updateAttachmentUrls(prosemirrorJson: any) {
|
||||
export function updateAttachmentUrlsToLocalPaths(prosemirrorJson: any) {
|
||||
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) => {
|
||||
if (isAttachmentNode(node.type.name)) {
|
||||
if (node.attrs.src && node.attrs.src.startsWith('/files')) {
|
||||
//@ts-expect-error
|
||||
node.attrs.src = node.attrs.src.replace('/files', 'files');
|
||||
} else if (node.attrs.url && node.attrs.url.startsWith('/files')) {
|
||||
//@ts-expect-error
|
||||
node.attrs.url = node.attrs.url.replace('/files', 'files');
|
||||
if (node.attrs.src) {
|
||||
// @ts-ignore
|
||||
node.attrs.src = replacePrefix(node.attrs.src);
|
||||
}
|
||||
if (node.attrs.url) {
|
||||
// @ts-ignore
|
||||
node.attrs.url = replacePrefix(node.attrs.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -19,18 +19,27 @@ export class MailService {
|
||||
async sendEmail(message: MailMessage): Promise<void> {
|
||||
if (message.template) {
|
||||
// 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 });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
async sendToQueue(message: MailMessage): Promise<void> {
|
||||
if (message.template) {
|
||||
// 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, {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
@ -11,7 +11,12 @@ export enum QueueJob {
|
||||
DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments',
|
||||
PAGE_CONTENT_UPDATE = 'page-content-update',
|
||||
|
||||
DELETE_USER_AVATARS = 'delete-user-avatars',
|
||||
|
||||
PAGE_BACKLINKS = 'page-backlinks',
|
||||
|
||||
STRIPE_SEATS_SYNC = 'sync-stripe-seats',
|
||||
TRIAL_ENDED = 'trial-ended',
|
||||
WELCOME_EMAIL = 'welcome-email',
|
||||
FIRST_PAYMENT_EMAIL = 'first-payment-email',
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "docmost",
|
||||
"homepage": "https://docmost.com",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nx run-many -t build",
|
||||
@ -14,7 +14,7 @@
|
||||
"client:dev": "nx run client:dev",
|
||||
"server:dev": "nx run server:start:dev",
|
||||
"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\""
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -16,4 +16,4 @@ export * from "./lib/drawio";
|
||||
export * from "./lib/excalidraw";
|
||||
export * from "./lib/embed";
|
||||
export * from "./lib/mention";
|
||||
export * from "./lib/markdown";
|
||||
export * from "./lib/markdown";
|
||||
@ -78,10 +78,13 @@ export const Details = Node.create<DetailsOptions>({
|
||||
dom.setAttribute("data-type", this.name);
|
||||
btn.setAttribute("data-type", `${this.name}Button`);
|
||||
div.setAttribute("data-type", `${this.name}Container`);
|
||||
if (node.attrs.open) {
|
||||
dom.setAttribute("open", "true");
|
||||
} else {
|
||||
dom.removeAttribute("open");
|
||||
|
||||
if (editor.isEditable) {
|
||||
if (node.attrs.open) {
|
||||
dom.setAttribute("open", "true");
|
||||
} else {
|
||||
dom.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
ico.innerHTML = icon("right-line");
|
||||
@ -111,6 +114,7 @@ export const Details = Node.create<DetailsOptions>({
|
||||
if (updatedNode.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
if (!editor.isEditable) return true;
|
||||
if (updatedNode.attrs.open) {
|
||||
dom.setAttribute("open", "true");
|
||||
} else {
|
||||
@ -132,6 +136,10 @@ export const Details = Node.create<DetailsOptions>({
|
||||
}
|
||||
|
||||
const slice = state.doc.slice(range.start, range.end);
|
||||
|
||||
if (slice.content.firstChild.type.name === "detailsSummary")
|
||||
return false;
|
||||
|
||||
if (
|
||||
!state.schema.nodes.detailsContent.contentMatch.matchFragment(
|
||||
slice.content,
|
||||
|
||||
346
pnpm-lock.yaml
generated
346
pnpm-lock.yaml
generated
@ -272,6 +272,9 @@ importers:
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
jwt-decode:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
katex:
|
||||
specifier: 0.16.21
|
||||
version: 0.16.21
|
||||
@ -483,9 +486,6 @@ importers:
|
||||
cookie:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
fix-esm:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
fs-extra:
|
||||
specifier: ^11.3.0
|
||||
version: 11.3.0
|
||||
@ -505,8 +505,8 @@ importers:
|
||||
specifier: ^2.1.35
|
||||
version: 2.1.35
|
||||
nanoid:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
specifier: 3.3.11
|
||||
version: 3.3.11
|
||||
nestjs-kysely:
|
||||
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)
|
||||
@ -630,7 +630,7 @@ importers:
|
||||
version: 7.0.0
|
||||
ts-jest:
|
||||
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:
|
||||
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)))
|
||||
@ -872,18 +872,10 @@ packages:
|
||||
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/core@7.24.3':
|
||||
resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/core@7.24.5':
|
||||
resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -916,10 +908,6 @@ packages:
|
||||
resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
|
||||
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':
|
||||
resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -949,18 +937,10 @@ packages:
|
||||
resolution: {integrity: sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==}
|
||||
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':
|
||||
resolution: {integrity: sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==}
|
||||
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':
|
||||
resolution: {integrity: sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1071,10 +1051,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1083,10 +1059,6 @@ packages:
|
||||
resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helpers@7.24.1':
|
||||
resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helpers@7.24.6':
|
||||
resolution: {integrity: sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1099,11 +1071,6 @@ packages:
|
||||
resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -1143,13 +1110,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@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':
|
||||
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1615,14 +1575,6 @@ packages:
|
||||
resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1631,10 +1583,6 @@ packages:
|
||||
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -4753,11 +4701,6 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@ -4819,9 +4762,6 @@ packages:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
caniuse-lite@1.0.30001600:
|
||||
resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==}
|
||||
|
||||
caniuse-lite@1.0.30001684:
|
||||
resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
|
||||
|
||||
@ -5460,9 +5400,6 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-to-chromium@1.4.715:
|
||||
resolution: {integrity: sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==}
|
||||
|
||||
electron-to-chromium@1.5.65:
|
||||
resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==}
|
||||
|
||||
@ -5796,9 +5733,6 @@ packages:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fix-esm@1.0.1:
|
||||
resolution: {integrity: sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
@ -6612,6 +6546,10 @@ packages:
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
|
||||
jwt-decode@4.0.0:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
katex@0.16.21:
|
||||
resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
|
||||
hasBin: true
|
||||
@ -7013,21 +6951,16 @@ packages:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
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}
|
||||
hasBin: true
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
@ -7097,9 +7030,6 @@ packages:
|
||||
node-machine-id@1.1.12:
|
||||
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||
|
||||
node-releases@2.0.14:
|
||||
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
|
||||
|
||||
node-releases@2.0.18:
|
||||
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
|
||||
|
||||
@ -8646,12 +8576,6 @@ packages:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
|
||||
hasBin: true
|
||||
@ -9672,30 +9596,8 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.0.1
|
||||
|
||||
'@babel/compat-data@7.23.5': {}
|
||||
|
||||
'@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':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
@ -9786,14 +9688,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.26.2
|
||||
@ -9837,20 +9731,11 @@ snapshots:
|
||||
|
||||
'@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':
|
||||
dependencies:
|
||||
'@babel/template': 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':
|
||||
dependencies:
|
||||
'@babel/types': 7.24.6
|
||||
@ -9874,15 +9759,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
@ -9975,8 +9851,6 @@ snapshots:
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@babel/helper-validator-option@7.23.5': {}
|
||||
|
||||
'@babel/helper-validator-option@7.25.9': {}
|
||||
|
||||
'@babel/helper-wrap-function@7.22.20':
|
||||
@ -9985,14 +9859,6 @@ snapshots:
|
||||
'@babel/template': 7.25.9
|
||||
'@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':
|
||||
dependencies:
|
||||
'@babel/template': 7.25.9
|
||||
@ -10010,10 +9876,6 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.0.0
|
||||
|
||||
'@babel/parser@7.24.1':
|
||||
dependencies:
|
||||
'@babel/types': 7.24.0
|
||||
|
||||
'@babel/parser@7.24.5':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.0
|
||||
@ -10051,19 +9913,13 @@ snapshots:
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10077,9 +9933,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10088,9 +9944,9 @@ snapshots:
|
||||
'@babel/core': 7.24.6
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10119,11 +9975,6 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
@ -10139,9 +9990,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10155,9 +10006,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10181,9 +10032,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10197,9 +10048,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10213,9 +10064,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10229,9 +10080,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10245,9 +10096,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10261,9 +10112,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10282,9 +10133,9 @@ snapshots:
|
||||
'@babel/core': 7.26.0
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.25.9
|
||||
optional: true
|
||||
|
||||
@ -10454,13 +10305,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.0
|
||||
@ -10763,18 +10607,6 @@ snapshots:
|
||||
dependencies:
|
||||
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':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
@ -10787,21 +10619,6 @@ snapshots:
|
||||
'@babel/parser': 7.26.2
|
||||
'@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':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
@ -13996,13 +13813,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
babel-jest@29.7.0(@babel/core@7.24.3):
|
||||
babel-jest@29.7.0(@babel/core@7.24.5):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@jest/transform': 29.7.0
|
||||
'@types/babel__core': 7.20.5
|
||||
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
|
||||
graceful-fs: 4.2.11
|
||||
slash: 3.0.0
|
||||
@ -14086,21 +13903,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3)
|
||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3)
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5)
|
||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.5)
|
||||
'@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.5)
|
||||
'@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.5)
|
||||
'@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.5)
|
||||
'@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.5)
|
||||
'@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.5)
|
||||
optional: true
|
||||
|
||||
babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.6):
|
||||
@ -14119,11 +13936,11 @@ snapshots:
|
||||
'@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-preset-jest@29.6.3(@babel/core@7.24.3):
|
||||
babel-preset-jest@29.6.3(@babel/core@7.24.5):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
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
|
||||
|
||||
babel-preset-jest@29.6.3(@babel/core@7.24.6):
|
||||
@ -14188,13 +14005,6 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001684
|
||||
@ -14266,8 +14076,6 @@ snapshots:
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001600: {}
|
||||
|
||||
caniuse-lite@1.0.30001684: {}
|
||||
|
||||
chalk@2.4.2:
|
||||
@ -14914,8 +14722,6 @@ snapshots:
|
||||
dependencies:
|
||||
jake: 10.8.7
|
||||
|
||||
electron-to-chromium@1.4.715: {}
|
||||
|
||||
electron-to-chromium@1.5.65: {}
|
||||
|
||||
emittery@0.13.1: {}
|
||||
@ -15469,14 +15275,6 @@ snapshots:
|
||||
locate-path: 6.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:
|
||||
dependencies:
|
||||
flatted: 3.2.9
|
||||
@ -16534,6 +16332,8 @@ snapshots:
|
||||
jwa: 1.4.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jwt-decode@4.0.0: {}
|
||||
|
||||
katex@0.16.21:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
@ -16904,12 +16704,10 @@ snapshots:
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
nanoid@5.1.0: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
needle@3.3.1:
|
||||
@ -16974,8 +16772,6 @@ snapshots:
|
||||
|
||||
node-machine-id@1.1.12: {}
|
||||
|
||||
node-releases@2.0.14: {}
|
||||
|
||||
node-releases@2.0.18: {}
|
||||
|
||||
nodemailer@6.10.0: {}
|
||||
@ -17415,7 +17211,7 @@ snapshots:
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@ -17427,7 +17223,7 @@ snapshots:
|
||||
|
||||
postcss@8.5.2:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@ -18477,7 +18273,7 @@ snapshots:
|
||||
|
||||
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:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -18491,10 +18287,10 @@ snapshots:
|
||||
typescript: 5.7.3
|
||||
yargs-parser: 21.1.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/core': 7.24.5
|
||||
'@jest/transform': 29.7.0
|
||||
'@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))):
|
||||
dependencies:
|
||||
@ -18678,12 +18474,6 @@ snapshots:
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
browserslist: 4.24.2
|
||||
|
||||
Reference in New Issue
Block a user