From 895c1817ae13a3b47bd0d25a09e226964f0aa828 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:45:36 +0100 Subject: [PATCH 01/22] feat: bug fixes (#2084) * handle enter in inline code * fix: duplicate comment cache * track link nodes (backlinks) * fix en-US translation * fix internal a-links * overrides * 0.71.1 --- apps/client/package.json | 2 +- .../public/locales/en-US/translation.json | 518 +++++++++--------- .../features/comment/queries/comment-query.ts | 5 + .../features/editor/extensions/extensions.ts | 19 + apps/server/package.json | 2 +- .../processors/history.processor.ts | 3 + .../src/common/helpers/prosemirror/utils.ts | 25 + .../src/core/page/services/page.service.ts | 31 ++ .../queue/constants/queue.interface.ts | 1 + .../queue/tasks/backlinks.task.ts | 33 +- package.json | 9 +- pnpm-lock.yaml | 433 +++++++-------- 12 files changed, 593 insertions(+), 488 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 4b767f6041..22192e56ef 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.71.0", + "version": "0.71.1", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 171ba7b910..3ecf103bd0 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1,7 +1,7 @@ { - "Account": "Account ", + "Account": "Account", "Active": "Active", - "Add": "Add.", + "Add": "Add", "Add group members": "Add group members", "Add groups": "Add groups", "Add members": "Add members", @@ -44,24 +44,24 @@ "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.", "Description": "Description", "Details": "Details", - "e.g ACME": "e.g. ACME", - "e.g ACME Inc": "e.g. ACME Inc", - "e.g Developers": "e.g. Developers", - "e.g Group for developers": "e.g. Group for developers", - "e.g product": "e.g. product", - "e.g Product Team": "e.g. Product Team", - "e.g Sales": "e.g. Sales", - "e.g Space for product team": "e.g. Space for product team", - "e.g Space for sales team to collaborate": "e.g. Space for sales team to collaborate", + "e.g ACME": "e.g ACME", + "e.g ACME Inc": "e.g ACME Inc", + "e.g Developers": "e.g Developers", + "e.g Group for developers": "e.g Group for developers", + "e.g product": "e.g product", + "e.g Product Team": "e.g Product Team", + "e.g Sales": "e.g Sales", + "e.g Space for product team": "e.g Space for product team", + "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate", "Edit": "Edit", - "Read": "Read.", + "Read": "Read", "Edit group": "Edit group", "Email": "Email", "Enter a strong password": "Enter a strong password", "Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]", - "enter valid emails addresses": "Enter valid email addresses", + "enter valid emails addresses": "enter valid emails addresses", "Enter your current password": "Enter your current password", - "enter your full name": "Enter your full name", + "enter your full name": "enter your full name", "Enter your new password": "Enter your new password", "Enter your new preferred email": "Enter your new preferred email", "Enter your password": "Enter your password", @@ -87,7 +87,7 @@ "Import pages": "Import pages", "Import pages & space settings": "Import pages & space settings", "Importing pages": "Importing pages", - "invalid invitation link": "Invalid invitation link", + "invalid invitation link": "invalid invitation link", "Invitation signup": "Invitation signup", "Invite by email": "Invite by email", "Invite members": "Invite members", @@ -113,7 +113,7 @@ "New email": "New email", "New page": "New page", "New password": "New password", - "No group found": "No group found.", + "No group found": "No group found", "No page history saved yet.": "No page history saved yet.", "No pages yet": "No pages yet", "No shared pages": "No shared pages", @@ -149,56 +149,56 @@ "Search for users": "Search for users", "Search for users and groups": "Search for users and groups", "Search...": "Search...", - "Select language": "Select language.", - "Select role": "Select role.", - "Select role to assign to all invited members": "Select role to assign to all invited members.", - "Select theme": "Select theme.", - "Send invitation": "Send invitation.", - "Invitation sent": "Invitation sent.", - "Settings": "Settings.", - "Setup workspace": "Setup workspace.", - "Sign In": "Sign In.", - "Sign Up": "Sign Up.", - "Slug": "Slug.", - "Space": "Space.", - "Space description": "Space description.", - "Space menu": "Space menu.", - "Space name": "Space name.", - "Space settings": "Space settings.", - "Space slug": "Space slug.", - "Spaces": "Spaces.", - "Spaces you belong to": "Spaces you belong to.", - "No space found": "No space found.", - "Search for spaces": "Search for spaces.", + "Select language": "Select language", + "Select role": "Select role", + "Select role to assign to all invited members": "Select role to assign to all invited members", + "Select theme": "Select theme", + "Send invitation": "Send invitation", + "Invitation sent": "Invitation sent", + "Settings": "Settings", + "Setup workspace": "Setup workspace", + "Sign In": "Sign In", + "Sign Up": "Sign Up", + "Slug": "Slug", + "Space": "Space", + "Space description": "Space description", + "Space menu": "Space menu", + "Space name": "Space name", + "Space settings": "Space settings", + "Space slug": "Space slug", + "Spaces": "Spaces", + "Spaces you belong to": "Spaces you belong to", + "No space found": "No space found", + "Search for spaces": "Search for spaces", "Start typing to search...": "Start typing to search...", - "Status": "Status.", - "Successfully imported": "Successfully imported.", - "Successfully restored": "Successfully restored.", - "System settings": "System settings.", - "Theme": "Theme.", + "Status": "Status", + "Successfully imported": "Successfully imported", + "Successfully restored": "Successfully restored", + "System settings": "System settings", + "Theme": "Theme", "To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.", - "Toggle full page width": "Toggle full page width.", + "Toggle full page width": "Toggle full page width", "Unable to import pages. Please try again.": "Unable to import pages. Please try again.", - "untitled": "untitled.", - "Untitled": "Untitled.", - "Updated successfully": "Updated successfully.", - "User": "User.", - "Workspace": "Workspace.", - "Workspace Name": "Workspace name.", - "Workspace settings": "Workspace settings.", + "untitled": "untitled", + "Untitled": "Untitled", + "Updated successfully": "Updated successfully", + "User": "User", + "Workspace": "Workspace", + "Workspace Name": "Workspace Name", + "Workspace settings": "Workspace settings", "You can change your password here.": "You can change your password here.", - "Your Email": "Your email.", + "Your Email": "Your Email", "Your import is complete.": "Your import is complete.", - "Your name": "Your name.", - "Your Name": "Your name.", - "Your password": "Your password.", + "Your name": "Your name", + "Your Name": "Your Name", + "Your password": "Your password", "Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.", - "Sidebar toggle": "Sidebar toggle.", - "Comments": "Comments.", - "404 page not found": "404 page not found.", + "Sidebar toggle": "Sidebar toggle", + "Comments": "Comments", + "404 page not found": "404 page not found", "Sorry, we can't find the page you are looking for.": "Sorry, we can't find the page you are looking for.", - "Take me back to homepage": "Take me back to the homepage.", - "Forgot password": "Forgot password.", + "Take me back to homepage": "Take me back to homepage", + "Forgot password": "Forgot password", "Forgot your password?": "Forgot your password?", "A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.", "Send reset link": "Send reset link", @@ -222,16 +222,16 @@ "Comment deleted successfully": "Comment deleted successfully", "Failed to delete comment": "Failed to delete comment", "Comment resolved successfully": "Comment resolved successfully", - "Comment re-opened successfully": "Comment re-opened successfully.", - "Comment unresolved successfully": "Comment marked as unresolved successfully.", + "Comment re-opened successfully": "Comment re-opened successfully", + "Comment unresolved successfully": "Comment unresolved successfully", "Failed to resolve comment": "Failed to resolve comment", - "Resolve comment": "Resolve comment.", - "Unresolve comment": "Mark comment as unresolved.", - "Resolve Comment Thread": "Resolve comment thread.", - "Unresolve Comment Thread": "Mark comment thread as unresolved.", + "Resolve comment": "Resolve comment", + "Unresolve comment": "Unresolve comment", + "Resolve Comment Thread": "Resolve Comment Thread", + "Unresolve Comment Thread": "Unresolve Comment Thread", "Are you sure you want to resolve this comment thread? This will mark it as completed.": "Are you sure you want to resolve this comment thread? This will mark it as completed.", "Are you sure you want to unresolve this comment thread?": "Are you sure you want to unresolve this comment thread?", - "Resolved": "Resolved.", + "Resolved": "Resolved", "No active comments.": "No active comments.", "Revoke invitation": "Revoke invitation", "Revoke": "Revoke", @@ -241,9 +241,9 @@ "Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.", "Invite link": "Invite link", "Copy": "Copy", - "Copy to space": "Copy to space.", + "Copy to space": "Copy to space", "Copied": "Copied", - "Duplicate": "Duplicate.", + "Duplicate": "Duplicate", "Select a user": "Select a user", "Select a group": "Select a group", "Export all pages and attachments in this space.": "Export all pages and attachments in this space.", @@ -251,7 +251,7 @@ "Are you sure you want to delete this space?": "Are you sure you want to delete this space?", "Delete this space with all its pages and data.": "Delete this space with all its pages and data.", "All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.", - "Confirm space name": "Confirm space name.", + "Confirm space name": "Confirm space name", "Type the space name {{spaceName}} to confirm your action.": "Type the space name {{spaceName}} to confirm your action.", "Format": "Format", "Include subpages": "Include subpages", @@ -267,7 +267,7 @@ "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", - "Justify": "Justify.", + "Justify": "Justify", "Merge cells": "Merge cells", "Split cell": "Split cell", "Delete column": "Delete column", @@ -312,7 +312,7 @@ "Pink": "Pink", "Gray": "Gray", "Embed link": "Embed link", - "Invalid {{provider}} embed link": "Invalid {{provider}} embed link.", + "Invalid {{provider}} embed link": "Invalid {{provider}} embed link", "Embed {{provider}}": "Embed {{provider}}", "Enter {{provider}} link to embed": "Enter {{provider}} link to embed", "Bold": "Bold", @@ -345,41 +345,41 @@ "Upload any file from your device.": "Upload any file from your device.", "Uploading {{name}}": "Uploading {{name}}", "Uploading file": "Uploading file", - "Table": "Table.", + "Table": "Table", "Insert a table.": "Insert a table.", "Insert collapsible block.": "Insert collapsible block.", - "Video": "Video.", - "Divider": "Divider.", - "Quote": "Quote.", - "Image": "Image.", - "Audio": "Audio.", + "Video": "Video", + "Divider": "Divider", + "Quote": "Quote", + "Image": "Image", + "Audio": "Audio", "Embed PDF": "Embed PDF", "Upload and embed a PDF file.": "Upload and embed a PDF file.", "Embed as PDF": "Embed as PDF", "Failed to load PDF": "Failed to load PDF", "Convert to attachment": "Convert to attachment", - "File attachment": "File attachment.", - "Toggle block": "Toggle block.", - "Callout": "Callout.", + "File attachment": "File attachment", + "Toggle block": "Toggle block", + "Callout": "Callout", "Insert callout notice.": "Insert callout notice.", - "Math inline": "Math inline.", + "Math inline": "Math inline", "Insert inline math equation.": "Insert inline math equation.", - "Math block": "Math block.", - "Insert math equation": "Insert math equation.", - "Mermaid diagram": "Mermaid diagram.", - "Insert mermaid diagram": "Insert mermaid diagram.", - "Insert and design Drawio diagrams": "Insert and design Drawio diagrams.", - "Insert current date": "Insert current date.", - "Draw and sketch excalidraw diagrams": "Draw and sketch Excalidraw diagrams.", - "Multiple": "Multiple.", + "Math block": "Math block", + "Insert math equation": "Insert math equation", + "Mermaid diagram": "Mermaid diagram", + "Insert mermaid diagram": "Insert mermaid diagram", + "Insert and design Drawio diagrams": "Insert and design Drawio diagrams", + "Insert current date": "Insert current date", + "Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams", + "Multiple": "Multiple", "Turn into": "Turn into", "Text align": "Text align", "This page may have been deleted, moved, or you may not have access.": "This page may have been deleted, moved, or you may not have access.", "Go to homepage": "Go to homepage", "Pages you create will show up here.": "Pages you create will show up here.", - "Heading {{level}}": "Heading {{level}}.", - "Toggle title": "Toggle title.", - "Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands.", + "Heading {{level}}": "Heading {{level}}", + "Toggle title": "Toggle title", + "Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands", "Write...": "Write...", "Column count": "Column count", "{{count}} Columns": "{{count}} Columns", @@ -389,27 +389,27 @@ "Wide center": "Wide center", "Left wide": "Left wide", "Right wide": "Right wide", - "Names do not match": "Names do not match.", - "Today, {{time}}": "Today, {{time}}.", - "Yesterday, {{time}}": "Yesterday, {{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: {{creatorName}}": "Created by: {{creatorName}}.", - "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}}.", - "New update": "New update.", - "{{latestVersion}} is available": "{{latestVersion}} is available.", - "Default page edit mode": "Default page edit mode.", + "Names do not match": "Names do not match", + "Today, {{time}}": "Today, {{time}}", + "Yesterday, {{time}}": "Yesterday, {{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: {{creatorName}}": "Created by: {{creatorName}}", + "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}}", + "New update": "New update", + "{{latestVersion}} is available": "{{latestVersion}} is available", + "Default page edit mode": "Default page edit mode", "Choose your preferred page edit mode. Avoid accidental edits.": "Choose your preferred page edit mode. Avoid accidental edits.", - "Reading": "Reading.", - "Delete member": "Delete member.", - "Member deleted successfully": "Member deleted successfully.", + "Reading": "Reading", + "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.", "Deactivate member": "Deactivate member", "Activate member": "Activate member", @@ -418,33 +418,33 @@ "Deactivate": "Deactivate", "Activate": "Activate", "Deactivated": "Deactivated", - "Move": "Move.", - "Move page": "Move page.", + "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.", + "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.", - "Share": "Share.", - "Public sharing": "Public sharing.", - "Shared by": "Shared by.", - "Shared at": "Shared at.", - "Inherits public sharing from": "Inherits public sharing from.", - "Share to web": "Share to web.", - "Shared to web": "Shared to web.", - "Anyone with the link can view this page": "Anyone with the link can view this page.", - "Make this page publicly accessible": "Make this page publicly accessible.", - "Include sub-pages": "Include sub-pages.", - "Make sub-pages public too": "Make sub-pages public too.", - "Allow search engines to index page": "Allow search engines to index page.", - "Open page": "Open page.", - "Page": "Page.", - "Delete public share link": "Delete public share link.", - "Delete share": "Delete share.", + "Share": "Share", + "Public sharing": "Public sharing", + "Shared by": "Shared by", + "Shared at": "Shared at", + "Inherits public sharing from": "Inherits public sharing from", + "Share to web": "Share to web", + "Shared to web": "Shared to web", + "Anyone with the link can view this page": "Anyone with the link can view this page", + "Make this page publicly accessible": "Make this page publicly accessible", + "Include sub-pages": "Include sub-pages", + "Make sub-pages public too": "Make sub-pages public too", + "Allow search engines to index page": "Allow search engines to index page", + "Open page": "Open page", + "Page": "Page", + "Delete public share link": "Delete public share link", + "Delete share": "Delete share", "Are you sure you want to delete this shared link?": "Are you sure you want to delete this shared link?", - "Publicly shared pages from spaces you are a member of will appear here": "Publicly shared pages from spaces you are a member of will appear here.", - "Share deleted successfully": "Share deleted successfully.", - "Share not found": "Share not found.", - "Failed to share page": "Failed to share page.", + "Publicly shared pages from spaces you are a member of will appear here": "Publicly shared pages from spaces you are a member of will appear here", + "Share deleted successfully": "Share deleted successfully", + "Share not found": "Share not found", + "Failed to share page": "Failed to share page", "Disable public sharing": "Disable public sharing", "Prevent members from sharing pages publicly.": "Prevent members from sharing pages publicly.", "Toggle public sharing": "Toggle public sharing", @@ -464,135 +464,135 @@ "Public sharing is disabled": "Public sharing is disabled", "Public sharing has been disabled at the workspace level.": "Public sharing has been disabled at the workspace level.", "Public sharing has been disabled for this space.": "Public sharing has been disabled for this space.", - "Copy page": "Copy page.", + "Copy page": "Copy page", "Copy page to a different space.": "Copy page to a different space.", - "Page copied successfully": "Page copied successfully.", - "Page duplicated successfully": "Page duplicated successfully.", - "Find": "Find.", - "Not found": "Not found.", - "Previous Match (Shift+Enter)": "Previous Match (Shift+Enter).", - "Next match (Enter)": "Next match (Enter).", - "Match case (Alt+C)": "Match case (Alt+C).", - "Replace": "Replace.", - "Close (Escape)": "Close (Escape).", - "Replace (Enter)": "Replace (Enter).", - "Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter).", - "Replace all": "Replace all.", - "View all spaces": "View all spaces.", - "Error": "Error.", - "Failed to disable MFA": "Failed to disable MFA.", - "Disable two-factor authentication": "Disable two-factor authentication.", + "Page copied successfully": "Page copied successfully", + "Page duplicated successfully": "Page duplicated successfully", + "Find": "Find", + "Not found": "Not found", + "Previous Match (Shift+Enter)": "Previous Match (Shift+Enter)", + "Next match (Enter)": "Next match (Enter)", + "Match case (Alt+C)": "Match case (Alt+C)", + "Replace": "Replace", + "Close (Escape)": "Close (Escape)", + "Replace (Enter)": "Replace (Enter)", + "Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)", + "Replace all": "Replace all", + "View all spaces": "View all spaces", + "Error": "Error", + "Failed to disable MFA": "Failed to disable MFA", + "Disable two-factor authentication": "Disable two-factor authentication", "Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.", "Please enter your password to disable two-factor authentication:": "Please enter your password to disable two-factor authentication:", - "Two-factor authentication has been enabled": "Two-factor authentication has been enabled.", - "Two-factor authentication has been disabled": "Two-factor authentication has been disabled.", - "2-step verification": "2-step verification.", + "Two-factor authentication has been enabled": "Two-factor authentication has been enabled", + "Two-factor authentication has been disabled": "Two-factor authentication has been disabled", + "2-step verification": "2-step verification", "Protect your account with an additional verification layer when signing in.": "Protect your account with an additional verification layer when signing in.", "Two-factor authentication is active on your account.": "Two-factor authentication is active on your account.", - "Add 2FA method": "Add 2FA method.", - "Backup codes": "Backup codes.", - "Disable": "Disable.", - "Invalid verification code": "Invalid verification code.", - "New backup codes have been generated": "New backup codes have been generated.", - "Failed to regenerate backup codes": "Failed to regenerate backup codes.", - "About backup codes": "About backup codes.", + "Add 2FA method": "Add 2FA method", + "Backup codes": "Backup codes", + "Disable": "Disable", + "Invalid verification code": "Invalid verification code", + "New backup codes have been generated": "New backup codes have been generated", + "Failed to regenerate backup codes": "Failed to regenerate backup codes", + "About backup codes": "About backup codes", "Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.", "You can regenerate new backup codes at any time. This will invalidate all existing codes.": "You can regenerate new backup codes at any time. This will invalidate all existing codes.", - "Confirm password": "Confirm password.", - "Generate new backup codes": "Generate new backup codes.", - "Save your new backup codes": "Save your new backup codes.", + "Confirm password": "Confirm password", + "Generate new backup codes": "Generate new backup codes", + "Save your new backup codes": "Save your new backup codes", "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.", - "Your new backup codes": "Your new backup codes.", - "I've saved my backup codes": "I've saved my backup codes.", - "Failed to setup MFA": "Failed to setup MFA.", - "Setup & Verify": "Setup & Verify.", - "Add to authenticator": "Add to authenticator.", - "1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app.", + "Your new backup codes": "Your new backup codes", + "I've saved my backup codes": "I've saved my backup codes", + "Failed to setup MFA": "Failed to setup MFA", + "Setup & Verify": "Setup & Verify", + "Add to authenticator": "Add to authenticator", + "1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app", "Can't scan the code?": "Can't scan the code?", "Enter this code manually in your authenticator app:": "Enter this code manually in your authenticator app:", - "2. Enter the 6-digit code from your authenticator": "2. Enter the 6-digit code from your authenticator.", - "Verify and enable": "Verify and enable.", + "2. Enter the 6-digit code from your authenticator": "2. Enter the 6-digit code from your authenticator", + "Verify and enable": "Verify and enable", "Failed to generate QR code. Please try again.": "Failed to generate QR code. Please try again.", - "Backup": "Backup.", - "Save codes": "Save codes.", - "Save your backup codes": "Save your backup codes.", + "Backup": "Backup", + "Save codes": "Save codes", + "Save your backup codes": "Save your backup codes", "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.", - "Print": "Print.", + "Print": "Print", "Two-factor authentication has been set up. Please log in again.": "Two-factor authentication has been set up. Please log in again.", - "Two-Factor authentication required": "Two-factor authentication required.", - "Your workspace requires two-factor authentication for all users": "Your workspace requires two-factor authentication for all users.", + "Two-Factor authentication required": "Two-factor authentication required", + "Your workspace requires two-factor authentication for all users": "Your workspace requires two-factor authentication for all users", "To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.", - "Set up two-factor authentication": "Set up two-factor authentication.", - "Cancel and logout": "Cancel and logout.", + "Set up two-factor authentication": "Set up two-factor authentication", + "Cancel and logout": "Cancel and logout", "Your workspace requires two-factor authentication. Please set it up to continue.": "Your workspace requires two-factor authentication. Please set it up to continue.", "This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "This adds an extra layer of security to your account by requiring a verification code from your authenticator app.", - "Password is required": "Password is required.", - "Password must be at least 8 characters": "Password must be at least 8 characters.", - "Please enter a 6-digit code": "Please enter a 6-digit code.", - "Code must be exactly 6 digits": "Code must be exactly 6 digits.", - "Enter the 6-digit code found in your authenticator app": "Enter the 6-digit code found in your authenticator app.", + "Password is required": "Password is required", + "Password must be at least 8 characters": "Password must be at least 8 characters", + "Please enter a 6-digit code": "Please enter a 6-digit code", + "Code must be exactly 6 digits": "Code must be exactly 6 digits", + "Enter the 6-digit code found in your authenticator app": "Enter the 6-digit code found in your authenticator app", "Need help authenticating?": "Need help authenticating?", - "MFA QR Code": "MFA QR Code.", + "MFA QR Code": "MFA QR Code", "Account created successfully. Please log in to set up two-factor authentication.": "Account created successfully. Please log in to set up two-factor authentication.", "Password reset successful. Please log in with your new password and complete two-factor authentication.": "Password reset successful. Please log in with your new password and complete two-factor authentication.", "Password reset successful. Please log in with your new password to set up two-factor authentication.": "Password reset successful. Please log in with your new password to set up two-factor authentication.", "Password reset was successful. Please log in with your new password.": "Password reset was successful. Please log in with your new password.", - "Two-factor authentication": "Two-factor authentication.", - "Use authenticator app instead": "Use authenticator app instead.", - "Verify backup code": "Verify backup code.", - "Use backup code": "Use backup code.", - "Enter one of your backup codes": "Enter one of your backup codes.", - "Backup code": "Backup code.", + "Two-factor authentication": "Two-factor authentication", + "Use authenticator app instead": "Use authenticator app instead", + "Verify backup code": "Verify backup code", + "Use backup code": "Use backup code", + "Enter one of your backup codes": "Enter one of your backup codes", + "Backup code": "Backup code", "Enter one of your backup codes. Each backup code can only be used once.": "Enter one of your backup codes. Each backup code can only be used once.", - "Verify": "Verify.", - "Trash": "Trash.", + "Verify": "Verify", + "Trash": "Trash", "Pages in trash will be permanently deleted after {{count}} days.": "Pages in trash will be permanently deleted after {{count}} days.", - "Deleted": "Deleted.", - "No pages in trash": "No pages in trash.", + "Deleted": "Deleted", + "No pages in trash": "No pages in trash", "Permanently delete page?": "Permanently delete page?", "Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.", "Restore '{{title}}' and its sub-pages?": "Restore '{{title}}' and its sub-pages?", - "Move to trash": "Move to trash.", + "Move to trash": "Move to trash", "Move this page to trash?": "Move this page to trash?", - "Restore page": "Restore page.", - "Page moved to trash": "Page moved to trash.", - "Page restored successfully": "Page restored successfully.", - "Deleted by": "Deleted by.", - "Deleted at": "Deleted at.", - "Preview": "Preview.", - "Subpages": "Subpages.", - "Failed to load subpages": "Failed to load subpages.", - "No subpages": "No subpages.", - "Subpages (Child pages)": "Subpages (Child pages).", - "List all subpages of the current page": "List all subpages of the current page.", - "Attachments": "Attachments.", - "All spaces": "All spaces.", - "Unknown": "Unknown.", - "Find a space": "Find a space.", - "Search in all your spaces": "Search in all your spaces.", - "Type": "Type.", - "Enterprise": "Enterprise.", - "Download attachment": "Download attachment.", - "Allowed email domains": "Allowed email domains.", - "Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can sign up via SSO.", - "Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space.", - "Enforce two-factor authentication": "Enforce two-factor authentication.", + "Restore page": "Restore page", + "Page moved to trash": "Page moved to trash", + "Page restored successfully": "Page restored successfully", + "Deleted by": "Deleted by", + "Deleted at": "Deleted at", + "Preview": "Preview", + "Subpages": "Subpages", + "Failed to load subpages": "Failed to load subpages", + "No subpages": "No subpages", + "Subpages (Child pages)": "Subpages (Child pages)", + "List all subpages of the current page": "List all subpages of the current page", + "Attachments": "Attachments", + "All spaces": "All spaces", + "Unknown": "Unknown", + "Find a space": "Find a space", + "Search in all your spaces": "Search in all your spaces", + "Type": "Type", + "Enterprise": "Enterprise", + "Download attachment": "Download attachment", + "Allowed email domains": "Allowed email domains", + "Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can signup via SSO.", + "Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space", + "Enforce two-factor authentication": "Enforce two-factor authentication", "Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.", - "Toggle MFA enforcement": "Toggle MFA enforcement.", - "Display name": "Display name.", - "Allow signup": "Allow signup.", - "Enabled": "Enabled.", - "Advanced Settings": "Advanced Settings.", - "Enable TLS/SSL": "Enable TLS/SSL.", - "Use secure connection to LDAP server": "Use secure connection to LDAP server.", - "Group sync": "Group sync.", + "Toggle MFA enforcement": "Toggle MFA enforcement", + "Display name": "Display name", + "Allow signup": "Allow signup", + "Enabled": "Enabled", + "Advanced Settings": "Advanced Settings", + "Enable TLS/SSL": "Enable TLS/SSL", + "Use secure connection to LDAP server": "Use secure connection to LDAP server", + "Group sync": "Group sync", "No SSO providers found.": "No SSO providers found.", - "Delete SSO provider": "Delete SSO provider.", + "Delete SSO provider": "Delete SSO provider", "Are you sure you want to delete this SSO provider?": "Are you sure you want to delete this SSO provider?", - "Action": "Action.", - "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration.", - "Icon": "Icon.", - "Upload image": "Upload image.", + "Action": "Action", + "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration", + "Icon": "Icon", + "Upload image": "Upload image", "Remove image": "Remove image", "Failed to remove image": "Failed to remove image", "Image exceeds 10MB limit.": "Image exceeds 10MB limit.", @@ -670,13 +670,13 @@ "More options": "More options", "{{name}} mentioned you in a comment": "{{name}} mentioned you in a comment", "{{name}} commented on a page": "{{name}} commented on a page", - "{{name}} resolved a comment": "{{name}} resolved a comment.", - "{{name}} mentioned you on a page": "{{name}} mentioned you on a page.", - "{{name}} gave you edit access to a page": "{{name}} gave you edit access to a page.", - "{{name}} gave you view access to a page": "{{name}} gave you view access to a page.", - "{{name}} updated a page": "{{name}} has updated a page.", - "Watch page": "Watch this page", - "Stop watching": "Stop watching this page", + "{{name}} resolved a comment": "{{name}} resolved a comment", + "{{name}} mentioned you on a page": "{{name}} mentioned you on a page", + "{{name}} gave you edit access to a page": "{{name}} gave you edit access to a page", + "{{name}} gave you view access to a page": "{{name}} gave you view access to a page", + "{{name}} updated a page": "{{name}} updated a page", + "Watch page": "Watch page", + "Stop watching": "Stop watching", "Email notifications": "Email notifications", "Page updates": "Page updates", "Get notified when pages you watch are updated.": "Receive notifications when the pages you watch are updated.", @@ -726,30 +726,30 @@ "Removed page restriction": "Removed page restriction", "Added page permission": "Added page permission", "Removed page permission": "Removed page permission", - "Verifying your email": "Verifying your email.", + "Verifying your email": "Verifying your email", "Please wait...": "Please wait...", "Verification failed. The link may have expired.": "Verification failed. The link may have expired.", - "Check your email": "Check your email.", + "Check your email": "Check your email", "We sent a verification link to {{email}}.": "We sent a verification link to {{email}}.", "We sent a verification link to your email.": "We sent a verification link to your email.", "Click the link to verify your email and access your workspace.": "Click the link to verify your email and access your workspace.", - "Resend verification email": "Resend verification email.", + "Resend verification email": "Resend verification email", "Verification email sent. Please check your inbox.": "Verification email sent. Please check your inbox.", "Failed to resend verification email. Please try again.": "Failed to resend verification email. Please try again.", "We've sent you an email with your associated workspaces.": "We've sent you an email with your associated workspaces.", - "Load more": "Load more.", - "Log out of all devices": "Log out of all devices.", - "Log out of all sessions except this device": "Log out of all sessions except this device.", - "This Device": "This Device.", - "Unknown device": "Unknown device.", - "No active sessions": "No active sessions.", - "Session revoked": "Session revoked.", - "All other sessions revoked": "All other sessions revoked.", - "Last used": "Last used.", - "Created": "Created.", - "Rename": "Rename.", - "Publish": "Publish.", - "Security": "Security.", - "Enforce SSO": "Enforce SSO.", - "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password." + "Load more": "Load more", + "Log out of all devices": "Log out of all devices", + "Log out of all sessions except this device": "Log out of all sessions except this device", + "This Device": "This Device", + "Unknown device": "Unknown device", + "No active sessions": "No active sessions", + "Session revoked": "Session revoked", + "All other sessions revoked": "All other sessions revoked", + "Last used": "Last used", + "Created": "Created", + "Rename": "Rename", + "Publish": "Publish", + "Security": "Security", + "Enforce SSO": "Enforce SSO", + "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password." } diff --git a/apps/client/src/features/comment/queries/comment-query.ts b/apps/client/src/features/comment/queries/comment-query.ts index 6c86c0398f..6b7432ebff 100644 --- a/apps/client/src/features/comment/queries/comment-query.ts +++ b/apps/client/src/features/comment/queries/comment-query.ts @@ -65,6 +65,11 @@ export function useCreateCommentMutation() { ) as InfiniteData> | undefined; if (cache && cache.pages.length > 0) { + const alreadyExists = cache.pages.some((page) => + page.items.some((c) => c.id === newComment.id), + ); + if (alreadyExists) return; + const lastIdx = cache.pages.length - 1; queryClient.setQueryData(RQ_KEY(newComment.pageId), { ...cache, diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 8f6e6cdbf2..a61b994b76 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -142,6 +142,25 @@ export const mainExtensions = [ }), ]; }, + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + const { from, to } = editor.state.selection; + if (from !== to) return false; + if (!editor.isActive("code")) return false; + + const $from = editor.state.doc.resolve(from); + const codeType = editor.state.schema.marks.code; + const nodeAfter = $from.nodeAfter; + + if (nodeAfter && codeType.isInSet(nodeAfter.marks)) { + return false; + } + + return editor.chain().unsetCode().splitBlock().run(); + }, + }; + }, }), SharedStorage, Heading, diff --git a/apps/server/package.json b/apps/server/package.json index c6e0dc2c04..252d73ed89 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.71.0", + "version": "0.71.1", "description": "", "author": "", "private": true, diff --git a/apps/server/src/collaboration/processors/history.processor.ts b/apps/server/src/collaboration/processors/history.processor.ts index d7e27f6062..5374e74596 100644 --- a/apps/server/src/collaboration/processors/history.processor.ts +++ b/apps/server/src/collaboration/processors/history.processor.ts @@ -11,6 +11,7 @@ import { import { extractMentions, extractPageMentions, + extractInternalLinkSlugIds, } from '../../common/helpers/prosemirror/utils'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; @@ -77,12 +78,14 @@ export class HistoryProcessor extends WorkerHost implements OnModuleDestroy { const mentions = extractMentions(page.content); const pageMentions = extractPageMentions(mentions); + const internalLinkSlugIds = extractInternalLinkSlugIds(page.content); await this.generalQueue .add(QueueJob.PAGE_BACKLINKS, { pageId, workspaceId: page.workspaceId, mentions: pageMentions, + internalLinkSlugIds, } as IPageBacklinkJob) .catch((err) => { this.logger.error( diff --git a/apps/server/src/common/helpers/prosemirror/utils.ts b/apps/server/src/common/helpers/prosemirror/utils.ts index 7704306e04..07b6828bf0 100644 --- a/apps/server/src/common/helpers/prosemirror/utils.ts +++ b/apps/server/src/common/helpers/prosemirror/utils.ts @@ -7,6 +7,10 @@ import { validate as isValidUUID } from 'uuid'; import { Transform } from '@tiptap/pm/transform'; import { TiptapTransformer } from '@hocuspocus/transformer'; import * as Y from 'yjs'; +import { + INTERNAL_LINK_REGEX, + extractPageSlugId, +} from '../../../integrations/export/utils'; export interface MentionNode { id: string; @@ -64,6 +68,27 @@ export function extractPageMentions(mentionList: MentionNode[]): MentionNode[] { return pageMentionList as MentionNode[]; } +export function extractInternalLinkSlugIds(prosemirrorJson: any): string[] { + const slugIds: string[] = []; + const doc = jsonToNode(prosemirrorJson); + + doc.descendants((node: Node) => { + for (const mark of node.marks) { + if (mark.type.name === 'link' && mark.attrs.internal && mark.attrs.href) { + const match = mark.attrs.href.match(INTERNAL_LINK_REGEX); + if (match) { + const slugId = extractPageSlugId(match[5]); + if (slugId && !slugIds.includes(slugId)) { + slugIds.push(slugId); + } + } + } + } + }); + + return slugIds; +} + export function extractUserMentionIdsFromJson(json: any): string[] { const userIds: string[] = []; diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 8df9e4bd48..c162782c97 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -47,6 +47,10 @@ import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { EventName } from '../../../common/events/event.contants'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CollaborationGateway } from '../../../collaboration/collaboration.gateway'; +import { + INTERNAL_LINK_REGEX, + extractPageSlugId, +} from '../../../integrations/export/utils'; import { markdownToHtml } from '@docmost/editor-ext'; import { WatcherService } from '../../watcher/watcher.service'; import { sql } from 'kysely'; @@ -510,6 +514,11 @@ export class PageService { }); }); + const slugIdMap = new Map(); + for (const [, entry] of pageMap) { + slugIdMap.set(entry.oldSlugId, entry); + } + const attachmentMap = new Map(); const insertablePages: InsertablePage[] = await Promise.all( @@ -576,6 +585,28 @@ export class PageService { node.attrs.slugId = mappedPage.newSlugId; } } + + // Update internal page links in link marks + for (const mark of node.marks) { + if ( + mark.type.name === 'link' && + mark.attrs.internal && + mark.attrs.href + ) { + const match = mark.attrs.href.match(INTERNAL_LINK_REGEX); + if (match) { + const slugId = extractPageSlugId(match[5]); + if (slugId && slugIdMap.has(slugId)) { + const mappedPage = slugIdMap.get(slugId); + //@ts-ignore + mark.attrs.href = mark.attrs.href.replace( + slugId, + mappedPage.newSlugId, + ); + } + } + } + } }); const prosemirrorJson = prosemirrorDoc.toJSON(); diff --git a/apps/server/src/integrations/queue/constants/queue.interface.ts b/apps/server/src/integrations/queue/constants/queue.interface.ts index f0683a4eec..58937419af 100644 --- a/apps/server/src/integrations/queue/constants/queue.interface.ts +++ b/apps/server/src/integrations/queue/constants/queue.interface.ts @@ -4,6 +4,7 @@ export interface IPageBacklinkJob { pageId: string; workspaceId: string; mentions: MentionNode[]; + internalLinkSlugIds?: string[]; } export interface IAddPageWatchersJob { diff --git a/apps/server/src/integrations/queue/tasks/backlinks.task.ts b/apps/server/src/integrations/queue/tasks/backlinks.task.ts index 268adaaea0..9d707272a1 100644 --- a/apps/server/src/integrations/queue/tasks/backlinks.task.ts +++ b/apps/server/src/integrations/queue/tasks/backlinks.task.ts @@ -11,7 +11,7 @@ export async function processBacklinks( backlinkRepo: BacklinkRepo, data: IPageBacklinkJob, ): Promise { - const { pageId, mentions, workspaceId } = data; + const { pageId, mentions, workspaceId, internalLinkSlugIds = [] } = data; await executeTx(db, async (trx) => { const existingBacklinks = await trx @@ -20,7 +20,28 @@ export async function processBacklinks( .where('sourcePageId', '=', pageId) .execute(); - if (existingBacklinks.length === 0 && mentions.length === 0) { + const mentionTargetPageIds = mentions + .filter((mention) => mention.entityId !== pageId) + .map((mention) => mention.entityId); + + let resolvedLinkPageIds: string[] = []; + if (internalLinkSlugIds.length > 0) { + const resolvedPages = await trx + .selectFrom('pages') + .select('id') + .where('slugId', 'in', internalLinkSlugIds) + .where('workspaceId', '=', workspaceId) + .execute(); + resolvedLinkPageIds = resolvedPages + .map((p) => p.id) + .filter((id) => id !== pageId); + } + + const allTargetPageIds = [ + ...new Set([...mentionTargetPageIds, ...resolvedLinkPageIds]), + ]; + + if (existingBacklinks.length === 0 && allTargetPageIds.length === 0) { return; } @@ -28,16 +49,12 @@ export async function processBacklinks( (backlink) => backlink.targetPageId, ); - const targetPageIds = mentions - .filter((mention) => mention.entityId !== pageId) - .map((mention) => mention.entityId); - let validTargetPages = []; - if (targetPageIds.length > 0) { + if (allTargetPageIds.length > 0) { validTargetPages = await trx .selectFrom('pages') .select('id') - .where('id', 'in', targetPageIds) + .where('id', 'in', allTargetPageIds) .where('workspaceId', '=', workspaceId) .execute(); } diff --git a/package.json b/package.json index 36db0934ed..2c49556b37 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "docmost", "homepage": "https://docmost.com", - "version": "0.71.0", + "version": "0.71.1", "private": true, "scripts": { "build": "nx run-many -t build", @@ -109,7 +109,8 @@ "nanoid@^3": "3.3.8", "socket.io-parser": "4.2.6", "serialize-javascript": "7.0.3", - "lodash-es": "4.17.23", + "lodash-es": "4.18.1", + "lodash": "4.18.1", "@hono/node-server": "1.19.10", "undici": "7.24.0", "ajv@^6": "6.14.0", @@ -126,7 +127,9 @@ "yaml@>=1.0.0 <1.10.3": "1.10.3", "yaml@>=2.0.0 <2.8.3": "2.8.3", "path-to-regexp@^8": "8.4.0", - "brace-expansion@^5": "5.0.5" + "brace-expansion@^5": "5.0.5", + "@xmldom/xmldom": "0.8.12", + "handlebars": "4.7.9" }, "neverBuiltDependencies": [] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77878a199b..c00668da58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,8 @@ overrides: nanoid@^3: 3.3.8 socket.io-parser: 4.2.6 serialize-javascript: 7.0.3 - lodash-es: 4.17.23 + lodash-es: 4.18.1 + lodash: 4.18.1 '@hono/node-server': 1.19.10 undici: 7.24.0 ajv@^6: 6.14.0 @@ -34,6 +35,8 @@ overrides: yaml@>=2.0.0 <2.8.3: 2.8.3 path-to-regexp@^8: 8.4.0 brace-expansion@^5: 5.0.5 + '@xmldom/xmldom': 0.8.12 + handlebars: 4.7.9 patchedDependencies: react-arborist@3.4.0: @@ -923,44 +926,44 @@ packages: resolution: {integrity: sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.23': - resolution: {integrity: sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w==} + '@aws-sdk/core@3.973.26': + resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} engines: {node: '>=20.0.0'} '@aws-sdk/crc64-nvme@3.972.5': resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.21': - resolution: {integrity: sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg==} + '@aws-sdk/credential-provider-env@3.972.24': + resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.23': - resolution: {integrity: sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA==} + '@aws-sdk/credential-provider-http@3.972.26': + resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.23': - resolution: {integrity: sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ==} + '@aws-sdk/credential-provider-ini@3.972.28': + resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.23': - resolution: {integrity: sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg==} + '@aws-sdk/credential-provider-login@3.972.28': + resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.24': - resolution: {integrity: sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw==} + '@aws-sdk/credential-provider-node@3.972.29': + resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.21': - resolution: {integrity: sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww==} + '@aws-sdk/credential-provider-process@3.972.24': + resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.23': - resolution: {integrity: sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA==} + '@aws-sdk/credential-provider-sso@3.972.28': + resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.23': - resolution: {integrity: sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag==} + '@aws-sdk/credential-provider-web-identity@3.972.28': + resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} engines: {node: '>=20.0.0'} '@aws-sdk/lib-storage@3.1014.0': @@ -977,8 +980,8 @@ packages: resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.3': - resolution: {integrity: sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ==} + '@aws-sdk/middleware-flexible-checksums@3.974.6': + resolution: {integrity: sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-host-header@3.972.8': @@ -993,40 +996,40 @@ packages: resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.8': - resolution: {integrity: sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==} + '@aws-sdk/middleware-recursion-detection@3.972.9': + resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.23': - resolution: {integrity: sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ==} + '@aws-sdk/middleware-sdk-s3@3.972.27': + resolution: {integrity: sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-ssec@3.972.8': resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.24': - resolution: {integrity: sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ==} + '@aws-sdk/middleware-user-agent@3.972.28': + resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.996.13': - resolution: {integrity: sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw==} + '@aws-sdk/nested-clients@3.996.18': + resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.972.9': - resolution: {integrity: sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==} + '@aws-sdk/region-config-resolver@3.972.10': + resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} engines: {node: '>=20.0.0'} '@aws-sdk/s3-request-presigner@3.1014.0': resolution: {integrity: sha512-XEcK50lToSoLPrQztKQhONYQW45613H8oEL00mBUd/+OZgk0+3zJ8kSNDsIJioZ3H7Be+yC3CL6a22dZFIKUXQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.11': - resolution: {integrity: sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA==} + '@aws-sdk/signature-v4-multi-region@3.996.15': + resolution: {integrity: sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1014.0': - resolution: {integrity: sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA==} + '@aws-sdk/token-providers@3.1021.0': + resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} engines: {node: '>=20.0.0'} '@aws-sdk/types@3.973.6': @@ -1052,8 +1055,8 @@ packages: '@aws-sdk/util-user-agent-browser@3.972.8': resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} - '@aws-sdk/util-user-agent-node@3.973.10': - resolution: {integrity: sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g==} + '@aws-sdk/util-user-agent-node@3.973.14': + resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -1061,8 +1064,8 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.15': - resolution: {integrity: sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==} + '@aws-sdk/xml-builder@3.972.16': + resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.3': @@ -4373,8 +4376,8 @@ packages: resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.12': - resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + '@smithy/core@3.23.13': + resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} engines: {node: '>=18.0.0'} '@smithy/credential-provider-imds@4.2.12': @@ -4437,16 +4440,16 @@ packages: resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.27': - resolution: {integrity: sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==} + '@smithy/middleware-endpoint@4.4.28': + resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.44': - resolution: {integrity: sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==} + '@smithy/middleware-retry@4.4.46': + resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.15': - resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + '@smithy/middleware-serde@4.2.16': + resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} engines: {node: '>=18.0.0'} '@smithy/middleware-stack@4.2.12': @@ -4457,8 +4460,8 @@ packages: resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.0': - resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + '@smithy/node-http-handler@4.5.1': + resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} engines: {node: '>=18.0.0'} '@smithy/property-provider@4.2.12': @@ -4489,8 +4492,8 @@ packages: resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.7': - resolution: {integrity: sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==} + '@smithy/smithy-client@4.12.8': + resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} engines: {node: '>=18.0.0'} '@smithy/types@4.13.1': @@ -4525,12 +4528,12 @@ packages: resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.43': - resolution: {integrity: sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==} + '@smithy/util-defaults-mode-browser@4.3.44': + resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.47': - resolution: {integrity: sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==} + '@smithy/util-defaults-mode-node@4.2.48': + resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.3.3': @@ -4545,12 +4548,12 @@ packages: resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.12': - resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + '@smithy/util-retry@4.2.13': + resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.20': - resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + '@smithy/util-stream@4.5.21': + resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.2': @@ -4565,8 +4568,8 @@ packages: resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.2.13': - resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + '@smithy/util-waiter@4.2.14': + resolution: {integrity: sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==} engines: {node: '>=18.0.0'} '@smithy/uuid@1.1.2': @@ -5545,8 +5548,8 @@ packages: resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} engines: {node: '>= 16'} - '@xmldom/xmldom@0.8.10': - resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + '@xmldom/xmldom@0.8.12': + resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} engines: {node: '>=10.0.0'} '@xtuc/ieee754@1.2.0': @@ -7298,8 +7301,8 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -8307,8 +8310,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -8349,8 +8352,8 @@ packages: lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -11024,26 +11027,26 @@ snapshots: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.23 - '@aws-sdk/credential-provider-node': 3.972.24 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-node': 3.972.29 '@aws-sdk/middleware-bucket-endpoint': 3.972.8 '@aws-sdk/middleware-expect-continue': 3.972.8 - '@aws-sdk/middleware-flexible-checksums': 3.974.3 + '@aws-sdk/middleware-flexible-checksums': 3.974.6 '@aws-sdk/middleware-host-header': 3.972.8 '@aws-sdk/middleware-location-constraint': 3.972.8 '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.8 - '@aws-sdk/middleware-sdk-s3': 3.972.23 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-sdk-s3': 3.972.27 '@aws-sdk/middleware-ssec': 3.972.8 - '@aws-sdk/middleware-user-agent': 3.972.24 - '@aws-sdk/region-config-resolver': 3.972.9 - '@aws-sdk/signature-v4-multi-region': 3.996.11 + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/region-config-resolver': 3.972.10 + '@aws-sdk/signature-v4-multi-region': 3.996.15 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.10 + '@aws-sdk/util-user-agent-node': 3.973.14 '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/eventstream-serde-browser': 4.2.12 '@smithy/eventstream-serde-config-resolver': 4.3.12 '@smithy/eventstream-serde-node': 4.2.12 @@ -11054,41 +11057,41 @@ snapshots: '@smithy/invalid-dependency': 4.2.12 '@smithy/md5-js': 4.2.12 '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 '@smithy/middleware-stack': 4.2.12 '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/url-parser': 4.2.12 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 '@smithy/util-endpoints': 3.3.3 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-retry': 4.2.13 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 - '@smithy/util-waiter': 4.2.13 + '@smithy/util-waiter': 4.2.14 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.973.23': + '@aws-sdk/core@3.973.26': dependencies: '@aws-sdk/types': 3.973.6 - '@aws-sdk/xml-builder': 3.972.15 - '@smithy/core': 3.23.12 + '@aws-sdk/xml-builder': 3.972.16 + '@smithy/core': 3.23.13 '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/util-base64': 4.3.2 '@smithy/util-middleware': 4.2.12 @@ -11100,37 +11103,37 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.21': + '@aws-sdk/credential-provider-env@3.972.24': dependencies: - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.23': + '@aws-sdk/credential-provider-http@3.972.26': dependencies: - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.23': + '@aws-sdk/credential-provider-ini@3.972.28': dependencies: - '@aws-sdk/core': 3.973.23 - '@aws-sdk/credential-provider-env': 3.972.21 - '@aws-sdk/credential-provider-http': 3.972.23 - '@aws-sdk/credential-provider-login': 3.972.23 - '@aws-sdk/credential-provider-process': 3.972.21 - '@aws-sdk/credential-provider-sso': 3.972.23 - '@aws-sdk/credential-provider-web-identity': 3.972.23 - '@aws-sdk/nested-clients': 3.996.13 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-login': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/credential-provider-imds': 4.2.12 '@smithy/property-provider': 4.2.12 @@ -11140,10 +11143,10 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.23': + '@aws-sdk/credential-provider-login@3.972.28': dependencies: - '@aws-sdk/core': 3.973.23 - '@aws-sdk/nested-clients': 3.996.13 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 @@ -11153,14 +11156,14 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.24': + '@aws-sdk/credential-provider-node@3.972.29': dependencies: - '@aws-sdk/credential-provider-env': 3.972.21 - '@aws-sdk/credential-provider-http': 3.972.23 - '@aws-sdk/credential-provider-ini': 3.972.23 - '@aws-sdk/credential-provider-process': 3.972.21 - '@aws-sdk/credential-provider-sso': 3.972.23 - '@aws-sdk/credential-provider-web-identity': 3.972.23 + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-ini': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 '@aws-sdk/types': 3.973.6 '@smithy/credential-provider-imds': 4.2.12 '@smithy/property-provider': 4.2.12 @@ -11170,20 +11173,20 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.21': + '@aws-sdk/credential-provider-process@3.972.24': dependencies: - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.23': + '@aws-sdk/credential-provider-sso@3.972.28': dependencies: - '@aws-sdk/core': 3.973.23 - '@aws-sdk/nested-clients': 3.996.13 - '@aws-sdk/token-providers': 3.1014.0 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/token-providers': 3.1021.0 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -11192,10 +11195,10 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.23': + '@aws-sdk/credential-provider-web-identity@3.972.28': dependencies: - '@aws-sdk/core': 3.973.23 - '@aws-sdk/nested-clients': 3.996.13 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -11208,8 +11211,8 @@ snapshots: dependencies: '@aws-sdk/client-s3': 3.1014.0 '@smithy/abort-controller': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/smithy-client': 4.12.7 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/smithy-client': 4.12.8 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 @@ -11232,12 +11235,12 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.3': + '@aws-sdk/middleware-flexible-checksums@3.974.6': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/crc64-nvme': 3.972.5 '@aws-sdk/types': 3.973.6 '@smithy/is-array-buffer': 4.2.2 @@ -11245,7 +11248,7 @@ snapshots: '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 @@ -11268,7 +11271,7 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.8': + '@aws-sdk/middleware-recursion-detection@3.972.9': dependencies: '@aws-sdk/types': 3.973.6 '@aws/lambda-invoke-store': 0.2.3 @@ -11276,20 +11279,20 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.23': + '@aws-sdk/middleware-sdk-s3@3.972.27': dependencies: - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/node-config-provider': 4.3.12 '@smithy/protocol-http': 5.3.12 '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/util-config-provider': 4.2.2 '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 @@ -11299,61 +11302,61 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.24': + '@aws-sdk/middleware-user-agent@3.972.28': dependencies: - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.996.13': + '@aws-sdk/nested-clients@3.996.18': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.23 + '@aws-sdk/core': 3.973.26 '@aws-sdk/middleware-host-header': 3.972.8 '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.8 - '@aws-sdk/middleware-user-agent': 3.972.24 - '@aws-sdk/region-config-resolver': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/region-config-resolver': 3.972.10 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.10 + '@aws-sdk/util-user-agent-node': 3.973.14 '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/fetch-http-handler': 5.3.15 '@smithy/hash-node': 4.2.12 '@smithy/invalid-dependency': 4.2.12 '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 '@smithy/middleware-stack': 4.2.12 '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/url-parser': 4.2.12 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 '@smithy/util-endpoints': 3.3.3 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.972.9': + '@aws-sdk/region-config-resolver@3.972.10': dependencies: '@aws-sdk/types': 3.973.6 '@smithy/config-resolver': 4.4.13 @@ -11363,28 +11366,28 @@ snapshots: '@aws-sdk/s3-request-presigner@3.1014.0': dependencies: - '@aws-sdk/signature-v4-multi-region': 3.996.11 + '@aws-sdk/signature-v4-multi-region': 3.996.15 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-format-url': 3.972.8 - '@smithy/middleware-endpoint': 4.4.27 + '@smithy/middleware-endpoint': 4.4.28 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.11': + '@aws-sdk/signature-v4-multi-region@3.996.15': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.972.23 + '@aws-sdk/middleware-sdk-s3': 3.972.27 '@aws-sdk/types': 3.973.6 '@smithy/protocol-http': 5.3.12 '@smithy/signature-v4': 5.3.12 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1014.0': + '@aws-sdk/token-providers@3.1021.0': dependencies: - '@aws-sdk/core': 3.973.23 - '@aws-sdk/nested-clients': 3.996.13 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -11428,16 +11431,16 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.10': + '@aws-sdk/util-user-agent-node@3.973.14': dependencies: - '@aws-sdk/middleware-user-agent': 3.972.24 + '@aws-sdk/middleware-user-agent': 3.972.28 '@aws-sdk/types': 3.973.6 '@smithy/node-config-provider': 4.3.12 '@smithy/types': 4.13.1 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.15': + '@aws-sdk/xml-builder@3.972.16': dependencies: '@smithy/types': 4.13.1 fast-xml-parser: 5.5.8 @@ -12255,23 +12258,23 @@ snapshots: dependencies: '@chevrotain/gast': 11.0.3 '@chevrotain/types': 11.0.3 - lodash-es: 4.17.23 + lodash-es: 4.18.1 '@chevrotain/cst-dts-gen@11.1.2': dependencies: '@chevrotain/gast': 11.1.2 '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.1 '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 - lodash-es: 4.17.23 + lodash-es: 4.18.1 '@chevrotain/gast@11.1.2': dependencies: '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.1 '@chevrotain/regexp-to-ast@11.0.3': {} @@ -13578,7 +13581,7 @@ snapshots: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 17.2.3 dotenv-expand: 12.0.3 - lodash: 4.17.23 + lodash: 4.18.1 rxjs: 7.8.2 '@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': @@ -13710,7 +13713,7 @@ snapshots: '@types/xml-encryption': 1.2.4 '@types/xml2js': 0.4.14 '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.8.10 + '@xmldom/xmldom': 0.8.12 debug: 4.4.0 xml-crypto: 6.1.2 xml-encryption: 3.1.0 @@ -14922,7 +14925,7 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 - '@smithy/core@3.23.12': + '@smithy/core@3.23.13': dependencies: '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 @@ -14930,7 +14933,7 @@ snapshots: '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 '@smithy/uuid': 1.1.2 tslib: 2.8.1 @@ -15026,10 +15029,10 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.27': + '@smithy/middleware-endpoint@4.4.28': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-serde': 4.2.15 + '@smithy/core': 3.23.13 + '@smithy/middleware-serde': 4.2.16 '@smithy/node-config-provider': 4.3.12 '@smithy/shared-ini-file-loader': 4.4.7 '@smithy/types': 4.13.1 @@ -15037,21 +15040,21 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.44': + '@smithy/middleware-retry@4.4.46': dependencies: '@smithy/node-config-provider': 4.3.12 '@smithy/protocol-http': 5.3.12 '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.15': + '@smithy/middleware-serde@4.2.16': dependencies: - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 tslib: 2.8.1 @@ -15068,9 +15071,8 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.5.0': + '@smithy/node-http-handler@4.5.1': dependencies: - '@smithy/abort-controller': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/querystring-builder': 4.2.12 '@smithy/types': 4.13.1 @@ -15117,14 +15119,14 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/smithy-client@4.12.7': + '@smithy/smithy-client@4.12.8': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-endpoint': 4.4.27 + '@smithy/core': 3.23.13 + '@smithy/middleware-endpoint': 4.4.28 '@smithy/middleware-stack': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 tslib: 2.8.1 '@smithy/types@4.13.1': @@ -15165,20 +15167,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.43': + '@smithy/util-defaults-mode-browser@4.3.44': dependencies: '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.47': + '@smithy/util-defaults-mode-node@4.2.48': dependencies: '@smithy/config-resolver': 4.4.13 '@smithy/credential-provider-imds': 4.2.12 '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 tslib: 2.8.1 @@ -15197,16 +15199,16 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/util-retry@4.2.12': + '@smithy/util-retry@4.2.13': dependencies: '@smithy/service-error-classification': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/util-stream@4.5.20': + '@smithy/util-stream@4.5.21': dependencies: '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/types': 4.13.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 @@ -15228,9 +15230,8 @@ snapshots: '@smithy/util-buffer-from': 4.2.2 tslib: 2.8.1 - '@smithy/util-waiter@4.2.13': + '@smithy/util-waiter@4.2.14': dependencies: - '@smithy/abort-controller': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 @@ -16293,7 +16294,7 @@ snapshots: '@xmldom/is-dom-node@1.0.1': {} - '@xmldom/xmldom@0.8.10': {} + '@xmldom/xmldom@0.8.12': {} '@xtuc/ieee754@1.2.0': {} @@ -16849,12 +16850,12 @@ snapshots: chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 - lodash-es: 4.17.23 + lodash-es: 4.18.1 chevrotain-allstar@0.3.1(chevrotain@11.1.2): dependencies: chevrotain: 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.1 chevrotain@11.0.3: dependencies: @@ -16863,7 +16864,7 @@ snapshots: '@chevrotain/regexp-to-ast': 11.0.3 '@chevrotain/types': 11.0.3 '@chevrotain/utils': 11.0.3 - lodash-es: 4.17.23 + lodash-es: 4.18.1 chevrotain@11.1.2: dependencies: @@ -16872,7 +16873,7 @@ snapshots: '@chevrotain/regexp-to-ast': 11.1.2 '@chevrotain/types': 11.1.2 '@chevrotain/utils': 11.1.2 - lodash-es: 4.17.23 + lodash-es: 4.18.1 chokidar@3.6.0: dependencies: @@ -17333,7 +17334,7 @@ snapshots: dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.23 + lodash-es: 4.18.1 data-urls@5.0.0: dependencies: @@ -18396,7 +18397,7 @@ snapshots: hachure-fill@0.5.2: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -19591,7 +19592,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.1: {} lodash.debounce@4.0.8: {} @@ -19619,7 +19620,7 @@ snapshots: lodash.throttle@4.1.1: {} - lodash@4.17.23: {} + lodash@4.18.1: {} log-symbols@4.1.0: dependencies: @@ -19688,7 +19689,7 @@ snapshots: mammoth@1.12.0: dependencies: - '@xmldom/xmldom': 0.8.10 + '@xmldom/xmldom': 0.8.12 argparse: 1.0.10 base64-js: 1.5.1 bluebird: 3.4.7 @@ -19752,7 +19753,7 @@ snapshots: dompurify: 3.3.3 katex: 0.16.40 khroma: 2.1.0 - lodash-es: 4.17.23 + lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -19898,7 +19899,7 @@ snapshots: node-emoji@1.11.0: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 node-fetch@2.7.0: dependencies: @@ -21728,7 +21729,7 @@ snapshots: dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 + handlebars: 4.7.9 jest: 30.3.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 @@ -22292,12 +22293,12 @@ snapshots: xml-crypto@6.1.2: dependencies: '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.8.10 + '@xmldom/xmldom': 0.8.12 xpath: 0.0.33 xml-encryption@3.1.0: dependencies: - '@xmldom/xmldom': 0.8.10 + '@xmldom/xmldom': 0.8.12 escape-html: 1.0.3 xpath: 0.0.32 From e1bbceb9a63e2eeb927c945476c529607f752da0 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:10:41 +0100 Subject: [PATCH 02/22] fix: logs --- apps/server/src/common/helpers/utils.ts | 12 ++++++++++++ apps/server/src/common/logger/pino.config.ts | 3 ++- apps/server/src/ee | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/server/src/common/helpers/utils.ts b/apps/server/src/common/helpers/utils.ts index 36ff5b6396..c37e9a47f3 100644 --- a/apps/server/src/common/helpers/utils.ts +++ b/apps/server/src/common/helpers/utils.ts @@ -142,6 +142,18 @@ export function isUserDisabled(user: { return !!(user.deactivatedAt || user.deletedAt); } +const SENSITIVE_URL_PREFIXES = ['/api/sso/']; + +export function redactSensitiveUrl(url: string): string { + if (url && SENSITIVE_URL_PREFIXES.some((prefix) => url.includes(prefix))) { + const qsIndex = url.indexOf('?'); + if (qsIndex !== -1) { + return url.substring(0, qsIndex); + } + } + return url; +} + export function createByteCountingStream(source: Readable) { let bytesRead = 0; const stream = new Transform({ diff --git a/apps/server/src/common/logger/pino.config.ts b/apps/server/src/common/logger/pino.config.ts index 0b8cd11a0c..4c3b199339 100644 --- a/apps/server/src/common/logger/pino.config.ts +++ b/apps/server/src/common/logger/pino.config.ts @@ -1,5 +1,6 @@ import { Params } from 'nestjs-pino'; import { stdTimeFunctions } from 'pino'; +import { redactSensitiveUrl } from '../helpers/utils'; const CONTEXTS_TO_IGNORE = [ 'InstanceLoader', @@ -52,7 +53,7 @@ export function createPinoConfig(): Params { serializers: { req: (req) => ({ method: req.method, - url: req.url, + url: redactSensitiveUrl(req.url), ip: req.ip || req.remoteAddress, userAgent: req.headers?.['user-agent'], }), diff --git a/apps/server/src/ee b/apps/server/src/ee index 38158a5ab6..dc7ae0e3b0 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 38158a5ab6a4c12c90a3124604b88f05d76efd7d +Subproject commit dc7ae0e3b066df842248b2295ccfad190a0c5a93 From 4966f9b152a9275ddbc12732e09864ca445e3ea7 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:24:46 +0100 Subject: [PATCH 03/22] fix(deps): package updates --- apps/client/package.json | 4 +- apps/server/package.json | 20 +- pnpm-lock.yaml | 852 +++++++++++++-------------------------- 3 files changed, 290 insertions(+), 586 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 22192e56ef..150a938957 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -67,7 +67,7 @@ "@types/node": "22.19.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^6.0.0", + "@vitejs/plugin-react": "^6.0.1", "eslint": "^9.28.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -80,6 +80,6 @@ "prettier": "^3.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.1", - "vite": "^8.0.1" + "vite": "8.0.5" } } diff --git a/apps/server/package.json b/apps/server/package.json index 252d73ed89..9994ba43d9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -48,19 +48,19 @@ "@nestjs-labs/nestjs-ioredis": "^11.0.4", "@nestjs/bullmq": "^11.0.4", "@nestjs/cache-manager": "^3.1.0", - "@nestjs/common": "^11.1.17", + "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.3", - "@nestjs/core": "^11.1.17", + "@nestjs/core": "^11.1.18", "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "11.0.2", - "@nestjs/mapped-types": "^2.1.0", + "@nestjs/mapped-types": "^2.1.1", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-fastify": "^11.1.17", - "@nestjs/platform-socket.io": "^11.1.17", + "@nestjs/platform-fastify": "^11.1.18", + "@nestjs/platform-socket.io": "^11.1.18", "@nestjs/schedule": "^6.1.1", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", - "@nestjs/websockets": "^11.1.17", + "@nestjs/websockets": "^11.1.18", "@node-saml/passport-saml": "^5.1.0", "@react-email/components": "1.0.10", "@react-email/render": "2.0.4", @@ -121,9 +121,9 @@ }, "devDependencies": { "@eslint/js": "^9.28.0", - "@nestjs/cli": "^11.0.16", - "@nestjs/schematics": "^11.0.9", - "@nestjs/testing": "^11.1.17", + "@nestjs/cli": "^11.0.18", + "@nestjs/schematics": "^11.0.10", + "@nestjs/testing": "^11.1.18", "@types/bcrypt": "^6.0.0", "@types/debounce": "^1.2.4", "@types/fs-extra": "^11.0.4", @@ -146,7 +146,7 @@ "source-map-support": "^0.5.21", "supertest": "^7.2.2", "ts-jest": "^29.4.6", - "ts-loader": "^9.5.4", + "ts-loader": "^9.5.7", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c00668da58..ff37ec7826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,8 +410,8 @@ importers: specifier: ^18.3.1 version: 18.3.1 '@vitejs/plugin-react': - specifier: ^6.0.0 - version: 6.0.1(vite@8.0.1(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: specifier: ^9.28.0 version: 9.39.4(jiti@2.4.2) @@ -449,8 +449,8 @@ importers: specifier: ^8.57.1 version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) vite: - specifier: ^8.0.1 - version: 8.0.1(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.5 + version: 8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) apps/server: dependencies: @@ -498,55 +498,55 @@ importers: version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@nest-lab/throttler-storage-redis': specifier: ^1.2.0 - version: 1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2) + version: 1.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2) '@nestjs-labs/nestjs-ioredis': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1) + version: 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(ioredis@5.10.1) '@nestjs/bullmq': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bullmq@5.71.0) + version: 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.71.0) '@nestjs/cache-manager': specifier: ^3.1.0 - version: 3.1.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) + version: 3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) '@nestjs/common': - specifier: ^11.1.17 - version: 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.3 - version: 4.0.3(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.1.17 - version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + version: 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@nestjs/jwt': specifier: 11.0.2 - version: 11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/mapped-types': - specifier: ^2.1.0 - version: 2.1.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + specifier: ^2.1.1 + version: 2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) '@nestjs/passport': specifier: ^11.0.5 - version: 11.0.5(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': - specifier: ^11.1.17 - version: 11.1.17(@fastify/static@9.0.0)(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + specifier: ^11.1.18 + version: 11.1.18(@fastify/static@9.0.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@nestjs/platform-socket.io': - specifier: ^11.1.17 - version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.1.1 - version: 6.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + version: 6.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@nestjs/terminus': specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/throttler': specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2) '@nestjs/websockets': - specifier: ^11.1.17 - version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@node-saml/passport-saml': specifier: ^5.1.0 version: 5.1.0 @@ -633,13 +633,13 @@ importers: version: 5.1.7 nestjs-cls: specifier: ^6.2.0 - version: 6.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 6.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: ^3.1.2 - version: 3.1.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(kysely@0.28.14)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(kysely@0.28.14)(reflect-metadata@0.2.2) nestjs-pino: specifier: ^4.6.1 - version: 4.6.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) + version: 4.6.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) nodemailer: specifier: ^8.0.4 version: 8.0.4 @@ -723,14 +723,14 @@ importers: specifier: ^9.28.0 version: 9.39.4 '@nestjs/cli': - specifier: ^11.0.16 - version: 11.0.16(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0) + specifier: ^11.0.18 + version: 11.0.18(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0) '@nestjs/schematics': - specifier: ^11.0.9 - version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + specifier: ^11.0.10 + version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': - specifier: ^11.1.17 - version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 @@ -798,8 +798,8 @@ importers: specifier: ^29.4.6 version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.28.5))(jest-util@30.3.0)(jest@30.3.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: - specifier: ^9.5.4 - version: 9.5.4(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))) + specifier: ^9.5.7 + version: 9.5.7(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3) @@ -862,8 +862,8 @@ packages: resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} - '@angular-devkit/core@19.2.17': - resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} + '@angular-devkit/core@19.2.23': + resolution: {integrity: sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -871,26 +871,13 @@ packages: chokidar: optional: true - '@angular-devkit/core@19.2.19': - resolution: {integrity: sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - peerDependencies: - chokidar: ^4.0.0 - peerDependenciesMeta: - chokidar: - optional: true - - '@angular-devkit/schematics-cli@19.2.19': - resolution: {integrity: sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==} + '@angular-devkit/schematics-cli@19.2.23': + resolution: {integrity: sha512-M8g7Gu3Lc5bbzijd2QLcQhfdpfMVE32YXQ6FIkA8x91Kmd2gb8aVvGYPLYUN5619P+ABWhN5Dn2PKuk01zz3vg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@19.2.17': - resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - - '@angular-devkit/schematics@19.2.19': - resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} + '@angular-devkit/schematics@19.2.23': + resolution: {integrity: sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@antfu/install-pkg@1.1.0': @@ -2337,15 +2324,6 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} - '@inquirer/checkbox@4.1.2': - resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/checkbox@4.3.2': resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} @@ -2364,24 +2342,6 @@ packages: '@types/node': optional: true - '@inquirer/confirm@5.1.6': - resolution: {integrity: sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/core@10.1.7': - resolution: {integrity: sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -2400,15 +2360,6 @@ packages: '@types/node': optional: true - '@inquirer/editor@4.2.7': - resolution: {integrity: sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/expand@4.0.23': resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} @@ -2418,15 +2369,6 @@ packages: '@types/node': optional: true - '@inquirer/expand@4.0.9': - resolution: {integrity: sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -2436,23 +2378,10 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.10': - resolution: {integrity: sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==} - engines: {node: '>=18'} - '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/input@4.1.6': - resolution: {integrity: sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/input@4.3.1': resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} @@ -2471,15 +2400,6 @@ packages: '@types/node': optional: true - '@inquirer/number@3.0.9': - resolution: {integrity: sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/password@4.0.23': resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} @@ -2489,15 +2409,6 @@ packages: '@types/node': optional: true - '@inquirer/password@4.0.9': - resolution: {integrity: sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/prompts@7.10.1': resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} engines: {node: '>=18'} @@ -2516,15 +2427,6 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@4.0.9': - resolution: {integrity: sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/rawlist@4.1.11': resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} @@ -2534,15 +2436,6 @@ packages: '@types/node': optional: true - '@inquirer/search@3.0.9': - resolution: {integrity: sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/search@3.2.2': resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} @@ -2552,15 +2445,6 @@ packages: '@types/node': optional: true - '@inquirer/select@4.0.9': - resolution: {integrity: sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/select@4.4.2': resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} @@ -2579,15 +2463,6 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.4': - resolution: {integrity: sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} @@ -2976,12 +2851,12 @@ packages: keyv: '>=5' rxjs: ^7.8.1 - '@nestjs/cli@11.0.16': - resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} + '@nestjs/cli@11.0.18': + resolution: {integrity: sha512-z72OS+sFrDgIkNu/e/vUhbnjHZwAYQS8fBJKXLiFyz8059IVuY2FKebV2YMxyhY+920d4LX1hBIAGL5qQNdR7g==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 '@swc/core': ^1.3.62 peerDependenciesMeta: '@swc/cli': @@ -2989,8 +2864,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.17': - resolution: {integrity: sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==} + '@nestjs/common@11.1.18': + resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3008,8 +2883,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.17': - resolution: {integrity: sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==} + '@nestjs/core@11.1.18': + resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3037,12 +2912,12 @@ packages: peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - '@nestjs/mapped-types@2.1.0': - resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 class-transformer: ^0.4.0 || ^0.5.0 - class-validator: ^0.13.0 || ^0.14.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 reflect-metadata: ^0.1.12 || ^0.2.0 peerDependenciesMeta: class-transformer: @@ -3056,8 +2931,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-fastify@11.1.17': - resolution: {integrity: sha512-7nndTu6I2SvIEQbQhv+GVLDpoN05OFZ7x4JhdKhJ+m+1DSNXdgAcPaM1P4FX4xBMKo4m+chbwdcTaGp0ECEnwA==} + '@nestjs/platform-fastify@11.1.18': + resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 '@fastify/view': ^10.0.0 || ^11.0.0 @@ -3069,8 +2944,8 @@ packages: '@fastify/view': optional: true - '@nestjs/platform-socket.io@11.1.17': - resolution: {integrity: sha512-BSOAsENdmTtsnDL0hb4takbWzPy9WoPybjlM57ab3/rQgm0biMFYUupH2uzmCjmmIXJL/EFbAWznVl8xw2Sa6Q==} + '@nestjs/platform-socket.io@11.1.18': + resolution: {integrity: sha512-DiFRpMIdFaHqZQFwqLqGHMdNurrKVkRkMHxIrecjooPHJNNIMgrbpYZ+oJW8hpwifUyZUL4r4uXXRy4+yFEMjA==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -3082,8 +2957,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/schematics@11.0.9': - resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} + '@nestjs/schematics@11.0.10': + resolution: {integrity: sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==} peerDependencies: typescript: '>=4.8.2' @@ -3135,8 +3010,8 @@ packages: typeorm: optional: true - '@nestjs/testing@11.1.17': - resolution: {integrity: sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==} + '@nestjs/testing@11.1.18': + resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3155,8 +3030,8 @@ packages: '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 reflect-metadata: ^0.1.13 || ^0.2.0 - '@nestjs/websockets@11.1.17': - resolution: {integrity: sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==} + '@nestjs/websockets@11.1.18': + resolution: {integrity: sha512-HLO/QGlJoJaMMDphgjbcIwW7/8EA8nnFQjf+qPvA+wWLLfPKoiFPUezc5m9YgN2A7jmzwo6YmEAXeHyqO9tvTw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3326,8 +3201,8 @@ packages: resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} engines: {node: '>=14'} - '@oxc-project/types@0.120.0': - resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -4245,97 +4120,97 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@rolldown/binding-android-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.10': - resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': - resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': - resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.10': - resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -5998,9 +5873,6 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -6153,8 +6025,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-json@4.4.1: - resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} component-emitter@1.3.1: @@ -6755,14 +6627,14 @@ packages: resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.17.1: - resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} @@ -7002,10 +6874,6 @@ packages: exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} @@ -7086,8 +6954,8 @@ packages: file-saver@2.0.5: resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} - file-type@21.3.2: - resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} filelist@1.0.4: @@ -7424,10 +7292,6 @@ packages: typescript: optional: true - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -9617,10 +9481,6 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9634,8 +9494,8 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.0.0-rc.10: - resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -9753,10 +9613,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-javascript@7.0.3: - resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} - engines: {node: '>=20.0.0'} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -10050,10 +9906,6 @@ packages: tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -10062,8 +9914,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -10195,8 +10047,8 @@ packages: jest-util: optional: true - ts-loader@9.5.4: - resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} + ts-loader@9.5.7: + resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} engines: {node: '>=12.0.0'} peerDependencies: typescript: '*' @@ -10484,14 +10336,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@8.0.1: - resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + vite@8.0.5: + resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -10585,12 +10437,12 @@ packages: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} - webpack@5.104.1: - resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} + webpack@5.105.4: + resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -10825,10 +10677,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} - engines: {node: '>=18'} - yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} @@ -10908,18 +10756,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@angular-devkit/core@19.2.17(chokidar@4.0.3)': - dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - jsonc-parser: 3.3.1 - picomatch: 4.0.4 - rxjs: 7.8.1 - source-map: 0.7.4 - optionalDependencies: - chokidar: 4.0.3 - - '@angular-devkit/core@19.2.19(chokidar@4.0.3)': + '@angular-devkit/core@19.2.23(chokidar@4.0.3)': dependencies: ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) @@ -10930,10 +10767,10 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.19(@types/node@25.5.0)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.23(@types/node@25.5.0)(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.19(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) '@inquirer/prompts': 7.3.2(@types/node@25.5.0) ansi-colors: 4.1.3 symbol-observable: 4.0.0 @@ -10942,19 +10779,9 @@ snapshots: - '@types/node' - chokidar - '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': - dependencies: - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) - jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - - '@angular-devkit/schematics@19.2.19(chokidar@4.0.3)': + '@angular-devkit/schematics@19.2.23(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) jsonc-parser: 3.3.1 magic-string: 0.30.17 ora: 5.4.1 @@ -12333,11 +12160,11 @@ snapshots: '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 + optional: true '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 - optional: true '@emnapi/wasi-threads@1.1.0': dependencies: @@ -12785,16 +12612,6 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.1.2(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@25.5.0) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/checkbox@4.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -12812,26 +12629,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/confirm@5.1.6(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - optionalDependencies: - '@types/node': 25.5.0 - - '@inquirer/core@10.1.7(@types/node@25.5.0)': - dependencies: - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@25.5.0) - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/core@10.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -12853,14 +12650,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/editor@4.2.7(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - external-editor: 3.1.0 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/expand@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -12869,14 +12658,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/expand@4.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/external-editor@1.0.3(@types/node@25.5.0)': dependencies: chardet: 2.1.1 @@ -12884,17 +12665,8 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/figures@1.0.10': {} - '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.1.6(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/input@4.3.1(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -12909,13 +12681,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/number@3.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/password@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -12924,14 +12689,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/password@4.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - ansi-escapes: 4.3.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/prompts@7.10.1(@types/node@25.5.0)': dependencies: '@inquirer/checkbox': 4.3.2(@types/node@25.5.0) @@ -12949,24 +12706,16 @@ snapshots: '@inquirer/prompts@7.3.2(@types/node@25.5.0)': dependencies: - '@inquirer/checkbox': 4.1.2(@types/node@25.5.0) - '@inquirer/confirm': 5.1.6(@types/node@25.5.0) - '@inquirer/editor': 4.2.7(@types/node@25.5.0) - '@inquirer/expand': 4.0.9(@types/node@25.5.0) - '@inquirer/input': 4.1.6(@types/node@25.5.0) - '@inquirer/number': 3.0.9(@types/node@25.5.0) - '@inquirer/password': 4.0.9(@types/node@25.5.0) - '@inquirer/rawlist': 4.0.9(@types/node@25.5.0) - '@inquirer/search': 3.0.9(@types/node@25.5.0) - '@inquirer/select': 4.0.9(@types/node@25.5.0) - optionalDependencies: - '@types/node': 25.5.0 - - '@inquirer/rawlist@4.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/type': 3.0.4(@types/node@25.5.0) - yoctocolors-cjs: 2.1.2 + '@inquirer/checkbox': 4.3.2(@types/node@25.5.0) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@inquirer/editor': 4.2.23(@types/node@25.5.0) + '@inquirer/expand': 4.0.23(@types/node@25.5.0) + '@inquirer/input': 4.3.1(@types/node@25.5.0) + '@inquirer/number': 3.0.23(@types/node@25.5.0) + '@inquirer/password': 4.0.23(@types/node@25.5.0) + '@inquirer/rawlist': 4.1.11(@types/node@25.5.0) + '@inquirer/search': 3.2.2(@types/node@25.5.0) + '@inquirer/select': 4.4.2(@types/node@25.5.0) optionalDependencies: '@types/node': 25.5.0 @@ -12978,15 +12727,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/search@3.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@25.5.0) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/search@3.2.2(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -12996,16 +12736,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/select@4.0.9(@types/node@25.5.0)': - dependencies: - '@inquirer/core': 10.1.7(@types/node@25.5.0) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@25.5.0) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 25.5.0 - '@inquirer/select@4.4.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -13020,10 +12750,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/type@3.0.4(@types/node@25.5.0)': - optionalDependencies: - '@types/node': 25.5.0 - '@ioredis/commands@1.5.0': {} '@ioredis/commands@1.5.1': {} @@ -13485,7 +13211,7 @@ snapshots: '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.9.1 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.1.1': @@ -13495,63 +13221,63 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)': + '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2) ioredis: 5.10.1 reflect-metadata: 0.2.2 tslib: 2.8.1 - '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1)': + '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(ioredis@5.10.1)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) ioredis: 5.10.1 tslib: 2.8.1 - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(bullmq@5.71.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.71.0)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) bullmq: 5.71.0 tslib: 2.8.1 - '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.2.8 keyv: 5.6.0 rxjs: 7.8.2 - '@nestjs/cli@11.0.16(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)': + '@nestjs/cli@11.0.18(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)': dependencies: - '@angular-devkit/core': 19.2.19(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.19(@types/node@25.5.0)(chokidar@4.0.3) + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.23(@types/node@25.5.0)(chokidar@4.0.3) '@inquirer/prompts': 7.10.1(@types/node@25.5.0) - '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + '@nestjs/schematics': 11.0.10(chokidar@4.0.3)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.5.25(@swc/helpers@0.5.5) @@ -13561,9 +13287,9 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.3.2 + file-type: 21.3.4 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -13576,17 +13302,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.3(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 17.2.3 dotenv-expand: 12.0.3 lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -13596,39 +13322,39 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/websockets': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/passport@11.0.5(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.1.17(@fastify/static@9.0.0)(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/platform-fastify@11.1.18(@fastify/static@9.0.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: '@fastify/cors': 11.2.0 '@fastify/formbody': 8.0.2 - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 fastify: 5.8.3 fastify-plugin: 5.1.0 @@ -13640,10 +13366,10 @@ snapshots: optionalDependencies: '@fastify/static': 9.0.0 - '@nestjs/platform-socket.io@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -13652,55 +13378,55 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/schedule@6.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 - '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': + '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) - comment-json: 4.4.1 + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + comment-json: 4.6.2 jsonc-parser: 3.3.1 pluralize: 8.0.0 typescript: 5.9.3 transitivePeerDependencies: - chokidar - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 - '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/websockets@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -13757,7 +13483,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.23.7(@babel/core@7.28.5) '@babel/preset-env': 7.23.8(@babel/core@7.28.5) '@babel/preset-typescript': 7.23.3(@babel/core@7.28.5) - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.29.2 '@nx/devkit': 22.6.1(nx@22.6.1) '@nx/workspace': 22.6.1 '@zkochan/js-yaml': 0.0.7 @@ -13907,7 +13633,7 @@ snapshots: '@opentelemetry/semantic-conventions@1.39.0': {} - '@oxc-project/types@0.120.0': {} + '@oxc-project/types@0.122.0': {} '@paralleldrive/cuid2@2.3.1': dependencies: @@ -14829,54 +14555,54 @@ snapshots: '@remirror/core-constants@3.0.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.10': + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.10': + '@rolldown/binding-darwin-x64@1.0.0-rc.12': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': optional: true - '@rolldown/pluginutils@1.0.0-rc.10': {} + '@rolldown/pluginutils@1.0.0-rc.12': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -16211,10 +15937,10 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.1(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@webassemblyjs/ast@1.14.1': dependencies: @@ -16323,9 +16049,9 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-phases@1.0.4(acorn@8.15.0): + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -16584,7 +16310,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.29.2 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -16818,8 +16544,6 @@ snapshots: char-regex@1.0.2: {} - chardet@0.7.0: {} - chardet@2.1.1: {} check-disk-space@3.4.0: {} @@ -16984,10 +16708,9 @@ snapshots: commander@8.3.0: {} - comment-json@4.4.1: + comment-json@4.6.2: dependencies: array-timsort: 1.0.3 - core-util-is: 1.0.3 esprima: 4.0.1 component-emitter@1.3.1: {} @@ -17608,12 +17331,12 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.17.1: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 - enhanced-resolve@5.19.0: + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -18072,12 +17795,6 @@ snapshots: exsolve@1.0.7: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.2.5 - fast-copy@4.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -18145,7 +17862,7 @@ snapshots: fastq@1.17.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 fb-watchman@2.0.2: dependencies: @@ -18167,7 +17884,7 @@ snapshots: file-saver@2.0.5: {} - file-type@21.3.2: + file-type@21.3.4: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 @@ -18230,7 +17947,7 @@ snapshots: dependencies: is-callable: 1.2.7 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -18243,9 +17960,9 @@ snapshots: node-abort-controller: 3.1.1 schema-utils: 3.3.0 semver: 7.7.4 - tapable: 2.2.1 + tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) form-data@4.0.5: dependencies: @@ -18544,10 +18261,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -19872,23 +19585,23 @@ snapshots: neo-async@2.6.2: {} - nestjs-cls@6.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(kysely@0.28.14)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(kysely@0.28.14)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.14 reflect-metadata: 0.2.2 - nestjs-pino@4.6.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + nestjs-pino@4.6.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) pino: 10.1.0 pino-http: 11.0.0 rxjs: 7.8.2 @@ -19952,7 +19665,7 @@ snapshots: flat: 5.0.2 front-matter: 4.0.2 ignore: 7.0.5 - jest-diff: 30.2.0 + jest-diff: 30.3.0 jsonc-parser: 3.2.0 lines-and-columns: 2.0.3 minimatch: 10.2.4 @@ -21090,8 +20803,6 @@ snapshots: ret@0.5.0: {} - reusify@1.0.4: {} - reusify@1.1.0: {} rfc6902@5.2.0: {} @@ -21100,26 +20811,26 @@ snapshots: robust-predicates@3.0.2: {} - rolldown@1.0.0-rc.10: + rolldown@1.0.0-rc.12: dependencies: - '@oxc-project/types': 0.120.0 - '@rolldown/pluginutils': 1.0.0-rc.10 + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-x64': 1.0.0-rc.10 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 rope-sequence@1.3.4: {} @@ -21268,8 +20979,6 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@7.0.3: {} - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -21622,8 +21331,6 @@ snapshots: tailwindcss@4.1.18: {} - tapable@2.2.1: {} - tapable@2.3.0: {} tar-stream@2.2.0: @@ -21634,21 +21341,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - terser-webpack-plugin@5.3.16(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))): + terser-webpack-plugin@5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - serialize-javascript: 7.0.3 terser: 5.39.0 - webpack: 5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) optionalDependencies: '@swc/core': 1.5.25(@swc/helpers@0.5.5) terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -21745,15 +21451,15 @@ snapshots: babel-jest: 30.3.0(@babel/core@7.28.5) jest-util: 30.3.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))): + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.19.0 + enhanced-resolve: 5.20.1 micromatch: 4.0.8 semver: 7.7.4 source-map: 0.7.4 typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3): dependencies: @@ -21778,8 +21484,8 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.17.1 - tapable: 2.2.1 + enhanced-resolve: 5.19.0 + tapable: 2.3.0 tsconfig-paths: 4.2.0 tsconfig-paths@4.2.0: @@ -22060,12 +21766,12 @@ snapshots: vary@1.1.2: {} - vite@8.0.1(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.8 - rolldown: 1.0.0-rc.10 + rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.19.1 @@ -22126,9 +21832,9 @@ snapshots: webpack-node-externals@3.0.0: {} - webpack-sources@3.3.3: {} + webpack-sources@3.3.4: {} - webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5)): + webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -22136,11 +21842,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.19.0 + enhanced-resolve: 5.20.1 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -22152,9 +21858,9 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.104.1(@swc/core@1.5.25(@swc/helpers@0.5.5))) + terser-webpack-plugin: 5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild @@ -22403,8 +22109,6 @@ snapshots: yocto-queue@1.2.1: {} - yoctocolors-cjs@2.1.2: {} - yoctocolors-cjs@2.1.3: {} yoctocolors@2.1.2: {} From da9b43681e0e087eb10fe38e05dd7a94e8203377 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:37:51 +0100 Subject: [PATCH 04/22] feat: watch space (#2096) --- .../public/locales/en-US/translation.json | 4 + .../components/sidebar/space-sidebar.tsx | 143 ++++++++++++------ .../space/queries/space-watcher-query.ts | 49 ++++++ .../space/services/space-watcher-service.ts | 28 ++++ .../services/page.notification.ts | 18 ++- .../src/core/watcher/dto/space-watcher.dto.ts | 7 + .../core/watcher/space-watcher.controller.ts | 95 ++++++++++++ .../src/core/watcher/watcher.controller.ts | 7 +- .../server/src/core/watcher/watcher.module.ts | 3 +- .../src/core/watcher/watcher.service.ts | 34 ++++- .../database/repos/watcher/watcher.repo.ts | 130 +++++++++++++--- .../emails/page-update-email.tsx | 6 +- 12 files changed, 449 insertions(+), 75 deletions(-) create mode 100644 apps/client/src/features/space/queries/space-watcher-query.ts create mode 100644 apps/client/src/features/space/services/space-watcher-service.ts create mode 100644 apps/server/src/core/watcher/dto/space-watcher.dto.ts create mode 100644 apps/server/src/core/watcher/space-watcher.controller.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 3ecf103bd0..5db91598f7 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -677,6 +677,8 @@ "{{name}} updated a page": "{{name}} updated a page", "Watch page": "Watch page", "Stop watching": "Stop watching", + "Watch space": "Watch space", + "Stop watching space": "Stop watching space", "Email notifications": "Email notifications", "Page updates": "Page updates", "Get notified when pages you watch are updated.": "Receive notifications when the pages you watch are updated.", @@ -690,6 +692,8 @@ "Get notified when your comment is resolved.": "Receive a notification when your comment is resolved.", "You are now watching this page": "You’re now watching this page", "You are no longer watching this page": "You’re no longer watching this page", + "You are now watching this space": "You’re now watching this space", + "You are no longer watching this space": "You’re no longer watching this space", "Direct": "Direct", "Updates": "Updates", "Today": "Today", diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx index ced237be93..d80322125a 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx @@ -9,6 +9,8 @@ import { import { IconArrowDown, IconDots, + IconEye, + IconEyeOff, IconFileExport, IconHome, IconPlus, @@ -16,6 +18,11 @@ import { IconSettings, IconTrash, } from "@tabler/icons-react"; +import { + useSpaceWatchStatusQuery, + useWatchSpaceMutation, + useUnwatchSpaceMutation, +} from "@/features/space/queries/space-watcher-query.ts"; import classes from "./space-sidebar.module.css"; import React from "react"; import { useAtom } from "jotai"; @@ -160,13 +167,20 @@ export function SpaceSidebar() { {t("Pages")} - {spaceAbility.can( - SpaceCaslAction.Manage, - SpaceCaslSubject.Page, - ) && ( - - + + + {spaceAbility.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Page, + ) && ( - - )} + )} +
@@ -204,9 +218,14 @@ export function SpaceSidebar() { interface SpaceMenuProps { spaceId: string; + canManagePages: boolean; onSpaceSettings: () => void; } -function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) { +function SpaceMenu({ + spaceId, + canManagePages, + onSpaceSettings, +}: SpaceMenuProps) { const { t } = useTranslation(); const { spaceSlug } = useParams(); const [importOpened, { open: openImportModal, close: closeImportModal }] = @@ -214,15 +233,24 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) { const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); + const { data: watchStatus } = useSpaceWatchStatusQuery(spaceId); + const watchMutation = useWatchSpaceMutation(); + const unwatchMutation = useUnwatchSpaceMutation(); + const isWatching = watchStatus?.watching ?? false; + + const handleToggleWatch = () => { + if (isWatching) { + unwatchMutation.mutate(spaceId); + } else { + watchMutation.mutate(spaceId); + } + }; + return ( <> - + } + onClick={handleToggleWatch} + leftSection={ + isWatching ? : + } > - {t("Import pages")} + {isWatching ? t("Stop watching space") : t("Watch space")} - } - > - {t("Export space")} - + {canManagePages && ( + <> + - + } + > + {t("Import pages")} + - } - > - {t("Space settings")} - + } + > + {t("Export space")} + - } - > - {t("Trash")} - + + + } + > + {t("Space settings")} + + + } + > + {t("Trash")} + + + )} - + {canManagePages && ( + <> + - + + + )} ); } diff --git a/apps/client/src/features/space/queries/space-watcher-query.ts b/apps/client/src/features/space/queries/space-watcher-query.ts new file mode 100644 index 0000000000..ae4d56967a --- /dev/null +++ b/apps/client/src/features/space/queries/space-watcher-query.ts @@ -0,0 +1,49 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + watchSpace, + unwatchSpace, + getSpaceWatchStatus, +} from "@/features/space/services/space-watcher-service"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; + +const SPACE_WATCHER_KEY = "space-watcher"; + +export function useSpaceWatchStatusQuery(spaceId: string) { + return useQuery({ + queryKey: [SPACE_WATCHER_KEY, spaceId], + queryFn: () => getSpaceWatchStatus(spaceId), + enabled: !!spaceId, + staleTime: 60_000, + }); +} + +export function useWatchSpaceMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + return useMutation({ + mutationFn: (spaceId: string) => watchSpace(spaceId), + onSuccess: (_data, spaceId) => { + queryClient.setQueryData([SPACE_WATCHER_KEY, spaceId], { + watching: true, + }); + notifications.show({ message: t("You are now watching this space") }); + }, + }); +} + +export function useUnwatchSpaceMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + return useMutation({ + mutationFn: (spaceId: string) => unwatchSpace(spaceId), + onSuccess: (_data, spaceId) => { + queryClient.setQueryData([SPACE_WATCHER_KEY, spaceId], { + watching: false, + }); + notifications.show({ + message: t("You are no longer watching this space"), + }); + }, + }); +} diff --git a/apps/client/src/features/space/services/space-watcher-service.ts b/apps/client/src/features/space/services/space-watcher-service.ts new file mode 100644 index 0000000000..bcbeccc9b7 --- /dev/null +++ b/apps/client/src/features/space/services/space-watcher-service.ts @@ -0,0 +1,28 @@ +import api from "@/lib/api-client"; + +export async function watchSpace( + spaceId: string, +): Promise<{ watching: boolean }> { + const req = await api.post<{ watching: boolean }>("/spaces/watch", { + spaceId, + }); + return req.data; +} + +export async function unwatchSpace( + spaceId: string, +): Promise<{ watching: boolean }> { + const req = await api.post<{ watching: boolean }>("/spaces/unwatch", { + spaceId, + }); + return req.data; +} + +export async function getSpaceWatchStatus( + spaceId: string, +): Promise<{ watching: boolean }> { + const req = await api.post<{ watching: boolean }>("/spaces/watch-status", { + spaceId, + }); + return req.data; +} diff --git a/apps/server/src/core/notification/services/page.notification.ts b/apps/server/src/core/notification/services/page.notification.ts index 9e5c75dd85..77ab967aad 100644 --- a/apps/server/src/core/notification/services/page.notification.ts +++ b/apps/server/src/core/notification/services/page.notification.ts @@ -179,7 +179,11 @@ export class PageNotificationService { async processPageUpdate(data: IPageUpdateNotificationJob, appUrl: string) { const { pageId, spaceId, workspaceId, actorIds } = data; - const watcherIds = await this.watcherRepo.getPageWatcherIds(pageId); + const watcherIds = await this.watcherRepo.getPageUpdateRecipientIds( + pageId, + spaceId, + ); + if (watcherIds.length === 0) return; const actorSet = new Set(actorIds); @@ -219,7 +223,7 @@ export class PageNotificationService { const context = await this.getPageContext(actorId, pageId, spaceId, appUrl); if (!context) return; - const { actor, pageTitle, basePageUrl } = context; + const { actor, pageTitle, basePageUrl, spaceName } = context; for (const userId of recipientIds) { const notification = await this.notificationService.create({ @@ -243,6 +247,7 @@ export class PageNotificationService { actorName: actor.name, pageTitle, pageUrl: basePageUrl, + spaceName, }), NotificationType.PAGE_UPDATED, ); @@ -421,7 +426,7 @@ export class PageNotificationService { .executeTakeFirst(), this.db .selectFrom('spaces') - .select(['id', 'slug']) + .select(['id', 'slug', 'name']) .where('id', '=', spaceId) .executeTakeFirst(), ]); @@ -432,6 +437,11 @@ export class PageNotificationService { const basePageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`; - return { actor, pageTitle: getPageTitle(page.title), basePageUrl }; + return { + actor, + pageTitle: getPageTitle(page.title), + basePageUrl, + spaceName: space.name, + }; } } diff --git a/apps/server/src/core/watcher/dto/space-watcher.dto.ts b/apps/server/src/core/watcher/dto/space-watcher.dto.ts new file mode 100644 index 0000000000..1df06010d0 --- /dev/null +++ b/apps/server/src/core/watcher/dto/space-watcher.dto.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SpaceWatcherDto { + @IsString() + @IsNotEmpty() + spaceId: string; +} diff --git a/apps/server/src/core/watcher/space-watcher.controller.ts b/apps/server/src/core/watcher/space-watcher.controller.ts new file mode 100644 index 0000000000..455c7d0d9a --- /dev/null +++ b/apps/server/src/core/watcher/space-watcher.controller.ts @@ -0,0 +1,95 @@ +import { + Body, + Controller, + ForbiddenException, + HttpCode, + HttpStatus, + NotFoundException, + Post, + UseGuards, +} from '@nestjs/common'; +import { WatcherService } from './watcher.service'; +import { AuthUser } from '../../common/decorators/auth-user.decorator'; +import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { User, Workspace } from '@docmost/db/types/entity.types'; +import { SpaceWatcherDto } from './dto/space-watcher.dto'; +import { SpaceRepo } from '@docmost/db/repos/space/space.repo'; +import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from '../casl/interfaces/space-ability.type'; + +@UseGuards(JwtAuthGuard) +@Controller('spaces') +export class SpaceWatcherController { + constructor( + private readonly watcherService: WatcherService, + private readonly spaceRepo: SpaceRepo, + private readonly spaceAbility: SpaceAbilityFactory, + ) {} + + private async loadSpaceAndAuthorize( + spaceId: string, + user: User, + workspace: Workspace, + ) { + const space = await this.spaceRepo.findById(spaceId, workspace.id); + if (!space) { + throw new NotFoundException('Space not found'); + } + + const ability = await this.spaceAbility.createForUser(user, space.id); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Settings)) { + throw new ForbiddenException(); + } + + return space; + } + + @HttpCode(HttpStatus.OK) + @Post('watch') + async watchSpace( + @Body() dto: SpaceWatcherDto, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + const space = await this.loadSpaceAndAuthorize(dto.spaceId, user, workspace); + + await this.watcherService.watchSpace(user.id, space.id, workspace.id); + + return { watching: true }; + } + + @HttpCode(HttpStatus.OK) + @Post('unwatch') + async unwatchSpace( + @Body() dto: SpaceWatcherDto, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + const space = await this.loadSpaceAndAuthorize(dto.spaceId, user, workspace); + + await this.watcherService.unwatchSpace(user.id, space.id); + + return { watching: false }; + } + + @HttpCode(HttpStatus.OK) + @Post('watch-status') + async getWatchStatus( + @Body() dto: SpaceWatcherDto, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + const space = await this.loadSpaceAndAuthorize(dto.spaceId, user, workspace); + + const watching = await this.watcherService.isWatchingSpace( + user.id, + space.id, + ); + + return { watching }; + } +} diff --git a/apps/server/src/core/watcher/watcher.controller.ts b/apps/server/src/core/watcher/watcher.controller.ts index cd10fa3795..24c317f682 100644 --- a/apps/server/src/core/watcher/watcher.controller.ts +++ b/apps/server/src/core/watcher/watcher.controller.ts @@ -59,7 +59,12 @@ export class WatcherController { await this.pageAccessService.validateCanView(page, user); - await this.watcherService.unwatchPage(user.id, page.id); + await this.watcherService.unwatchPage( + user.id, + page.id, + page.spaceId, + page.workspaceId, + ); return { watching: false }; } diff --git a/apps/server/src/core/watcher/watcher.module.ts b/apps/server/src/core/watcher/watcher.module.ts index 76267b5ab2..357b835207 100644 --- a/apps/server/src/core/watcher/watcher.module.ts +++ b/apps/server/src/core/watcher/watcher.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { WatcherService } from './watcher.service'; import { WatcherController } from './watcher.controller'; +import { SpaceWatcherController } from './space-watcher.controller'; import { PageAccessModule } from '../page/page-access/page-access.module'; @Module({ imports: [PageAccessModule], - controllers: [WatcherController], + controllers: [WatcherController, SpaceWatcherController], providers: [WatcherService], exports: [WatcherService], }) diff --git a/apps/server/src/core/watcher/watcher.service.ts b/apps/server/src/core/watcher/watcher.service.ts index 384a0787bd..3c5fe621be 100644 --- a/apps/server/src/core/watcher/watcher.service.ts +++ b/apps/server/src/core/watcher/watcher.service.ts @@ -50,14 +50,44 @@ export class WatcherService { return this.watcherRepo.insertMany(watchers, trx); } - async unwatchPage(userId: string, pageId: string) { - return this.watcherRepo.mute(userId, pageId); + async unwatchPage( + userId: string, + pageId: string, + spaceId: string, + workspaceId: string, + ) { + return this.watcherRepo.mute(userId, pageId, spaceId, workspaceId); } async isWatchingPage(userId: string, pageId: string): Promise { return this.watcherRepo.isWatching(userId, pageId); } + async watchSpace( + userId: string, + spaceId: string, + workspaceId: string, + trx?: KyselyTransaction, + ) { + const watcher: InsertableWatcher = { + userId, + pageId: null, + spaceId, + workspaceId, + type: WatcherType.SPACE, + addedById: userId, + }; + return this.watcherRepo.upsertSpace(watcher, trx); + } + + async unwatchSpace(userId: string, spaceId: string) { + return this.watcherRepo.deleteSpaceWatch(userId, spaceId); + } + + async isWatchingSpace(userId: string, spaceId: string): Promise { + return this.watcherRepo.isWatchingSpace(userId, spaceId); + } + async getPageWatchers(pageId: string, pagination: PaginationOptions) { return this.watcherRepo.findPageWatchers(pageId, pagination); } diff --git a/apps/server/src/database/repos/watcher/watcher.repo.ts b/apps/server/src/database/repos/watcher/watcher.repo.ts index 9739b4deb6..f1506ff928 100644 --- a/apps/server/src/database/repos/watcher/watcher.repo.ts +++ b/apps/server/src/database/repos/watcher/watcher.repo.ts @@ -20,18 +20,6 @@ export type WatcherType = (typeof WatcherType)[keyof typeof WatcherType]; export class WatcherRepo { constructor(@InjectKysely() private readonly db: KyselyDB) {} - async findByUserAndPage( - userId: string, - pageId: string, - ): Promise { - return this.db - .selectFrom('watchers') - .selectAll() - .where('userId', '=', userId) - .where('pageId', '=', pageId) - .executeTakeFirst(); - } - async findPageWatchers(pageId: string, pagination: PaginationOptions) { const query = this.db .selectFrom('watchers') @@ -66,6 +54,53 @@ export class WatcherRepo { return watchers.map((w) => w.userId); } + /** + * Recipients for a `page.updated` notification, combining: + * - Active page watchers on this page, AND + * - Active space watchers on this space, EXCLUDING any user who has a + * muted page watcher row for this page (per-page mute always wins). + * + * Deduplicated at the SQL level — a user watching both the page and the + * containing space appears once. + */ + async getPageUpdateRecipientIds( + pageId: string, + spaceId: string, + trx?: KyselyTransaction, + ): Promise { + const db = dbOrTx(this.db, trx); + + const pageWatchers = db + .selectFrom('watchers') + .select('userId') + .where('pageId', '=', pageId) + .where('type', '=', WatcherType.PAGE) + .where('mutedAt', 'is', null); + + const spaceWatchers = db + .selectFrom('watchers as sw') + .select('sw.userId') + .where('sw.spaceId', '=', spaceId) + .where('sw.pageId', 'is', null) + .where('sw.type', '=', WatcherType.SPACE) + .where((eb) => + eb.not( + eb.exists( + eb + .selectFrom('watchers as pw') + .select('pw.id') + .whereRef('pw.userId', '=', 'sw.userId') + .where('pw.pageId', '=', pageId) + .where('pw.type', '=', WatcherType.PAGE) + .where('pw.mutedAt', 'is not', null), + ), + ), + ); + + const rows = await pageWatchers.union(spaceWatchers).execute(); + return [...new Set(rows.map((r) => r.userId))]; + } + async insert( watcher: InsertableWatcher, trx?: KyselyTransaction, @@ -110,20 +145,81 @@ export class WatcherRepo { .executeTakeFirst(); } + async upsertSpace( + watcher: InsertableWatcher, + trx?: KyselyTransaction, + ): Promise { + const db = dbOrTx(this.db, trx); + return db + .insertInto('watchers') + .values(watcher) + .onConflict((oc) => + oc + .columns(['userId', 'spaceId']) + .where('pageId', 'is', null) + .doNothing(), + ) + .returningAll() + .executeTakeFirst(); + } + async mute( userId: string, pageId: string, + spaceId: string, + workspaceId: string, trx?: KyselyTransaction, ): Promise { const db = dbOrTx(this.db, trx); + const mutedAt = new Date(); await db - .updateTable('watchers') - .set({ mutedAt: new Date() }) + .insertInto('watchers') + .values({ + userId, + pageId, + spaceId, + workspaceId, + type: WatcherType.PAGE, + addedById: userId, + mutedAt, + }) + .onConflict((oc) => + oc + .columns(['userId', 'pageId']) + .where('pageId', 'is not', null) + .doUpdateSet({ mutedAt }), + ) + .execute(); + } + + async deleteSpaceWatch( + userId: string, + spaceId: string, + trx?: KyselyTransaction, + ): Promise { + const db = dbOrTx(this.db, trx); + await db + .deleteFrom('watchers') .where('userId', '=', userId) - .where('pageId', '=', pageId) + .where('spaceId', '=', spaceId) + .where('pageId', 'is', null) + .where('type', '=', WatcherType.SPACE) .execute(); } + async isWatchingSpace(userId: string, spaceId: string): Promise { + const watcher = await this.db + .selectFrom('watchers') + .select('id') + .where('userId', '=', userId) + .where('spaceId', '=', spaceId) + .where('pageId', 'is', null) + .where('type', '=', WatcherType.SPACE) + .executeTakeFirst(); + + return !!watcher; + } + async isWatching(userId: string, pageId: string): Promise { const watcher = await this.db .selectFrom('watchers') @@ -164,14 +260,14 @@ export class WatcherRepo { .where('spaceId', '=', spaceId) .where('userId', 'is not', null) .union( - this.db + db .selectFrom('spaceMembers') .innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId') .select('groupUsers.userId') .where('spaceMembers.spaceId', '=', spaceId), ); - await this.db + await db .deleteFrom('watchers') .where('userId', 'in', userIds) .where('spaceId', '=', spaceId) diff --git a/apps/server/src/integrations/transactional/emails/page-update-email.tsx b/apps/server/src/integrations/transactional/emails/page-update-email.tsx index 188d8a3415..c4c8576905 100644 --- a/apps/server/src/integrations/transactional/emails/page-update-email.tsx +++ b/apps/server/src/integrations/transactional/emails/page-update-email.tsx @@ -8,6 +8,7 @@ interface Props { actorName: string; pageTitle: string; pageUrl: string; + spaceName: string; } export const PageUpdateEmail = ({ @@ -15,6 +16,7 @@ export const PageUpdateEmail = ({ actorName, pageTitle, pageUrl, + spaceName, }: Props) => { return ( @@ -24,8 +26,8 @@ export const PageUpdateEmail = ({ {actorName} updated{' '} {pageTitle} - - . + {' '} + in {spaceName}. View page From 57efb91bd3ad626bcbc360f344be796018c10572 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:23:47 +0100 Subject: [PATCH 05/22] feat(ee): ai chat (#2098) * feat: ai chat * feat: ai chat * sync * cleanup * view space button --- .../public/locales/en-US/translation.json | 30 +- apps/client/src/App.tsx | 3 + apps/client/src/components/common/copy.tsx | 7 +- .../layouts/global/app-header.module.css | 16 + .../components/layouts/global/app-header.tsx | 94 +++- .../src/components/layouts/global/aside.tsx | 17 +- .../layouts/global/global-app-shell.tsx | 5 +- .../components/ui/card-carousel.module.css | 68 +++ .../src/components/ui/card-carousel.tsx | 77 ++++ .../ee/ai-chat/components/ai-chat-layout.tsx | 106 +++++ .../components/ai-chat-sidebar-item.tsx | 166 +++++++ .../ee/ai-chat/components/ai-chat-sidebar.tsx | 224 ++++++++++ .../ai-chat/components/aside-chat-history.tsx | 67 +++ .../ai-chat/components/aside-chat-panel.tsx | 258 +++++++++++ .../ai-chat/components/chat-empty-state.tsx | 91 ++++ .../src/ee/ai-chat/components/chat-input.tsx | 409 ++++++++++++++++++ .../ai-chat/components/chat-message-list.tsx | 174 ++++++++ .../ee/ai-chat/components/chat-message.tsx | 139 ++++++ .../ee/ai-chat/components/chat-tool-group.tsx | 56 +++ .../ai-chat/components/chat-tool-result.tsx | 49 +++ .../ee/ai-chat/components/enable-ai-chat.tsx | 67 +++ .../src/ee/ai-chat/hooks/use-chat-stream.ts | 227 ++++++++++ apps/client/src/ee/ai-chat/pages/ai-chat.tsx | 39 ++ .../src/ee/ai-chat/queries/ai-chat-query.ts | 61 +++ .../ee/ai-chat/services/ai-chat-service.ts | 144 ++++++ .../src/ee/ai-chat/styles/ai-chat.module.css | 169 ++++++++ .../styles/aside-chat-panel.module.css | 139 ++++++ .../ee/ai-chat/styles/chat-input.module.css | 242 +++++++++++ .../ee/ai-chat/styles/chat-message.module.css | 286 ++++++++++++ .../ee/ai-chat/styles/chat-sidebar.module.css | 138 ++++++ .../src/ee/ai-chat/types/ai-chat.types.ts | 49 +++ apps/client/src/ee/ai/pages/ai-settings.tsx | 2 + .../components/mention/mention-list.tsx | 2 +- .../components/mention/mention-suggestion.ts | 4 +- .../home/components/home-ai-prompt.module.css | 28 ++ .../home/components/home-ai-prompt.tsx | 60 +++ .../components/search-control.module.css | 2 + .../components/space-carousel.module.css | 22 + .../space/components/space-carousel.tsx | 77 ++++ .../workspace/types/workspace.types.ts | 1 + apps/client/src/pages/dashboard/home.tsx | 9 +- apps/server/package.json | 2 + .../core/attachment/attachment.constants.ts | 1 + .../core/attachment/attachment.controller.ts | 30 +- .../src/core/attachment/attachment.utils.ts | 2 + .../processors/attachment.processor.ts | 5 + .../attachment/services/attachment.service.ts | 25 ++ apps/server/src/core/auth/auth.controller.ts | 11 +- .../workspace/dto/update-workspace.dto.ts | 4 + .../workspace/services/workspace.service.ts | 17 +- .../migrations/20260409T132415-ai-chat.ts | 118 +++++ .../repos/attachment/attachment.repo.ts | 51 +++ apps/server/src/database/types/db.d.ts | 28 ++ .../server/src/database/types/entity.types.ts | 17 + apps/server/src/ee | 2 +- .../environment/environment.service.ts | 7 + .../src/integrations/export/export.service.ts | 2 +- .../queue/constants/queue.constants.ts | 1 + .../integrations/throttle/throttle.module.ts | 6 +- .../integrations/throttle/throttler-names.ts | 2 + .../throttle/user-throttler.guard.ts | 13 + .../src/lib/markdown/utils/marked.utils.ts | 7 +- pnpm-lock.yaml | 22 + 63 files changed, 4149 insertions(+), 48 deletions(-) create mode 100644 apps/client/src/components/ui/card-carousel.module.css create mode 100644 apps/client/src/components/ui/card-carousel.tsx create mode 100644 apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx create mode 100644 apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx create mode 100644 apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx create mode 100644 apps/client/src/ee/ai-chat/components/aside-chat-history.tsx create mode 100644 apps/client/src/ee/ai-chat/components/aside-chat-panel.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-empty-state.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-input.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-message-list.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-message.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-tool-group.tsx create mode 100644 apps/client/src/ee/ai-chat/components/chat-tool-result.tsx create mode 100644 apps/client/src/ee/ai-chat/components/enable-ai-chat.tsx create mode 100644 apps/client/src/ee/ai-chat/hooks/use-chat-stream.ts create mode 100644 apps/client/src/ee/ai-chat/pages/ai-chat.tsx create mode 100644 apps/client/src/ee/ai-chat/queries/ai-chat-query.ts create mode 100644 apps/client/src/ee/ai-chat/services/ai-chat-service.ts create mode 100644 apps/client/src/ee/ai-chat/styles/ai-chat.module.css create mode 100644 apps/client/src/ee/ai-chat/styles/aside-chat-panel.module.css create mode 100644 apps/client/src/ee/ai-chat/styles/chat-input.module.css create mode 100644 apps/client/src/ee/ai-chat/styles/chat-message.module.css create mode 100644 apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css create mode 100644 apps/client/src/ee/ai-chat/types/ai-chat.types.ts create mode 100644 apps/client/src/features/home/components/home-ai-prompt.module.css create mode 100644 apps/client/src/features/home/components/home-ai-prompt.tsx create mode 100644 apps/client/src/features/space/components/space-carousel.module.css create mode 100644 apps/client/src/features/space/components/space-carousel.tsx create mode 100644 apps/server/src/database/migrations/20260409T132415-ai-chat.ts create mode 100644 apps/server/src/integrations/throttle/throttler-names.ts create mode 100644 apps/server/src/integrations/throttle/user-throttler.guard.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 5db91598f7..b83b3548bb 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -627,6 +627,7 @@ "AI Answer": "AI Answer", "Ask AI": "Ask AI", "AI is thinking...": "AI is thinking...", + "Thinking": "Thinking", "Ask a question...": "Ask a question...", "AI Answers": "AI Answers", "AI-powered search (AI Answers)": "AI-powered search (AI Answers)", @@ -755,5 +756,32 @@ "Publish": "Publish", "Security": "Security", "Enforce SSO": "Enforce SSO", - "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password." + "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password.", + "AI-generated content may not be accurate.": "AI-generated content may not be accurate.", + "AI Chat": "AI Chat", + "Analyze for insights": "Analyze for insights", + "Ask anything...": "Ask anything...", + "Chat history": "Chat history", + "Chat name": "Chat name", + "Close": "Close", + "Docmost AI": "Docmost AI", + "Failed to load chat. An error occurred.": "Failed to load chat. An error occurred.", + "Failed to render this message.": "Failed to render this message.", + "How can I help you today?": "How can I help you today?", + "New chat": "New chat", + "No chat history": "No chat history", + "No chats found": "No chats found", + "No conversations yet": "No conversations yet", + "Open full page": "Open full page", + "Previous 7 days": "Previous 7 days", + "Previous 30 days": "Previous 30 days", + "Search chats...": "Search chats...", + "Start a new chat to see it here.": "Start a new chat to see it here.", + "Summarize this page": "Summarize this page", + "Toggle AI Chat": "Toggle AI Chat", + "Translate this page": "Translate this page", + "Try a different search term.": "Try a different search term.", + "Try again": "Try again", + "Untitled chat": "Untitled chat", + "What can I help you with?": "What can I help you with?" } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index b99e63b373..382660c750 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -38,6 +38,7 @@ import UserApiKeys from "@/ee/api-key/pages/user-api-keys"; import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys"; import AiSettings from "@/ee/ai/pages/ai-settings.tsx"; import AuditLogs from "@/ee/audit/pages/audit-logs.tsx"; +import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx"; import VerifyEmail from "@/ee/pages/verify-email.tsx"; export default function App() { @@ -81,6 +82,8 @@ export default function App() { }> } /> + } /> + } /> } /> } /> } /> diff --git a/apps/client/src/components/common/copy.tsx b/apps/client/src/components/common/copy.tsx index 81a7077114..745fc4ba66 100644 --- a/apps/client/src/components/common/copy.tsx +++ b/apps/client/src/components/common/copy.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Tooltip } from "@mantine/core"; +import { ActionIcon, MantineColor, MantineSize, Tooltip } from "@mantine/core"; import { CopyButton } from "@/components/common/copy-button"; import { IconCheck, IconCopy } from "@tabler/icons-react"; import React from "react"; @@ -6,8 +6,10 @@ import { useTranslation } from "react-i18next"; interface CopyProps { text: string; + size?: MantineSize; + color?: MantineColor; } -export default function CopyTextButton({ text }: CopyProps) { +export default function CopyTextButton({ text, size }: CopyProps) { const { t } = useTranslation(); return ( @@ -22,6 +24,7 @@ export default function CopyTextButton({ text }: CopyProps) { color={copied ? "teal" : "gray"} variant="subtle" onClick={copy} + size={size} > {copied ? : } diff --git a/apps/client/src/components/layouts/global/app-header.module.css b/apps/client/src/components/layouts/global/app-header.module.css index b2298f1358..a70b57e763 100644 --- a/apps/client/src/components/layouts/global/app-header.module.css +++ b/apps/client/src/components/layouts/global/app-header.module.css @@ -7,6 +7,19 @@ padding-right: var(--mantine-spacing-md); } +.brand { + display: flex; + align-items: center; + text-decoration: none; + color: inherit; + cursor: pointer; +} + +.brandIcon { + display: flex; + align-items: center; +} + .link { display: block; line-height: 1; @@ -16,6 +29,9 @@ color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); font-size: var(--mantine-font-size-sm); font-weight: 500; + user-select: none; + white-space: nowrap; + flex-shrink: 0; @mixin hover { background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); diff --git a/apps/client/src/components/layouts/global/app-header.tsx b/apps/client/src/components/layouts/global/app-header.tsx index 58b76b7162..9add5d7aa8 100644 --- a/apps/client/src/components/layouts/global/app-header.tsx +++ b/apps/client/src/components/layouts/global/app-header.tsx @@ -1,8 +1,18 @@ -import { Badge, Group, Text, Tooltip } from "@mantine/core"; +import { + ActionIcon, + Badge, + Box, + Group, + Text, + Tooltip, + UnstyledButton, +} from "@mantine/core"; import classes from "./app-header.module.css"; import React from "react"; import TopMenu from "@/components/layouts/global/top-menu.tsx"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; +import { IconSparkles } from "@tabler/icons-react"; +import useToggleAside from "@/hooks/use-toggle-aside.tsx"; import APP_ROUTE from "@/lib/app-route.ts"; import { useAtom } from "jotai"; import { @@ -23,8 +33,11 @@ import { shareSearchSpotlight, } from "@/features/search/constants.ts"; import { NotificationPopover } from "@/features/notification/components/notification-popover.tsx"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; -const links = [{ link: APP_ROUTE.HOME, label: "Home" }]; +const links = [ + { link: APP_ROUTE.HOME, label: "Home" }, +]; export function AppHeader() { const { t } = useTranslation(); @@ -34,9 +47,14 @@ export function AppHeader() { const [desktopOpened] = useAtom(desktopSidebarAtom); const toggleDesktop = useToggleSidebar(desktopSidebarAtom); const { isTrial, trialDaysLeft } = useTrial(); + const location = useLocation(); + const toggleAside = useToggleAside(); + const [workspace] = useAtom(workspaceAtom); + const aiChatEnabled = workspace?.settings?.ai?.chat === true; const isHomeRoute = location.pathname.startsWith("/home"); const isSpacesRoute = location.pathname === "/spaces"; + const isPageRoute = location.pathname.includes("/p/"); const hideSidebar = isHomeRoute || isSpacesRoute; const items = links.map((link) => ( @@ -73,15 +91,24 @@ export function AppHeader() { )} - - Docmost - + + + Docmost + + + Docmost + + {items} @@ -98,6 +125,49 @@ export function AppHeader() {
+ {aiChatEnabled && ( + <> + { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; + } + if (isPageRoute) { + e.preventDefault(); + toggleAside("chat"); + } + }} + > + {t("AI Chat")} + + + { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; + } + if (isPageRoute) { + e.preventDefault(); + toggleAside("chat"); + } + }} + > + + + + + )} {isCloud() && isTrial && trialDaysLeft !== 0 && ( ; title = "Table of contents"; break; + case "chat": + component = ; + title = "AI Chat"; + break; default: component = null; title = null; @@ -34,12 +39,14 @@ export default function Aside() { {component && ( <> - - {t(title)} - + {tab !== "chat" && ( + + {t(title)} + + )} - {tab === "comments" ? ( - + {tab === "comments" || tab === "chat" ? ( + component ) : ( -
+ {!isAiRoute &&
} {isSpaceRoute && } {isSettingsRoute && } + {isAiRoute && } )} diff --git a/apps/client/src/components/ui/card-carousel.module.css b/apps/client/src/components/ui/card-carousel.module.css new file mode 100644 index 0000000000..4ec513f234 --- /dev/null +++ b/apps/client/src/components/ui/card-carousel.module.css @@ -0,0 +1,68 @@ +.root { + position: relative; +} + +.track { + display: flex; + gap: var(--mantine-spacing-md); + overflow-x: auto; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 2px; + margin: -2px; +} + +.track::-webkit-scrollbar { + display: none; +} + +.track > * { + scroll-snap-align: start; + flex: 0 0 auto; +} + +.arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + cursor: pointer; + padding: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease, background-color 120ms ease, transform 120ms ease; + z-index: 2; +} + +.root:hover .arrow.visible, +.arrow.visible:focus-visible { + opacity: 1; + pointer-events: auto; +} + +.arrow:hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)); +} + +.arrow:active { + transform: translateY(-50%) scale(0.95); +} + +.arrowLeft { + left: -14px; +} + +.arrowRight { + right: -14px; +} diff --git a/apps/client/src/components/ui/card-carousel.tsx b/apps/client/src/components/ui/card-carousel.tsx new file mode 100644 index 0000000000..60d13abaf1 --- /dev/null +++ b/apps/client/src/components/ui/card-carousel.tsx @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import classes from "./card-carousel.module.css"; + +type Props = { + children: ReactNode; + ariaLabel?: string; +}; + +export default function CardCarousel({ children, ariaLabel }: Props) { + const { t } = useTranslation(); + const trackRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + const updateScrollState = useCallback(() => { + const el = trackRef.current; + if (!el) return; + const maxScroll = el.scrollWidth - el.clientWidth; + setCanScrollLeft(el.scrollLeft > 1); + setCanScrollRight(el.scrollLeft < maxScroll - 1); + }, []); + + useEffect(() => { + updateScrollState(); + const el = trackRef.current; + if (!el) return; + + const observer = new ResizeObserver(updateScrollState); + observer.observe(el); + for (const child of Array.from(el.children)) { + observer.observe(child); + } + + return () => observer.disconnect(); + }, [updateScrollState, children]); + + const scrollBy = (direction: 1 | -1) => { + const el = trackRef.current; + if (!el) return; + el.scrollBy({ left: direction * el.clientWidth * 0.85, behavior: "smooth" }); + }; + + return ( +
+
+ {children} +
+ + + + +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx new file mode 100644 index 0000000000..f0fe30356d --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx @@ -0,0 +1,106 @@ +import { useEffect, useRef } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { useChatInfoQuery } from "../queries/ai-chat-query"; +import { useChatStream } from "../hooks/use-chat-stream"; +import ChatMessageList from "./chat-message-list"; +import ChatEmptyState from "./chat-empty-state"; +import ChatInput from "./chat-input"; +import type { HomeAiPromptInitialState } from "@/features/home/components/home-ai-prompt"; +import classes from "../styles/ai-chat.module.css"; + +export default function AiChatLayout() { + const { chatId } = useParams<{ chatId: string }>(); + const location = useLocation(); + const navigate = useNavigate(); + const chatInfoQuery = useChatInfoQuery(chatId); + + // If the URL points at a chat the user does not own, the info fetch 404s. + // Bounce them back to /ai so they cannot interact with any chat UI (including + // kicking off orphan uploads) tied to a chat they have no access to. + useEffect(() => { + if (chatId && chatInfoQuery.isError) { + navigate("/ai", { replace: true }); + } + }, [chatId, chatInfoQuery.isError, navigate]); + const { + messages, + streamingContent, + streamingToolCalls, + isStreaming, + error, + sendMessage, + stopGeneration, + hydrateFromServer, + } = useChatStream(chatId); + + const autoSentRef = useRef(false); + + useEffect(() => { + if (chatInfoQuery.data?.messages) { + hydrateFromServer(chatInfoQuery.data.messages); + } + }, [chatInfoQuery.data, hydrateFromServer]); + + useEffect(() => { + if (autoSentRef.current || chatId) return; + const state = location.state as HomeAiPromptInitialState | null; + if (!state?.initialContent && !state?.initialAttachments?.length) return; + + autoSentRef.current = true; + sendMessage( + state.initialContent ?? "", + state.initialMentions ?? [], + state.initialAttachments ?? [], + ); + navigate(location.pathname, { replace: true, state: null }); + }, [chatId, location, navigate, sendMessage]); + + const hasMessages = messages.length > 0 || isStreaming; + + // While the redirect effect is running (or if the user is still on this + // component for any reason) never render the chat UI for a forbidden chat. + if (chatId && chatInfoQuery.isError) { + return null; + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {hasMessages ? ( + <> + +
+ +
+ + ) : ( + + )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx new file mode 100644 index 0000000000..f59b740e29 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx @@ -0,0 +1,166 @@ +import { useState, useRef, useEffect, useMemo, useCallback } from "react"; +import { ActionIcon, Menu, TextInput } from "@mantine/core"; +import { IconDots, IconTrash, IconEdit } from "@tabler/icons-react"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import type { AiChat } from "../types/ai-chat.types"; +import classes from "../styles/chat-sidebar.module.css"; + +type Props = { + chat: AiChat; + isActive: boolean; + onDelete: (chatId: string) => void; + onRename: (chatId: string, title: string) => void; +}; + +function formatChatDate( + isoString: string | Date, + locale: string | undefined, +): string { + const date = new Date(isoString); + if (Number.isNaN(date.getTime())) return ""; + + const now = new Date(); + const startOfToday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ).getTime(); + const ts = date.getTime(); + const sameYear = date.getFullYear() === now.getFullYear(); + + if (ts >= startOfToday) { + return date.toLocaleTimeString(locale, { + hour: "numeric", + minute: "2-digit", + }); + } + + if (sameYear) { + return date.toLocaleDateString(locale, { + month: "short", + day: "numeric", + }); + } + + return date.toLocaleDateString(locale, { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +export default function AiChatSidebarItem({ + chat, + isActive, + onDelete, + onRename, +}: Props) { + const { t, i18n } = useTranslation(); + const [renaming, setRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(""); + const inputRef = useRef(null); + + const formattedDate = useMemo( + () => formatChatDate(chat.updatedAt, i18n.language), + [chat.updatedAt, i18n.language], + ); + + useEffect(() => { + if (renaming) { + // Wait for the input to be mounted before selecting. + const id = window.setTimeout(() => inputRef.current?.select(), 0); + return () => window.clearTimeout(id); + } + }, [renaming]); + + const startRename = useCallback(() => { + setRenameValue(chat.title || ""); + setRenaming(true); + }, [chat.title]); + + const submitRename = useCallback(() => { + const trimmed = renameValue.trim(); + if (trimmed && trimmed !== chat.title) { + onRename(chat.id, trimmed); + } + setRenaming(false); + }, [renameValue, chat.id, chat.title, onRename]); + + if (renaming) { + return ( +
+ setRenameValue(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + submitRename(); + } else if (e.key === "Escape") { + e.preventDefault(); + setRenaming(false); + } + }} + onBlur={submitRename} + classNames={{ input: classes.chatItemRenameInput }} + style={{ flex: 1 }} + /> +
+ ); + } + + return ( + + + {chat.title || t("Untitled chat")} + + {formattedDate} +
+ + + e.preventDefault()} + > + + + + + } + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + startRename(); + }} + > + {t("Rename")} + + } + color="red" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + onDelete(chat.id); + }} + > + {t("Delete")} + + + +
+ + ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx new file mode 100644 index 0000000000..e3df3e52b8 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx @@ -0,0 +1,224 @@ +import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { ActionIcon, Center, TextInput, Loader, Tooltip } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { IconPlus, IconSearch, IconMessageCircle2 } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { + useChatsQuery, + useDeleteChatMutation, + useUpdateChatTitleMutation, + useSearchChatsQuery, +} from "../queries/ai-chat-query"; +import AiChatSidebarItem from "./ai-chat-sidebar-item"; +import type { AiChat } from "../types/ai-chat.types"; +import classes from "../styles/chat-sidebar.module.css"; + +type ChatGroup = { key: string; label: string; chats: AiChat[] }; + +function groupChatsByAge( + chats: AiChat[], + t: (key: string) => string, +): ChatGroup[] { + if (chats.length === 0) return []; + + const now = new Date(); + const startOfToday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ).getTime(); + const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000; + const startOfLast7 = startOfToday - 7 * 24 * 60 * 60 * 1000; + const startOfLast30 = startOfToday - 30 * 24 * 60 * 60 * 1000; + + const buckets: Record = { + today: { key: "today", label: t("Today"), chats: [] }, + yesterday: { key: "yesterday", label: t("Yesterday"), chats: [] }, + last7: { key: "last7", label: t("Previous 7 days"), chats: [] }, + last30: { key: "last30", label: t("Previous 30 days"), chats: [] }, + older: { key: "older", label: t("Older"), chats: [] }, + }; + + for (const chat of chats) { + const ts = new Date(chat.updatedAt).getTime(); + if (ts >= startOfToday) buckets.today.chats.push(chat); + else if (ts >= startOfYesterday) buckets.yesterday.chats.push(chat); + else if (ts >= startOfLast7) buckets.last7.chats.push(chat); + else if (ts >= startOfLast30) buckets.last30.chats.push(chat); + else buckets.older.chats.push(chat); + } + + return [ + buckets.today, + buckets.yesterday, + buckets.last7, + buckets.last30, + buckets.older, + ].filter((b) => b.chats.length > 0); +} + +export default function AiChatSidebar() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { chatId } = useParams<{ chatId: string }>(); + const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebouncedValue(search, 300); + const chatsQuery = useChatsQuery(); + const searchQuery = useSearchChatsQuery(debouncedSearch); + const deleteMutation = useDeleteChatMutation(); + const renameMutation = useUpdateChatTitleMutation(); + + const chats = useMemo(() => { + if (debouncedSearch) { + return searchQuery.data || []; + } + return chatsQuery.data?.pages.flatMap((p) => p.items) || []; + }, [debouncedSearch, searchQuery.data, chatsQuery.data]); + + const groupedChats = useMemo(() => groupChatsByAge(chats, t), [chats, t]); + + const sentinelRef = useRef(null); + const { hasNextPage, fetchNextPage, isFetchingNextPage } = chatsQuery; + const isSearching = Boolean(debouncedSearch); + + useEffect(() => { + if (isSearching) return; + const sentinel = sentinelRef.current; + if (!sentinel) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { threshold: 0.1 }, + ); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, [isSearching, hasNextPage, isFetchingNextPage, fetchNextPage]); + + const handleNewChat = useCallback( + (event: React.MouseEvent) => { + if ( + event.button !== 0 || + event.ctrlKey || + event.metaKey || + event.shiftKey + ) { + return; + } + event.preventDefault(); + navigate("/ai"); + }, + [navigate], + ); + + const handleDelete = useCallback( + (id: string) => { + deleteMutation.mutate(id, { + onSuccess: () => { + if (chatId === id) { + navigate("/ai"); + } + }, + }); + }, + [deleteMutation, chatId, navigate], + ); + + const handleRename = useCallback( + (chatId: string, title: string) => { + renameMutation.mutate({ chatId, title }); + }, + [renameMutation], + ); + + const isLoading = chatsQuery.isLoading || searchQuery.isLoading; + + return ( +
+
+ {t("AI Chat")} + + + + + +
+ + } + size="xs" + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + +
+ {isLoading && } + {!isLoading && chats.length === 0 && ( +
+ +
+ {isSearching ? t("No chats found") : t("No conversations yet")} +
+
+ {isSearching + ? t("Try a different search term.") + : t("Start a new chat to see it here.")} +
+
+ )} + {isSearching + ? chats.map((chat) => ( + + )) + : groupedChats.map((group) => ( +
+
{group.label}
+ {group.chats.map((chat) => ( + + ))} +
+ ))} + {!isSearching && ( + <> +
+ {isFetchingNextPage && ( +
+ +
+ )} + + )} +
+
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/aside-chat-history.tsx b/apps/client/src/ee/ai-chat/components/aside-chat-history.tsx new file mode 100644 index 0000000000..c31e5b104a --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/aside-chat-history.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { TextInput, Loader, Text, ScrollArea } from "@mantine/core"; +import { IconSearch } from "@tabler/icons-react"; +import { useChatsQuery, useSearchChatsQuery } from "../queries/ai-chat-query"; +import { useDebouncedValue } from "@mantine/hooks"; +import { useTranslation } from "react-i18next"; +import classes from "../styles/aside-chat-panel.module.css"; + +type Props = { + activeChatId: string | undefined; + onSelect: (chatId: string) => void; +}; + +export default function AsideChatHistory({ activeChatId, onSelect }: Props) { + const { t } = useTranslation(); + const [searchValue, setSearchValue] = useState(""); + const [debouncedSearch] = useDebouncedValue(searchValue, 300); + + const chatsQuery = useChatsQuery(); + const searchQuery = useSearchChatsQuery(debouncedSearch); + + const isSearching = debouncedSearch.length > 0; + const chats = isSearching + ? (searchQuery.data ?? []) + : (chatsQuery.data?.pages.flatMap((p) => p.items) ?? []); + const isLoading = isSearching ? searchQuery.isLoading : chatsQuery.isLoading; + + return ( +
+ } + size="xs" + mb="xs" + value={searchValue} + onChange={(e) => setSearchValue(e.currentTarget.value)} + /> + + {isLoading ? ( +
+ +
+ ) : chats.length === 0 ? ( + + {isSearching ? t("No chats found") : t("No chat history")} + + ) : ( + +
+ {chats.map((chat) => ( +
onSelect(chat.id)} + > + + {chat.title || t("Untitled chat")} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/aside-chat-panel.tsx b/apps/client/src/ee/ai-chat/components/aside-chat-panel.tsx new file mode 100644 index 0000000000..bd2b95697c --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/aside-chat-panel.tsx @@ -0,0 +1,258 @@ +import { useState, useEffect, useCallback } from "react"; +import { ActionIcon, Popover, Tooltip, UnstyledButton } from "@mantine/core"; +import { + IconPlus, + IconChevronDown, + IconArrowsDiagonal, + IconX, + IconSparkles, + IconFileText, + IconLanguage, + IconSearch, +} from "@tabler/icons-react"; +import { useAtom } from "jotai"; +import { useNavigate, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom"; +import { usePageQuery } from "@/features/page/queries/page-query"; +import { extractPageSlugId } from "@/lib"; +import { useChatStream } from "../hooks/use-chat-stream"; +import { useChatInfoQuery } from "../queries/ai-chat-query"; +import ChatMessageList from "./chat-message-list"; +import ChatInput from "./chat-input"; +import AsideChatHistory from "./aside-chat-history"; +import type { ChatAttachment, PageMention } from "../types/ai-chat.types"; +import classes from "../styles/aside-chat-panel.module.css"; + +type QuickAction = { + icon: React.ReactNode; + label: string; + prompt: string; +}; + +export default function AsideChatPanel() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [, setAsideState] = useAtom(asideStateAtom); + const [chatId, setChatId] = useState(undefined); + const [historyOpen, setHistoryOpen] = useState(false); + const [contextPages, setContextPages] = useState([]); + const { pageSlug } = useParams(); + const slugId = extractPageSlugId(pageSlug); + const { data: page } = usePageQuery({ pageId: slugId }); + + const chatInfoQuery = useChatInfoQuery(chatId); + const { + messages, + streamingContent, + streamingToolCalls, + isStreaming, + error, + sendMessage, + stopGeneration, + hydrateFromServer, + } = useChatStream(chatId, { + onChatCreated: (newChatId) => { + setChatId(newChatId); + }, + }); + + useEffect(() => { + if (page && !chatId) { + setContextPages([{ id: page.id, title: page.title || "", slugId: page.slugId }]); + } + }, [page, chatId]); + + const handleRemoveContextPage = useCallback((pageId: string) => { + setContextPages((prev) => prev.filter((p) => p.id !== pageId)); + }, []); + + useEffect(() => { + if (chatInfoQuery.data?.messages) { + hydrateFromServer(chatInfoQuery.data.messages); + } + }, [chatInfoQuery.data, hydrateFromServer]); + + // Drop the open chatId if the current user lost access to it (404/403 on + // the info fetch). Reverts the panel to a fresh chat instead of presenting + // an input tied to a chat the user does not own. + useEffect(() => { + if (chatId && chatInfoQuery.isError) { + setChatId(undefined); + } + }, [chatId, chatInfoQuery.isError]); + + const handleNewChat = useCallback( + (event: React.MouseEvent) => { + if ( + event.button !== 0 || + event.ctrlKey || + event.metaKey || + event.shiftKey + ) { + return; + } + event.preventDefault(); + setChatId(undefined); + if (page) { + setContextPages([ + { id: page.id, title: page.title || "", slugId: page.slugId }, + ]); + } + }, + [page], + ); + + const handleSelectChat = useCallback((selectedChatId: string) => { + setChatId(selectedChatId); + setHistoryOpen(false); + }, []); + + const handleExpand = useCallback(() => { + if (chatId) { + navigate(`/ai/chat/${chatId}`); + } else { + navigate("/ai"); + } + setAsideState({ tab: "", isAsideOpen: false }); + }, [chatId, navigate, setAsideState]); + + const handleClose = useCallback(() => { + setAsideState({ tab: "", isAsideOpen: false }); + }, [setAsideState]); + + const handleSend = useCallback( + (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => { + const contextPageId = contextPages.length > 0 ? contextPages[0].id : undefined; + sendMessage(content, mentions, attachments, contextPageId); + }, + [sendMessage, contextPages], + ); + + const handleQuickAction = useCallback( + (prompt: string) => { + handleSend(prompt, [], []); + }, + [handleSend], + ); + + const hasMessages = messages.length > 0 || isStreaming; + + const quickActions: QuickAction[] = [ + { icon: , label: t("Summarize this page"), prompt: "Summarize this page" }, + { icon: , label: t("Translate this page"), prompt: "Translate this page" }, + { icon: , label: t("Analyze for insights"), prompt: "Analyze this page for insights" }, + ]; + + return ( +
+
+ + + setHistoryOpen((o) => !o)} + > + + {chatInfoQuery.data?.chat?.title || t("New chat")} + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + {error && ( +
+ {error} +
+ )} + + {hasMessages ? ( + <> +
+ +
+ + ) : ( +
+ +
{t("How can I help you today?")}
+
+ {quickActions.map((action) => ( + + ))} +
+
+ )} + +
+ +
+
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx new file mode 100644 index 0000000000..d7bacf1874 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx @@ -0,0 +1,91 @@ +import { + IconSparkles, + IconSearch, + IconFilePlus, + IconEdit, + IconFileText, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import ChatInput from "./chat-input"; +import type { ChatAttachment, PageMention } from "../types/ai-chat.types"; +import classes from "../styles/ai-chat.module.css"; + +type Suggestion = { + icon: React.ReactNode; + text: string; + prompt: string; +}; + +const SUGGESTIONS: Suggestion[] = [ + { + icon: , + text: "Search across all pages", + prompt: "Search for pages about ", + }, + { + icon: , + text: "Create a new page", + prompt: "Create a new page titled ", + }, + { + icon: , + text: "Summarize a page", + prompt: "Summarize the page @", + }, + { + icon: , + text: "Update page content", + prompt: "Update the page @", + }, +]; + +type Props = { + isStreaming: boolean; + onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void; + onStop: () => void; +}; + +export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) { + const { t } = useTranslation(); + + const handleSuggestionClick = (prompt: string) => { + onSend(prompt, [], []); + }; + + return ( +
+ +
{t("Docmost AI")}
+
+ {t("What can I help you with?")} +
+ +
+ +
+ +
+
Get started
+
+ {SUGGESTIONS.map((s) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-input.tsx b/apps/client/src/ee/ai-chat/components/chat-input.tsx new file mode 100644 index 0000000000..d003adfb47 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-input.tsx @@ -0,0 +1,409 @@ +import { useCallback, useRef, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react"; +import { Popover } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { EditorContent, ReactNodeViewRenderer, useEditor } from "@tiptap/react"; +import { Placeholder } from "@tiptap/extension-placeholder"; +import { CharacterCount } from "@tiptap/extensions"; +import { StarterKit } from "@tiptap/starter-kit"; +import { Mention, LinkExtension } from "@docmost/editor-ext"; +import EmojiCommand from "@/features/editor/extensions/emoji-command"; +import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion"; +import MentionView from "@/features/editor/components/mention/mention-view"; +import { uploadChatFile } from "../services/ai-chat-service"; +import type { ChatAttachment, PageMention } from "../types/ai-chat.types"; +import classes from "../styles/chat-input.module.css"; + +type PendingAttachment = ChatAttachment & { uploading: boolean }; + +const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"]; +const ACCEPTED_FILE_TYPES = ".pdf,.docx,.txt,.csv,.md,.png,.jpg,.jpeg,.webp"; +// Kept in sync with MAX_ATTACHMENTS_PER_MESSAGE in apps/server/src/ee/ai-chat/ai-chat-limits.ts +const MAX_ATTACHMENTS_PER_MESSAGE = 5; + +type Props = { + isStreaming: boolean; + onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void; + onStop: () => void; + placeholder?: string; + autofocus?: boolean; + contextPages?: PageMention[]; + onRemoveContextPage?: (pageId: string) => void; + variant?: "card" | "flat"; + showDisclaimer?: boolean; + chatId?: string; +}; + +function extractMentions(json: any): PageMention[] { + const mentions: PageMention[] = []; + const seen = new Set(); + + function walk(node: any) { + if (node.type === "mention" && node.attrs?.entityType === "page" && node.attrs?.entityId) { + if (!seen.has(node.attrs.entityId)) { + seen.add(node.attrs.entityId); + mentions.push({ + id: node.attrs.entityId, + title: node.attrs.label || "", + slugId: node.attrs.slugId || "", + }); + } + } + if (node.content) { + for (const child of node.content) { + walk(child); + } + } + } + + walk(json); + return mentions; +} + +function editorJsonToText(json: any): string { + let text = ""; + + function walk(node: any) { + if (node.type === "text") { + text += node.text || ""; + } else if (node.type === "mention") { + text += `@${node.attrs?.label || ""}`; + } else if (node.type === "paragraph") { + if (text.length > 0) text += "\n"; + if (node.content) { + for (const child of node.content) { + walk(child); + } + } + return; + } + if (node.content) { + for (const child of node.content) { + walk(child); + } + } + } + + walk(json); + return text; +} + +export default function ChatInput({ + isStreaming, + onSend, + onStop, + placeholder, + autofocus = true, + contextPages, + onRemoveContextPage, + variant = "card", + showDisclaimer = true, + chatId, +}: Props) { + const chatIdRef = useRef(chatId); + chatIdRef.current = chatId; + const { t } = useTranslation(); + const [isEmpty, setIsEmpty] = useState(true); + const [pendingAttachments, setPendingAttachments] = useState([]); + const [plusMenuOpen, setPlusMenuOpen] = useState(false); + const fileInputRef = useRef(null); + const onSendRef = useRef(onSend); + onSendRef.current = onSend; + + const handleFileSelect = useCallback(async (files: FileList | null) => { + if (!files?.length) return; + + const room = MAX_ATTACHMENTS_PER_MESSAGE - pendingAttachments.length; + if (room <= 0) { + notifications.show({ + color: "yellow", + message: t("You can attach up to {{max}} files per message.", { + max: MAX_ATTACHMENTS_PER_MESSAGE, + }), + }); + if (fileInputRef.current) fileInputRef.current.value = ""; + return; + } + + const incoming = Array.from(files); + const accepted = incoming.slice(0, room); + + if (incoming.length > accepted.length) { + notifications.show({ + color: "yellow", + message: t( + "Only the first {{n}} file(s) were added (max {{max}} per message).", + { n: accepted.length, max: MAX_ATTACHMENTS_PER_MESSAGE }, + ), + }); + } + + for (const file of accepted) { + const tempId = `uploading-${Date.now()}-${Math.random()}`; + const ext = file.name.split(".").pop()?.toLowerCase() || ""; + + const placeholder: PendingAttachment = { + id: tempId, + fileName: file.name, + fileExt: ext, + fileSize: file.size, + mimeType: file.type, + uploading: true, + }; + + setPendingAttachments((prev) => [...prev, placeholder]); + + try { + const uploaded = await uploadChatFile(file, chatIdRef.current); + setPendingAttachments((prev) => + prev.map((a) => + a.id === tempId ? { ...uploaded, uploading: false } : a, + ), + ); + } catch { + setPendingAttachments((prev) => prev.filter((a) => a.id !== tempId)); + } + } + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }, [pendingAttachments.length, t]); + + const removeAttachment = useCallback((id: string) => { + setPendingAttachments((prev) => prev.filter((a) => a.id !== id)); + }, []); + + const handleSubmit = useCallback(() => { + if (!editor || isStreaming) return; + const json = editor.getJSON(); + const text = editorJsonToText(json).trim(); + const readyAttachments = pendingAttachments.filter((a) => !a.uploading); + if (!text && readyAttachments.length === 0) return; + + const mentions = extractMentions(json); + onSendRef.current(text, mentions, readyAttachments); + editor.commands.clearContent(); + editor.commands.focus(); + setPendingAttachments([]); + }, [isStreaming, pendingAttachments]); + + const handleSubmitRef = useRef(handleSubmit); + handleSubmitRef.current = handleSubmit; + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + gapcursor: false, + dropcursor: false, + link: false, + }), + Placeholder.configure({ + placeholder: placeholder || "Ask anything... Use @ to mention pages", + }), + CharacterCount.configure({ + limit: 50000, + }), + LinkExtension, + EmojiCommand, + Mention.configure({ + suggestion: { + allowSpaces: true, + items: () => [], + // @ts-ignore + render: mentionRenderItems, + }, + HTMLAttributes: { + class: "mention", + }, + }).extend({ + addNodeView() { + this.editor.isInitialized = true; + return ReactNodeViewRenderer(MentionView); + }, + }), + ], + editorProps: { + handleDOMEvents: { + keydown: (_view, event) => { + if ( + ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"].includes( + event.key, + ) + ) { + const emojiCommand = document.querySelector("#emoji-command"); + const mentionPopup = document.querySelector("#mention"); + if (emojiCommand || mentionPopup) { + return true; + } + } + + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSubmitRef.current(); + return true; + } + }, + }, + }, + content: "", + editable: true, + immediatelyRender: true, + shouldRerenderOnTransaction: false, + autofocus: autofocus ? "end" : false, + onUpdate: ({ editor: e }) => { + setIsEmpty(!e.getText().trim()); + }, + }); + + useEffect(() => { + if (editor && autofocus) { + editor.commands.focus(); + } + }, [editor]); + + const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0; + + const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper; + + return ( + <> +
+ handleFileSelect(e.target.files)} + /> + + {((contextPages?.length ?? 0) > 0 || pendingAttachments.length > 0) && ( +
+ {contextPages?.map((page) => ( +
+ + + {page.title || "Untitled"} + + {onRemoveContextPage && ( + + )} +
+ ))} + {pendingAttachments.map((attachment) => ( +
+ {IMAGE_EXTENSIONS.includes(attachment.fileExt) ? ( + + ) : ( + + )} + + {attachment.fileName} + + {!attachment.uploading && ( + + )} +
+ ))} +
+ )} + + +
+ + + + + + + + + + +
+ + {isStreaming ? ( + + ) : ( + + )} +
+
+ {showDisclaimer && ( +
+ {t("AI-generated content may not be accurate.")} +
+ )} + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-message-list.tsx b/apps/client/src/ee/ai-chat/components/chat-message-list.tsx new file mode 100644 index 0000000000..3a6fffef7d --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-message-list.tsx @@ -0,0 +1,174 @@ +import { useEffect, useRef, useCallback, useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { IconArrowDown, IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types"; +import ChatMessage from "./chat-message"; +import classes from "../styles/ai-chat.module.css"; + +function ChatMessageErrorFallback() { + const { t } = useTranslation(); + return ( +
+ + {t("Failed to render this message.")} +
+ ); +} + +type Props = { + messages: AiChatMessage[]; + isStreaming: boolean; + streamingContent: string; + streamingToolCalls: AiChatToolCall[]; +}; + +const BOTTOM_THRESHOLD_PX = 32; +const SCROLL_UP_THRESHOLD_PX = 5; +const SMOOTH_SCROLL_SETTLE_MS = 600; + +export default function ChatMessageList({ + messages, + isStreaming, + streamingContent, + streamingToolCalls, +}: Props) { + const containerRef = useRef(null); + const bottomRef = useRef(null); + const isAtBottomRef = useRef(true); + const isAutoScrollingRef = useRef(false); + const prevScrollTopRef = useRef(0); + const [showScrollButton, setShowScrollButton] = useState(false); + + const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { + const container = containerRef.current; + if (!container) return; + + isAutoScrollingRef.current = true; + const target = container.scrollHeight - container.clientHeight; + container.scrollTo({ top: target, behavior }); + prevScrollTopRef.current = target; + isAtBottomRef.current = true; + setShowScrollButton(false); + + if (behavior === "smooth") { + setTimeout(() => { + isAutoScrollingRef.current = false; + if (containerRef.current) { + prevScrollTopRef.current = containerRef.current.scrollTop; + } + }, SMOOTH_SCROLL_SETTLE_MS); + } else { + isAutoScrollingRef.current = false; + } + }, []); + + const handleScroll = useCallback(() => { + if (isAutoScrollingRef.current) return; + + const container = containerRef.current; + if (!container) return; + + const currentScrollTop = container.scrollTop; + const scrolledUp = + currentScrollTop < prevScrollTopRef.current - SCROLL_UP_THRESHOLD_PX; + prevScrollTopRef.current = currentScrollTop; + + const distanceFromBottom = + container.scrollHeight - currentScrollTop - container.clientHeight; + const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX; + + if (scrolledUp) { + isAtBottomRef.current = atBottom; + } else if (atBottom) { + isAtBottomRef.current = true; + } + + setShowScrollButton(!atBottom); + }, []); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + container.addEventListener("scroll", handleScroll, { passive: true }); + return () => container.removeEventListener("scroll", handleScroll); + }, [handleScroll]); + + // Instant scroll during streaming to keep up with rapid updates + useEffect(() => { + if (isAtBottomRef.current) { + scrollToBottom("instant"); + } + }, [streamingContent, streamingToolCalls.length, scrollToBottom]); + + // Smooth scroll for new messages. Always force-scroll when the latest + // message is from the user (they just sent it), even if they were reading + // scrollback. + useEffect(() => { + const lastMessage = messages[messages.length - 1]; + const lastIsUser = lastMessage?.role === "user"; + if (lastIsUser || isAtBottomRef.current) { + scrollToBottom("smooth"); + return; + } + + // No auto-scroll: recompute from actual layout so that chat switches to + // content that doesn't overflow correctly hide the button even when no + // scroll event fires. + const container = containerRef.current; + if (!container) return; + const distanceFromBottom = + container.scrollHeight - container.scrollTop - container.clientHeight; + const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX; + isAtBottomRef.current = atBottom; + setShowScrollButton(!atBottom); + }, [messages, scrollToBottom]); + + return ( +
+
+ {messages.map((msg) => ( + } + > + + + ))} + {isStreaming && ( + } + > + + + )} +
+
+ {showScrollButton && ( + + )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-message.tsx b/apps/client/src/ee/ai-chat/components/chat-message.tsx new file mode 100644 index 0000000000..d0e9443a72 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-message.tsx @@ -0,0 +1,139 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router"; +import DOMPurify from "dompurify"; +import { ActionIcon, Tooltip } from "@mantine/core"; +import { + IconCheck, + IconCopy, + IconFile, + IconLoader2, + IconPhoto, +} from "@tabler/icons-react"; +import { markdownToHtml } from "@docmost/editor-ext"; +import { CopyButton } from "@/components/common/copy-button"; +import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types"; +import ChatToolGroup from "./chat-tool-group"; +import classes from "../styles/chat-message.module.css"; +import CopyTextButton from "@/components/common/copy.tsx"; + +const chatSanitizer = DOMPurify(); +chatSanitizer.addHook("afterSanitizeAttributes", (node) => { + if (node.tagName === "A") { + const href = node.getAttribute("href") || ""; + if (href.startsWith("http://") || href.startsWith("https://")) { + node.setAttribute("target", "_blank"); + node.setAttribute("rel", "noopener noreferrer"); + } + } +}); + +const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"]; + +type Props = { + message: AiChatMessage; + isStreaming?: boolean; + streamingContent?: string; + streamingToolCalls?: AiChatToolCall[]; +}; + +export default function ChatMessage({ + message, + isStreaming, + streamingContent, + streamingToolCalls, +}: Props) { + const navigate = useNavigate(); + + const handleContentClick = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const anchor = target.closest("a"); + if (!anchor) return; + + const href = anchor.getAttribute("href"); + if (href && (href.startsWith("/s/") || href.startsWith("/p/"))) { + e.preventDefault(); + navigate(href); + } + }, + [navigate], + ); + + if (message.role === "tool") return null; + + const isUser = message.role === "user"; + const content = isStreaming ? streamingContent : message.content; + const toolCalls = isStreaming ? streamingToolCalls : message.toolCalls; + + if (isUser) { + const displayContent = (content || "").replace( + /\n\n[\s\S]*<\/referenced_pages>$/, + "", + ); + const attachments = + (message.metadata?.attachments as { + id: string; + fileName: string; + fileExt: string; + }[]) || []; + + return ( +
+
+ {attachments.length > 0 && ( +
+ {attachments.map((a) => ( + + {IMAGE_EXTENSIONS.includes(a.fileExt) ? ( + + ) : ( + + )} + {a.fileName} + + ))} +
+ )} + {displayContent} +
+
+ ); + } + + return ( +
+
+ {toolCalls && toolCalls.length > 0 && ( + + )} + {content && ( +
+ )} + {isStreaming && ( + <> + {!content && ( + + + Thinking + + )} + + + )} +
+ {!isStreaming && message.content && ( +
+ +
+ )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-tool-group.tsx b/apps/client/src/ee/ai-chat/components/chat-tool-group.tsx new file mode 100644 index 0000000000..b4e002f6b8 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-tool-group.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { + IconChevronRight, + IconChevronDown, + IconLoader2, +} from "@tabler/icons-react"; +import type { AiChatToolCall } from "../types/ai-chat.types"; +import ChatToolResult, { TOOL_LABELS } from "./chat-tool-result"; +import classes from "../styles/chat-message.module.css"; + +type Props = { + toolCalls: AiChatToolCall[]; + isStreaming?: boolean; +}; + +export default function ChatToolGroup({ toolCalls, isStreaming }: Props) { + const [expanded, setExpanded] = useState(false); + + if (!toolCalls || toolCalls.length === 0) return null; + + const activeCall = + isStreaming && toolCalls.length > 0 + ? [...toolCalls].reverse().find((tc) => tc.result === undefined) + : undefined; + + const activeLabel = activeCall + ? TOOL_LABELS[activeCall.name] || activeCall.name + : null; + + return ( +
+
setExpanded((prev) => !prev)} + > + {activeLabel ? ( + + ) : expanded ? ( + + ) : ( + + )} + + {activeLabel ? `${activeLabel}…` : `Steps ${toolCalls.length}`} + +
+ {expanded && ( +
+ {toolCalls.map((tc) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-tool-result.tsx b/apps/client/src/ee/ai-chat/components/chat-tool-result.tsx new file mode 100644 index 0000000000..55f20f9130 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/chat-tool-result.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { IconChevronRight, IconChevronDown } from "@tabler/icons-react"; +import type { AiChatToolCall } from "../types/ai-chat.types"; +import classes from "../styles/chat-message.module.css"; + +export const TOOL_LABELS: Record = { + list_spaces: "Listed spaces", + search_pages: "Searched pages", + get_page: "Read page", + create_page: "Created page", + update_page: "Updated page", +}; + +type Props = { + toolCall: AiChatToolCall; +}; + +export default function ChatToolResult({ toolCall }: Props) { + const [expanded, setExpanded] = useState(false); + const label = TOOL_LABELS[toolCall.name] || toolCall.name; + + return ( +
+
setExpanded((prev) => !prev)} + > + · + {expanded ? ( + + ) : ( + + )} + {label} +
+ {expanded && ( +
+
+            {JSON.stringify(
+              { args: toolCall.args, result: toolCall.result },
+              null,
+              2,
+            )}
+          
+
+ )} +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/components/enable-ai-chat.tsx b/apps/client/src/ee/ai-chat/components/enable-ai-chat.tsx new file mode 100644 index 0000000000..f4200e5bc0 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/enable-ai-chat.tsx @@ -0,0 +1,67 @@ +import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { Feature } from "@/ee/features"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; + +export default function EnableAiChat() { + const { t } = useTranslation(); + + return ( + +
+ + {t("AI Chat")} + + {t("Beta")} + + + + {t( + "Enable AI Chat to allow users to have multi-turn conversations with AI about your workspace content.", + )} + +
+ + +
+ ); +} + +function AiChatToggle() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState(workspace?.settings?.ai?.chat); + const hasAccess = useHasFeature(Feature.AI); + const upgradeLabel = useUpgradeLabel(); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ aiChat: value } as any); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err: any) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + + + ); +} diff --git a/apps/client/src/ee/ai-chat/hooks/use-chat-stream.ts b/apps/client/src/ee/ai-chat/hooks/use-chat-stream.ts new file mode 100644 index 0000000000..65a93a9701 --- /dev/null +++ b/apps/client/src/ee/ai-chat/hooks/use-chat-stream.ts @@ -0,0 +1,227 @@ +import { useState, useCallback, useEffect, useRef } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "react-router-dom"; +import { sendChatMessage } from "../services/ai-chat-service"; +import type { + AiChatMessage, + AiChatStreamEvent, + AiChatToolCall, + ChatAttachment, + PageMention, +} from "../types/ai-chat.types"; + +type ChatStreamOptions = { + onChatCreated?: (chatId: string) => void; +}; + +export function useChatStream( + chatId: string | undefined, + options?: ChatStreamOptions, +) { + const [messages, setMessages] = useState([]); + const [streamingContent, setStreamingContent] = useState(""); + const [streamingToolCalls, setStreamingToolCalls] = useState( + [], + ); + const [isStreaming, setIsStreaming] = useState(false); + const [error, setError] = useState(null); + const [errorCode, setErrorCode] = useState(null); + const [isRetryable, setIsRetryable] = useState(false); + const abortRef = useRef(null); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const currentChatIdRef = useRef(chatId); + currentChatIdRef.current = chatId; + // Tracks which chatId the local `messages` state currently represents. + // Set when we seed from a server fetch AND when we optimistically own a + // freshly-created chat after `chat_created`. This is the single authority + // marker that keeps server-state effects from clobbering in-flight streams. + const hydratedChatIdRef = useRef(undefined); + + // Reset local state when the consumer switches to a different chat. + // Skip the reset if the new chatId is one the hook itself already claimed + // during a new-chat flow — in that case our optimistic state is the truth. + useEffect(() => { + if (chatId && chatId === hydratedChatIdRef.current) return; + hydratedChatIdRef.current = undefined; + setMessages([]); + setError(null); + setErrorCode(null); + setIsRetryable(false); + }, [chatId]); + + const hydrateFromServer = useCallback((msgs: AiChatMessage[]) => { + const forId = currentChatIdRef.current; + if (!forId) return; + if (hydratedChatIdRef.current === forId) return; + hydratedChatIdRef.current = forId; + setMessages(msgs); + }, []); + + const sendMessage = useCallback( + (content: string, mentions: PageMention[] = [], attachments: ChatAttachment[] = [], contextPageId?: string) => { + if (isStreaming || (!content.trim() && attachments.length === 0)) return; + + setError(null); + setErrorCode(null); + setIsRetryable(false); + setIsStreaming(true); + setStreamingContent(""); + setStreamingToolCalls([]); + + const metadata: Record = {}; + if (mentions.length) { + metadata.mentionedPageIds = mentions.map((m) => m.id); + } + if (attachments.length) { + metadata.attachments = attachments.map((a) => ({ + id: a.id, + fileName: a.fileName, + fileExt: a.fileExt, + })); + } + + const userMessage: AiChatMessage = { + id: `temp-${Date.now()}`, + chatId: currentChatIdRef.current || "", + role: "user", + content, + toolCalls: null, + metadata: Object.keys(metadata).length ? metadata : null, + createdAt: new Date().toISOString(), + }; + + setMessages((prev) => [...prev, userMessage]); + + const attachmentIds = attachments.map((a) => a.id); + + const abortController = sendChatMessage( + { + chatId: currentChatIdRef.current, + content, + mentionedPageIds: mentions.map((m) => m.id), + ...(contextPageId && { contextPageId }), + ...(attachmentIds.length && { attachmentIds }), + }, + (event: AiChatStreamEvent) => { + switch (event.type) { + case "chat_created": + currentChatIdRef.current = event.chatId; + // Claim authority over this new chatId so when the consumer's + // prop catches up via navigation/onChatCreated, the reset effect + // sees a match and preserves our optimistic messages. + hydratedChatIdRef.current = event.chatId; + if (options?.onChatCreated) { + options.onChatCreated(event.chatId); + } else { + navigate(`/ai/chat/${event.chatId}`, { replace: true }); + } + queryClient.invalidateQueries({ queryKey: ["ai-chats"] }); + break; + case "content": + setStreamingContent((prev) => prev + event.text); + break; + case "tool_call": + setStreamingToolCalls((prev) => [ + ...prev, + { + id: event.id, + name: event.name, + args: event.args, + }, + ]); + break; + case "tool_result": + setStreamingToolCalls((prev) => + prev.map((tc) => + tc.id === event.id ? { ...tc, result: event.result } : tc, + ), + ); + break; + case "done": { + setStreamingContent((currentContent) => { + setStreamingToolCalls((currentToolCalls) => { + const assistantMessage: AiChatMessage = { + id: event.messageId, + chatId: currentChatIdRef.current || "", + role: "assistant", + content: currentContent || null, + toolCalls: currentToolCalls.length + ? currentToolCalls + : null, + metadata: event.usage ? { tokenUsage: event.usage } : null, + createdAt: new Date().toISOString(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + return []; + }); + return ""; + }); + setIsStreaming(false); + queryClient.invalidateQueries({ + queryKey: ["ai-chat", currentChatIdRef.current], + }); + break; + } + case "error": + setError(event.message); + setErrorCode(event.code || null); + setIsRetryable(event.retryable || false); + setIsStreaming(false); + break; + } + }, + (errorMsg) => { + setError(errorMsg); + setIsStreaming(false); + }, + () => { + setIsStreaming(false); + }, + ); + + abortRef.current = abortController; + }, + [isStreaming, navigate, queryClient], + ); + + const stopGeneration = useCallback(() => { + abortRef.current?.abort(); + abortRef.current = null; + + setStreamingContent((currentContent) => { + setStreamingToolCalls((currentToolCalls) => { + if (currentContent || currentToolCalls.length > 0) { + const partialMessage: AiChatMessage = { + id: `stopped-${Date.now()}`, + chatId: currentChatIdRef.current || "", + role: "assistant", + content: currentContent || null, + toolCalls: currentToolCalls.length ? currentToolCalls : null, + metadata: null, + createdAt: new Date().toISOString(), + }; + setMessages((prev) => [...prev, partialMessage]); + } + return []; + }); + return ""; + }); + + setIsStreaming(false); + }, []); + + return { + messages, + streamingContent, + streamingToolCalls, + isStreaming, + error, + errorCode, + isRetryable, + sendMessage, + stopGeneration, + hydrateFromServer, + }; +} diff --git a/apps/client/src/ee/ai-chat/pages/ai-chat.tsx b/apps/client/src/ee/ai-chat/pages/ai-chat.tsx new file mode 100644 index 0000000000..bb264b733a --- /dev/null +++ b/apps/client/src/ee/ai-chat/pages/ai-chat.tsx @@ -0,0 +1,39 @@ +import { useParams } from "react-router-dom"; +import { ErrorBoundary } from "react-error-boundary"; +import { Button } from "@mantine/core"; +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import AiChatLayout from "../components/ai-chat-layout"; +import { EmptyState } from "@/components/ui/empty-state.tsx"; +import classes from "../styles/ai-chat.module.css"; + +export default function AiChat() { + const { t } = useTranslation(); + const { chatId } = useParams<{ chatId: string }>(); + + return ( +
+ ( + + {t("Try again")} + + } + /> + )} + > + + +
+ ); +} diff --git a/apps/client/src/ee/ai-chat/queries/ai-chat-query.ts b/apps/client/src/ee/ai-chat/queries/ai-chat-query.ts new file mode 100644 index 0000000000..8992a11595 --- /dev/null +++ b/apps/client/src/ee/ai-chat/queries/ai-chat-query.ts @@ -0,0 +1,61 @@ +import { + useQuery, + useMutation, + useQueryClient, + useInfiniteQuery, +} from "@tanstack/react-query"; +import { + listChats, + getChatInfo, + deleteChat, + updateChatTitle, + searchChats, +} from "../services/ai-chat-service"; + +export function useChatsQuery() { + return useInfiniteQuery({ + queryKey: ["ai-chats"], + queryFn: ({ pageParam }) => + listChats({ cursor: pageParam, limit: 30 }), + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => + lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined, + }); +} + +export function useChatInfoQuery(chatId: string | undefined) { + return useQuery({ + queryKey: ["ai-chat", chatId], + queryFn: () => getChatInfo(chatId!), + enabled: !!chatId, + }); +} + +export function useDeleteChatMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (chatId: string) => deleteChat(chatId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["ai-chats"] }); + }, + }); +} + +export function useUpdateChatTitleMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ chatId, title }: { chatId: string; title: string }) => + updateChatTitle(chatId, title), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["ai-chats"] }); + }, + }); +} + +export function useSearchChatsQuery(query: string) { + return useQuery({ + queryKey: ["ai-chats-search", query], + queryFn: () => searchChats(query), + enabled: query.length > 0, + }); +} diff --git a/apps/client/src/ee/ai-chat/services/ai-chat-service.ts b/apps/client/src/ee/ai-chat/services/ai-chat-service.ts new file mode 100644 index 0000000000..2932372e05 --- /dev/null +++ b/apps/client/src/ee/ai-chat/services/ai-chat-service.ts @@ -0,0 +1,144 @@ +import api from "@/lib/api-client.ts"; +import type { + AiChat, + AiChatMessage, + AiChatStreamEvent, + ChatAttachment, +} from "../types/ai-chat.types"; +import { IPagination } from "@/lib/types.ts"; + +export async function createChat(): Promise { + const req = await api.post("/ai/chats/create"); + return req.data; +} + +export async function listChats(params?: { + limit?: number; + cursor?: string; +}): Promise> { + const req = await api.post("/ai/chats", params); + return req.data; +} + +export async function getChatInfo( + chatId: string, +): Promise<{ chat: AiChat; messages: AiChatMessage[] }> { + const req = await api.post("/ai/chats/info", { chatId }); + return req.data; +} + +export async function deleteChat(chatId: string): Promise { + await api.post("/ai/chats/delete", { chatId }); +} + +export async function updateChatTitle( + chatId: string, + title: string, +): Promise { + await api.post("/ai/chats/update", { chatId, title }); +} + +export async function searchChats(query: string): Promise { + const req = await api.post("/ai/chats/search", { query }); + return req.data; +} + +export async function uploadChatFile( + file: File, + chatId?: string, +): Promise { + const formData = new FormData(); + formData.append("file", file); + if (chatId) { + formData.append("chatId", chatId); + } + return await api.post("/ai/chats/upload", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); +} + +export function sendChatMessage( + params: { + chatId?: string; + content: string; + mentionedPageIds?: string[]; + contextPageId?: string; + attachmentIds?: string[]; + }, + onEvent: (event: AiChatStreamEvent) => void, + onError?: (error: string) => void, + onComplete?: () => void, +): AbortController { + const abortController = new AbortController(); + + (async () => { + try { + const response = await fetch("/api/ai/chats/send", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + signal: abortController.signal, + credentials: "include", + }); + + if (!response.ok) { + const errorBody = await response.text(); + let errorMessage = `HTTP error ${response.status}`; + try { + const parsed = JSON.parse(errorBody); + errorMessage = parsed.message || errorMessage; + } catch { + // use default + } + onError?.(errorMessage); + return; + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + onError?.("Response body is not readable"); + return; + } + + let buffer = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") { + onComplete?.(); + return; + } + try { + const parsed = JSON.parse(data) as AiChatStreamEvent; + onEvent(parsed); + } catch { + // Skip invalid JSON + } + } + } + } + } finally { + reader.releaseLock(); + } + + onComplete?.(); + } catch (error: any) { + if (error.name !== "AbortError") { + onError?.(error.message); + } + } + })(); + + return abortController; +} diff --git a/apps/client/src/ee/ai-chat/styles/ai-chat.module.css b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css new file mode 100644 index 0000000000..27b0f0c06b --- /dev/null +++ b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css @@ -0,0 +1,169 @@ +.layout { + display: flex; + height: 100%; + width: 100%; +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + height: calc(100vh - 45px - 2 * var(--mantine-spacing-md)); + max-width: 900px; + margin: 0 auto; + width: 100%; +} + +.messageListWrapper { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + min-height: 0; + height: 100%; + width: 100%; +} + +.messageList { + flex: 1; + overflow-y: auto; + padding: var(--mantine-spacing-md) var(--mantine-spacing-lg); +} + +.messageErrorFallback { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + margin-bottom: var(--mantine-spacing-lg); + border-radius: var(--mantine-radius-sm); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + font-size: var(--mantine-font-size-xs); +} + +.scrollToBottomButton { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + cursor: pointer; + padding: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease; + z-index: 2; +} + +.scrollToBottomButton:hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)); + border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3)); +} + +.scrollToBottomButton:active { + transform: translateX(-50%) scale(0.95); +} + +.inputArea { + padding: var(--mantine-spacing-xs) var(--mantine-spacing-lg) var(--mantine-spacing-lg); +} + +/* Empty state - Notion AI style centered layout */ +.emptyState { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--mantine-spacing-xl) var(--mantine-spacing-lg); +} + +.emptyStateIcon { + width: 48px; + height: 48px; + margin-bottom: var(--mantine-spacing-sm); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); +} + +.emptyStateBrand { + font-size: var(--mantine-font-size-xs); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + margin-bottom: var(--mantine-spacing-xs); +} + +.emptyStateTitle { + font-size: 1.5rem; + font-weight: 600; + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); + margin-bottom: var(--mantine-spacing-xl); + text-align: center; +} + +.emptyStateInput { + width: 100%; + max-width: 600px; + margin-bottom: var(--mantine-spacing-xl); + padding: 6px 0; +} + +.suggestionsSection { + width: 100%; + max-width: 600px; +} + +.suggestionsLabel { + font-size: var(--mantine-font-size-xs); + font-weight: 500; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: var(--mantine-spacing-sm); +} + +.suggestionsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--mantine-spacing-sm); +} + +.suggestionCard { + display: flex; + align-items: flex-start; + gap: var(--mantine-spacing-sm); + padding: var(--mantine-spacing-sm) var(--mantine-spacing-md); + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + border-radius: var(--mantine-radius-md); + cursor: pointer; + background: transparent; + transition: background-color 150ms, border-color 150ms; + text-align: left; + width: 100%; + + @mixin hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + } +} + +.suggestionIcon { + flex-shrink: 0; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + margin-top: 1px; +} + +.suggestionText { + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + line-height: 1.4; +} diff --git a/apps/client/src/ee/ai-chat/styles/aside-chat-panel.module.css b/apps/client/src/ee/ai-chat/styles/aside-chat-panel.module.css new file mode 100644 index 0000000000..f8958615c4 --- /dev/null +++ b/apps/client/src/ee/ai-chat/styles/aside-chat-panel.module.css @@ -0,0 +1,139 @@ +.panel { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 0 0 var(--mantine-spacing-sm) 0; + border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); +} + +.toolbarSpacer { + flex: 1; +} + +.titleButton { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: var(--mantine-radius-sm); + font-size: var(--mantine-font-size-sm); + font-weight: 500; + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); + max-width: 60%; + min-width: 0; +} + +.titleButton:hover { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); +} + +.titleText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.messages { + flex: 1; + overflow-y: auto; + padding: var(--mantine-spacing-sm) 0; + scroll-behavior: smooth; +} + +.inputArea { + padding-top: var(--mantine-spacing-sm); +} + +.emptyState { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--mantine-spacing-md); + padding: var(--mantine-spacing-xl) var(--mantine-spacing-sm); +} + +.emptyStateIcon { + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); +} + +.emptyStateTitle { + font-size: var(--mantine-font-size-lg); + font-weight: 600; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + text-align: center; +} + +.quickActions { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.quickAction { + display: flex; + align-items: center; + gap: var(--mantine-spacing-sm); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + border-radius: var(--mantine-radius-md); + cursor: pointer; + background: transparent; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: var(--mantine-font-size-sm); + text-align: left; + width: 100%; + transition: background-color 150ms, border-color 150ms; + + @mixin hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + } +} + +.quickActionIcon { + flex-shrink: 0; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); +} + +.historyList { + max-height: 300px; + overflow-y: auto; +} + +.historyItem { + display: flex; + align-items: center; + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + cursor: pointer; + border-radius: var(--mantine-radius-sm); + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + transition: background-color 150ms; + + @mixin hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + } + + &[data-active] { + background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); + } +} + +.historyItemTitle { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/apps/client/src/ee/ai-chat/styles/chat-input.module.css b/apps/client/src/ee/ai-chat/styles/chat-input.module.css new file mode 100644 index 0000000000..20b287c1d6 --- /dev/null +++ b/apps/client/src/ee/ai-chat/styles/chat-input.module.css @@ -0,0 +1,242 @@ +.inputWrapper { + position: relative; + overflow: hidden; + border: 1px solid + light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + border-radius: 16px; + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + box-shadow: light-dark( + 0 2px 40px 4px rgba(0, 0, 0, 0.07), + 0 2px 40px 4px rgba(0, 0, 0, 0.5) + ); + transition: + border-color 150ms, + box-shadow 150ms; + + &:focus-within { + border-color: light-dark( + var(--mantine-color-gray-3), + var(--mantine-color-dark-4) + ); + box-shadow: light-dark( + 0 4px 48px 6px rgba(0, 0, 0, 0.09), + 0 4px 48px 6px rgba(0, 0, 0, 0.6) + ); + } +} + +.inputWrapperFlat { + position: relative; + overflow: hidden; + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + border-radius: 12px; + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + box-shadow: none; + transition: border-color 150ms; + + &:focus-within { + border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + } +} + +.disclaimer { + margin-top: 6px; + text-align: center; + font-size: var(--mantine-font-size-xs); + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); +} + +.attachmentChips { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 10px 14px 0; +} + +.attachmentChip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 8px; + border-radius: 8px; + background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: var(--mantine-font-size-xs); + max-width: 200px; +} + +.attachmentChipUploading { + opacity: 0.55; +} + +.attachmentChipName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.attachmentChipRemove { + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + cursor: pointer; + padding: 0; + margin-left: 2px; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + border-radius: 50%; + + @mixin hover { + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); + } +} + +.editorContent { + overflow: hidden; + + :global(.ProseMirror) { + outline: none; + border: none; + background-color: transparent; + padding: 14px 18px 8px; + font-size: 15px; + line-height: 1.6; + max-height: 200px; + overflow-y: auto; + min-height: 24px; + color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0)); + } + + :global(.ProseMirror p) { + margin-block-start: 0; + margin-block-end: 0; + } + + :global(.ProseMirror p.is-editor-empty:first-child::before) { + color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3)); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } +} + +.actions { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 4px 12px 10px; + gap: var(--mantine-spacing-xs); +} + +.sendButton { + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + transition: background-color 150ms, opacity 150ms; + background: light-dark(var(--mantine-color-dark-9), var(--mantine-color-gray-0)); + color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-9)); + + &:disabled { + opacity: 0.25; + cursor: default; + } + + @mixin hover { + &:not(:disabled) { + opacity: 0.85; + } + } +} + +.attachButton { + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + cursor: pointer; + padding: 2px; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + transition: color 150ms; + + @mixin hover { + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + } +} + +.plusButton { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + background: none; + cursor: pointer; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + transition: color 150ms, background-color 150ms; + + @mixin hover { + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + } +} + +.plusMenuItem { + display: flex; + align-items: center; + gap: var(--mantine-spacing-sm); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + border: none; + background: none; + cursor: pointer; + width: 100%; + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + border-radius: var(--mantine-radius-sm); + transition: background-color 150ms; + + @mixin hover { + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + background: none; + } +} + +.plusMenuIcon { + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); +} + +.stopButton { + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + cursor: pointer; + transition: background-color 150ms; + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + + @mixin hover { + background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); + } +} diff --git a/apps/client/src/ee/ai-chat/styles/chat-message.module.css b/apps/client/src/ee/ai-chat/styles/chat-message.module.css new file mode 100644 index 0000000000..33e39dd615 --- /dev/null +++ b/apps/client/src/ee/ai-chat/styles/chat-message.module.css @@ -0,0 +1,286 @@ +.message { + margin-bottom: var(--mantine-spacing-lg); +} + +.userMessage { + composes: message; + display: flex; + justify-content: flex-end; +} + +.userBubble { + max-width: 75%; + padding: 10px 16px; + border-radius: 18px; + background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0)); + font-size: 15px; + line-height: 1.6; + word-wrap: break-word; + overflow-wrap: break-word; +} + +[data-aside-chat] .userBubble { + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); +} + +.userBubble p { + margin: 0; +} + +.messageAttachments { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 6px; +} + +.messageAttachmentChip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 6px; + background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2)); + font-size: var(--mantine-font-size-xs); + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.assistantMessage { + composes: message; +} + +.messageContent { + font-size: 15px; + line-height: 1.7; + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-1)); + word-wrap: break-word; + overflow-wrap: break-word; +} + +.messageContent p { + margin: 0 0 0.75em 0; +} + +.messageContent p:last-child { + margin-bottom: 0; +} + +.messageContent ul, +.messageContent ol { + margin: 0.5em 0 0.75em 0; + padding-left: 1.5em; +} + +.messageContent li { + margin-bottom: 0.3em; +} + +.messageContent h1, +.messageContent h2, +.messageContent h3 { + margin: 1em 0 0.5em 0; + font-weight: 600; + color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0)); +} + +.messageContent h1 { + font-size: 1.4em; +} + +.messageContent h2 { + font-size: 1.2em; +} + +.messageContent h3 { + font-size: 1.05em; +} + +.messageContent pre { + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); + padding: var(--mantine-spacing-sm) var(--mantine-spacing-md); + border-radius: var(--mantine-radius-md); + overflow-x: auto; + font-size: var(--mantine-font-size-sm); + margin: 0.75em 0; +} + +.messageContent code { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.88em; +} + +.messageContent pre code { + background: none; + padding: 0; +} + +.messageContent blockquote { + border-left: 3px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + padding-left: var(--mantine-spacing-md); + margin: 0.75em 0; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2)); +} + +.messageContent a { + color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4)); + text-decoration: none; + + @mixin hover { + text-decoration: underline; + } +} + +.messageContent a[href^="/s/"], +.messageContent a[href^="/p/"] { + color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1)); + font-weight: 500; + text-decoration: none; + cursor: pointer; + + @mixin light { + border-bottom: 0.05em solid var(--mantine-color-dark-0); + } + + @mixin dark { + border-bottom: 0.05em solid var(--mantine-color-dark-2); + } + + @mixin hover { + text-decoration: none; + @mixin light { + border-bottom-color: var(--mantine-color-dark-2); + } + @mixin dark { + border-bottom-color: var(--mantine-color-dark-0); + } + } +} + +.messageContent hr { + border: none; + border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); + margin: 1em 0; +} + +.toolGroup { + margin: 6px 0; + font-size: var(--mantine-font-size-xs); +} + +.toolGroupHeader { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + line-height: 1.4; + transition: color 120ms ease; +} + +.toolGroupHeader:hover { + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); +} + +.toolGroupLabel { + font-weight: 500; +} + +.toolGroupSteps { + margin-top: 4px; + padding-left: 14px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.toolStep { + font-size: var(--mantine-font-size-xs); +} + +.toolStepRow { + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + user-select: none; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + line-height: 1.5; + transition: color 120ms ease; +} + +.toolStepRow:hover { + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); +} + +.toolStepBullet { + display: inline-block; + width: 8px; + text-align: center; + opacity: 0.6; +} + +.toolStepDetails { + margin-top: 4px; + margin-left: 18px; + padding: 6px 10px; + border-radius: var(--mantine-radius-sm); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: 11px; + line-height: 1.5; + overflow-x: auto; +} + +.messageActions { + display: flex; + align-items: center; + gap: 4px; + margin-top: 4px; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); +} + +.processingIndicator { + display: inline-flex; + align-items: center; + gap: 6px; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + font-size: var(--mantine-font-size-sm); +} + +.processingSpinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.streamingCursor { + display: inline-block; + width: 2px; + height: 1em; + background: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); + animation: blink 1s step-end infinite; + vertical-align: text-bottom; + margin-left: 1px; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} diff --git a/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css b/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css new file mode 100644 index 0000000000..c79911473b --- /dev/null +++ b/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css @@ -0,0 +1,138 @@ +.sidebar { + height: 100%; + width: 100%; + padding: var(--mantine-spacing-md); + display: flex; + flex-direction: column; + gap: var(--mantine-spacing-xs); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: var(--mantine-spacing-xs); +} + +.title { + font-weight: 600; + font-size: var(--mantine-font-size-sm); +} + +.searchInput { + margin-bottom: var(--mantine-spacing-xs); +} + +.chatList { + flex: 1; + overflow-y: auto; +} + +.chatGroup + .chatGroup { + margin-top: var(--mantine-spacing-sm); +} + +.chatGroupLabel { + padding: 4px var(--mantine-spacing-xs); + font-size: var(--mantine-font-size-xs); + font-weight: 600; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + user-select: none; +} + +.chatListEmpty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--mantine-spacing-xl) var(--mantine-spacing-md); + text-align: center; + gap: 4px; + user-select: none; +} + +.chatListEmptyIcon { + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + margin-bottom: var(--mantine-spacing-xs); +} + +.chatListEmptyTitle { + font-size: var(--mantine-font-size-sm); + font-weight: 600; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); +} + +.chatListEmptyHint { + font-size: var(--mantine-font-size-xs); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3)); + line-height: 1.4; +} + +.chatItem { + display: flex; + align-items: center; + padding: 8px var(--mantine-spacing-xs); + border-radius: var(--mantine-radius-sm); + cursor: pointer; + text-decoration: none; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: var(--mantine-font-size-sm); + user-select: none; + gap: var(--mantine-spacing-xs); + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-6) + ); + } + + &[data-active] { + background-color: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-6) + ); + } +} + +.chatItemTitle { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chatItemDate { + font-size: var(--mantine-font-size-xs); + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + white-space: nowrap; + transition: opacity 150ms; +} + +.chatItemRenameInput { + font-size: var(--mantine-font-size-sm); + padding: 0; + height: auto; + min-height: 0; + background: transparent; + color: inherit; +} + +.chatItem:hover .chatItemDate { + opacity: 0; +} + +.chatItemActions { + position: absolute; + right: var(--mantine-spacing-xs); + opacity: 0; + transition: opacity 150ms; +} + +.chatItem { + position: relative; +} + +.chatItem:hover .chatItemActions { + opacity: 1; +} diff --git a/apps/client/src/ee/ai-chat/types/ai-chat.types.ts b/apps/client/src/ee/ai-chat/types/ai-chat.types.ts new file mode 100644 index 0000000000..89754d26e7 --- /dev/null +++ b/apps/client/src/ee/ai-chat/types/ai-chat.types.ts @@ -0,0 +1,49 @@ +export type AiChat = { + id: string; + workspaceId: string; + creatorId: string; + title: string | null; + createdAt: string; + updatedAt: string; +}; + +export type AiChatToolCall = { + id: string; + name: string; + args: Record; + result?: unknown; +}; + +export type AiChatMessage = { + id: string; + chatId: string; + role: 'user' | 'assistant' | 'tool'; + content: string | null; + toolCalls: AiChatToolCall[] | null; + metadata: Record | null; + createdAt: string; +}; + +export type AiChatStreamEvent = + | { type: 'chat_created'; chatId: string } + | { type: 'content'; text: string } + | { type: 'tool_call'; id: string; name: string; args: Record } + | { type: 'tool_result'; id: string; result: unknown } + | { type: 'done'; messageId: string; usage?: Record } + | { type: 'error'; message: string; code?: string; retryable?: boolean }; + +export type PageMention = { + id: string; + title: string; + slugId: string; + spaceSlug?: string; + icon?: string; +}; + +export type ChatAttachment = { + id: string; + fileName: string; + fileExt: string; + fileSize: number; + mimeType: string; +}; diff --git a/apps/client/src/ee/ai/pages/ai-settings.tsx b/apps/client/src/ee/ai/pages/ai-settings.tsx index c3f9381059..3a5a281efd 100644 --- a/apps/client/src/ee/ai/pages/ai-settings.tsx +++ b/apps/client/src/ee/ai/pages/ai-settings.tsx @@ -6,6 +6,7 @@ import useUserRole from "@/hooks/use-user-role.tsx"; import { useTranslation } from "react-i18next"; import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx"; import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx"; +import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx"; import McpSettings from "@/ee/ai/components/mcp-settings.tsx"; import { Alert, Stack, Tabs } from "@mantine/core"; import { IconInfoCircle } from "@tabler/icons-react"; @@ -71,6 +72,7 @@ export default function AiSettings() { {!isCloud() && } + diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index af6f8a1dc4..3ddf39762e 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -62,7 +62,7 @@ const MentionList = forwardRef((props, ref) => { query: props.query, includeUsers: true, includePages: true, - spaceId: space.id, + spaceId: space?.id, limit: props.query ? 10 : 5, preload: true, }); diff --git a/apps/client/src/features/editor/components/mention/mention-suggestion.ts b/apps/client/src/features/editor/components/mention/mention-suggestion.ts index 658fd182d5..894e720453 100644 --- a/apps/client/src/features/editor/components/mention/mention-suggestion.ts +++ b/apps/client/src/features/editor/components/mention/mention-suggestion.ts @@ -53,8 +53,8 @@ const mentionRenderItems = () => { const editorDom = props.editor?.view?.dom; const asideEl = editorDom?.closest(".mantine-AppShell-aside"); const dialogEl = editorDom?.closest("[data-comment-dialog]"); - const isInCommentContext = !!(asideEl || dialogEl); - // const isInCommentContext = !!asideEl; + const chatInput = editorDom?.closest("[data-chat-input]"); + const isInCommentContext = !!(asideEl || dialogEl || chatInput); component = new ReactRenderer(MentionList, { props: { ...props, isInCommentContext }, diff --git a/apps/client/src/features/home/components/home-ai-prompt.module.css b/apps/client/src/features/home/components/home-ai-prompt.module.css new file mode 100644 index 0000000000..e6d8160678 --- /dev/null +++ b/apps/client/src/features/home/components/home-ai-prompt.module.css @@ -0,0 +1,28 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--mantine-spacing-xl) var(--mantine-spacing-md) var(--mantine-spacing-lg); +} + +.heading { + font-size: 1.75rem; + font-weight: 600; + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); + text-align: center; + margin: 0; + line-height: 1.2; +} + +.subtitle { + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + text-align: center; + margin-top: 6px; + margin-bottom: var(--mantine-spacing-lg); +} + +.inputContainer { + width: 100%; + max-width: 640px; +} diff --git a/apps/client/src/features/home/components/home-ai-prompt.tsx b/apps/client/src/features/home/components/home-ai-prompt.tsx new file mode 100644 index 0000000000..c3c6939862 --- /dev/null +++ b/apps/client/src/features/home/components/home-ai-prompt.tsx @@ -0,0 +1,60 @@ +import { useAtomValue } from "jotai"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import ChatInput from "@/ee/ai-chat/components/chat-input"; +import type { + ChatAttachment, + PageMention, +} from "@/ee/ai-chat/types/ai-chat.types"; +import classes from "./home-ai-prompt.module.css"; + +export type HomeAiPromptInitialState = { + initialContent: string; + initialMentions: PageMention[]; + initialAttachments: ChatAttachment[]; +}; + +export default function HomeAiPrompt() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const workspace = useAtomValue(workspaceAtom); + + const aiChatEnabled = workspace?.settings?.ai?.chat === true; + if (!aiChatEnabled) return null; + + const handleSend = ( + content: string, + mentions: PageMention[], + attachments: ChatAttachment[], + ) => { + if (!content.trim() && attachments.length === 0) return; + const state: HomeAiPromptInitialState = { + initialContent: content, + initialMentions: mentions, + initialAttachments: attachments, + }; + navigate("/ai", { state }); + }; + + return ( +
+

+ {t("Welcome to {{name}}", { name: workspace?.name ?? "Docmost" })} +

+
+ {t("Ask anything or search your workspace")} +
+ +
+ {}} + placeholder={t("Ask anything... Use @ to mention pages")} + autofocus={false} + /> +
+
+ ); +} diff --git a/apps/client/src/features/search/components/search-control.module.css b/apps/client/src/features/search/components/search-control.module.css index 5e5a9c2697..80be81d9c7 100644 --- a/apps/client/src/features/search/components/search-control.module.css +++ b/apps/client/src/features/search/components/search-control.module.css @@ -29,6 +29,8 @@ border-radius: var(--mantine-radius-sm); border: 1px solid; font-weight: bold; + white-space: nowrap; + flex-shrink: 0; @mixin light { color: var(--mantine-color-gray-7); diff --git a/apps/client/src/features/space/components/space-carousel.module.css b/apps/client/src/features/space/components/space-carousel.module.css new file mode 100644 index 0000000000..87b9f1de7d --- /dev/null +++ b/apps/client/src/features/space/components/space-carousel.module.css @@ -0,0 +1,22 @@ +.card { + background-color: var(--mantine-color-body); + width: 220px; + + @mixin hover { + box-shadow: var(--mantine-shadow-xs); + transform: scale(1.02); + } +} + +.cardSection { + background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); +} + +.title { + font-family: + Greycliff CF, + var(--mantine-font-family); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/apps/client/src/features/space/components/space-carousel.tsx b/apps/client/src/features/space/components/space-carousel.tsx new file mode 100644 index 0000000000..e8c4eada0f --- /dev/null +++ b/apps/client/src/features/space/components/space-carousel.tsx @@ -0,0 +1,77 @@ +import { Text, Card, rem, Group, Button } from "@mantine/core"; +import { + prefetchSpace, + useGetSpacesQuery, +} from "@/features/space/queries/space-query.ts"; +import { getSpaceUrl } from "@/lib/config.ts"; +import { Link } from "react-router-dom"; +import classes from "./space-carousel.module.css"; +import { formatMemberCount } from "@/lib"; +import { useTranslation } from "react-i18next"; +import { IconArrowRight } from "@tabler/icons-react"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; +import CardCarousel from "@/components/ui/card-carousel"; + +export default function SpaceCarousel() { + const { t } = useTranslation(); + const { data } = useGetSpacesQuery({ limit: 20 }); + + const cards = data?.items.map((space) => ( + prefetchSpace(space.slug, space.id)} + className={classes.card} + withBorder + > + + + + + {space.name} + + + + {formatMemberCount(space.memberCount, t)} + + + )); + + return ( + <> + + + {t("Spaces you belong to")} + + + + {cards} + + {data?.items && data.items.length > 1 && ( + + + + )} + + ); +} diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index 4d98a5ef5c..96df25b78c 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -43,6 +43,7 @@ export interface IWorkspaceAiSettings { search?: boolean; generative?: boolean; mcp?: boolean; + chat?: boolean; } export interface IWorkspaceSharingSettings { diff --git a/apps/client/src/pages/dashboard/home.tsx b/apps/client/src/pages/dashboard/home.tsx index 900afa40de..cefff05383 100644 --- a/apps/client/src/pages/dashboard/home.tsx +++ b/apps/client/src/pages/dashboard/home.tsx @@ -1,6 +1,7 @@ import { Container, Space } from "@mantine/core"; import HomeTabs from "@/features/home/components/home-tabs"; -import SpaceGrid from "@/features/space/components/space-grid.tsx"; +import HomeAiPrompt from "@/features/home/components/home-ai-prompt"; +import SpaceCarousel from "@/features/space/components/space-carousel.tsx"; import { getAppName } from "@/lib/config.ts"; import { Helmet } from "react-helmet-async"; import { useTranslation } from "react-i18next"; @@ -16,7 +17,11 @@ export default function Home() { - + + + + + diff --git a/apps/server/package.json b/apps/server/package.json index 9994ba43d9..a1a061db62 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -75,10 +75,12 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "cookie": "^1.1.1", + "fast-bm25": "0.0.5", "fastify-ip": "^2.0.0", "fs-extra": "^11.3.4", "happy-dom": "20.8.9", "ioredis": "^5.10.1", + "js-tiktoken": "^1.0.21", "jsonwebtoken": "^9.0.3", "kysely": "^0.28.14", "kysely-migration-cli": "^0.4.2", diff --git a/apps/server/src/core/attachment/attachment.constants.ts b/apps/server/src/core/attachment/attachment.constants.ts index a9bce5c181..a5d906920c 100644 --- a/apps/server/src/core/attachment/attachment.constants.ts +++ b/apps/server/src/core/attachment/attachment.constants.ts @@ -3,6 +3,7 @@ export enum AttachmentType { WorkspaceIcon = 'workspace-icon', SpaceIcon = 'space-icon', File = 'file', + Chat = 'chat', } export const validImageExtensions = ['.jpg', '.png', '.jpeg']; diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index 6382b34e1e..7b24cc356d 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -178,21 +178,29 @@ export class AttachmentController { } const attachment = await this.attachmentRepo.findById(fileId); - if ( - !attachment || - attachment.workspaceId !== workspace.id || - !attachment.pageId || - !attachment.spaceId - ) { + if (!attachment || attachment.workspaceId !== workspace.id) { throw new NotFoundException(); } - const page = await this.pageRepo.findById(attachment.pageId); - if (!page) { - throw new NotFoundException(); - } + if (attachment.aiChatId) { + // Chat-owned attachment: only the user who uploaded (and therefore + // owns the chat, per AttachmentRepo.claimAttachmentsForChat) can + // read it back. + if (attachment.creatorId !== user.id) { + throw new NotFoundException(); + } + } else { + if (!attachment.pageId || !attachment.spaceId) { + throw new NotFoundException(); + } - await this.pageAccessService.validateCanView(page, user); + const page = await this.pageRepo.findById(attachment.pageId); + if (!page) { + throw new NotFoundException(); + } + + await this.pageAccessService.validateCanView(page, user); + } try { return await this.sendFileResponse(req, res, attachment, 'private'); diff --git a/apps/server/src/core/attachment/attachment.utils.ts b/apps/server/src/core/attachment/attachment.utils.ts index 88edb2af4b..616fed5303 100644 --- a/apps/server/src/core/attachment/attachment.utils.ts +++ b/apps/server/src/core/attachment/attachment.utils.ts @@ -71,6 +71,8 @@ export function getAttachmentFolderPath( return `${workspaceId}/space-logos`; case AttachmentType.File: return `${workspaceId}/files`; + case AttachmentType.Chat: + return `${workspaceId}/chat-files`; default: return `${workspaceId}/files`; } diff --git a/apps/server/src/core/attachment/processors/attachment.processor.ts b/apps/server/src/core/attachment/processors/attachment.processor.ts index bcf8a08a9e..7249a9fa25 100644 --- a/apps/server/src/core/attachment/processors/attachment.processor.ts +++ b/apps/server/src/core/attachment/processors/attachment.processor.ts @@ -28,6 +28,11 @@ export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy { job.data.pageId, ); } + if (job.name === QueueJob.DELETE_AI_CHAT_ATTACHMENTS) { + await this.attachmentService.handleDeleteAiChatAttachments( + job.data.aiChatId, + ); + } if ( job.name === QueueJob.ATTACHMENT_INDEX_CONTENT || job.name === QueueJob.ATTACHMENT_INDEXING diff --git a/apps/server/src/core/attachment/services/attachment.service.ts b/apps/server/src/core/attachment/services/attachment.service.ts index 6419ed58c6..766c9f65f3 100644 --- a/apps/server/src/core/attachment/services/attachment.service.ts +++ b/apps/server/src/core/attachment/services/attachment.service.ts @@ -289,6 +289,31 @@ export class AttachmentService { ); } + async handleDeleteAiChatAttachments(aiChatId: string) { + try { + const attachments = await this.attachmentRepo.findByAiChatId(aiChatId); + if (!attachments || attachments.length === 0) { + return; + } + + await Promise.all( + attachments.map(async (attachment) => { + try { + await this.storageService.delete(attachment.filePath); + await this.attachmentRepo.deleteAttachmentById(attachment.id); + } catch (err) { + this.logger.log( + `DeleteAiChatAttachments: failed to delete attachment ${attachment.id}:`, + err, + ); + } + }), + ); + } catch (err) { + throw err; + } + } + async handleDeleteSpaceAttachments(spaceId: string) { try { const attachments = await this.attachmentRepo.findBySpaceId(spaceId); diff --git a/apps/server/src/core/auth/auth.controller.ts b/apps/server/src/core/auth/auth.controller.ts index 441bfc1c9d..89bb9e1b08 100644 --- a/apps/server/src/core/auth/auth.controller.ts +++ b/apps/server/src/core/auth/auth.controller.ts @@ -11,6 +11,10 @@ import { Logger, } from '@nestjs/common'; import { SkipThrottle, ThrottlerGuard } from '@nestjs/throttler'; +import { + AI_CHAT_THROTTLER, + AUTH_THROTTLER, +} from '../../integrations/throttle/throttler-names'; import { LoginDto } from './dto/login.dto'; import { AuthService } from './services/auth.service'; import { SessionService } from '../session/session.service'; @@ -34,6 +38,7 @@ import { IAuditService, } from '../../integrations/audit/audit.service'; +@SkipThrottle({ [AI_CHAT_THROTTLER]: true }) @UseGuards(ThrottlerGuard) @Controller('auth') export class AuthController { @@ -113,7 +118,7 @@ export class AuthController { return workspace; } - @SkipThrottle() + @SkipThrottle({ [AUTH_THROTTLER]: true }) @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('change-password') @@ -176,7 +181,7 @@ export class AuthController { return this.authService.verifyUserToken(verifyUserTokenDto, workspace.id); } - @SkipThrottle() + @SkipThrottle({ [AUTH_THROTTLER]: true }) @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('collab-token') @@ -187,7 +192,7 @@ export class AuthController { return this.authService.getCollabToken(user, workspace.id); } - @SkipThrottle() + @SkipThrottle({ [AUTH_THROTTLER]: true }) @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('logout') diff --git a/apps/server/src/core/workspace/dto/update-workspace.dto.ts b/apps/server/src/core/workspace/dto/update-workspace.dto.ts index 9b822f50c1..d3f029d440 100644 --- a/apps/server/src/core/workspace/dto/update-workspace.dto.ts +++ b/apps/server/src/core/workspace/dto/update-workspace.dto.ts @@ -46,6 +46,10 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) { @IsBoolean() mcpEnabled: boolean; + @IsOptional() + @IsBoolean() + aiChat: boolean; + @IsOptional() @IsInt() @Min(1) diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index d0bd27c6d3..c6d822fff2 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -142,7 +142,7 @@ export class WorkspaceService { status = WorkspaceStatus.Active; plan = 'standard'; billingEmail = user.email; - settings = { ai: { generative: true } }; + settings = { ai: { generative: true, chat: true } }; } // create workspace @@ -458,11 +458,26 @@ export class WorkspaceService { ); } + if (typeof updateWorkspaceDto.aiChat !== 'undefined') { + const prev = settingsBefore?.ai?.chat ?? false; + if (prev !== updateWorkspaceDto.aiChat) { + before.aiChat = prev; + after.aiChat = updateWorkspaceDto.aiChat; + } + await this.workspaceRepo.updateAiSettings( + workspaceId, + 'chat', + updateWorkspaceDto.aiChat, + trx, + ); + } + delete updateWorkspaceDto.restrictApiToAdmins; delete updateWorkspaceDto.aiSearch; delete updateWorkspaceDto.generativeAi; delete updateWorkspaceDto.disablePublicSharing; delete updateWorkspaceDto.mcpEnabled; + delete updateWorkspaceDto.aiChat; await this.workspaceRepo.updateWorkspace( updateWorkspaceDto, diff --git a/apps/server/src/database/migrations/20260409T132415-ai-chat.ts b/apps/server/src/database/migrations/20260409T132415-ai-chat.ts new file mode 100644 index 0000000000..28b595f1dd --- /dev/null +++ b/apps/server/src/database/migrations/20260409T132415-ai-chat.ts @@ -0,0 +1,118 @@ +import { type Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('ai_chats') + .ifNotExists() + .addColumn('id', 'uuid', (col) => + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), + ) + .addColumn('workspace_id', 'uuid', (col) => + col.references('workspaces.id').onDelete('cascade').notNull(), + ) + .addColumn('creator_id', 'uuid', (col) => + col.references('users.id').notNull(), + ) + .addColumn('title', 'varchar', (col) => col) + .addColumn('created_at', 'timestamptz', (col) => + col.notNull().defaultTo(sql`now()`), + ) + .addColumn('updated_at', 'timestamptz', (col) => + col.notNull().defaultTo(sql`now()`), + ) + .addColumn('deleted_at', 'timestamptz', (col) => col) + .execute(); + + await db.schema + .createIndex('idx_ai_chats_workspace_creator') + .ifNotExists() + .on('ai_chats') + .columns(['workspace_id', 'creator_id', 'id']) + .execute(); + + await db.schema + .createTable('ai_chat_messages') + .ifNotExists() + .addColumn('id', 'uuid', (col) => + col.primaryKey().defaultTo(sql`gen_uuid_v7()`), + ) + .addColumn('chat_id', 'uuid', (col) => + col.references('ai_chats.id').onDelete('cascade').notNull(), + ) + .addColumn('workspace_id', 'uuid', (col) => + col.references('workspaces.id').onDelete('cascade').notNull(), + ) + .addColumn('user_id', 'uuid', (col) => + col.references('users.id').onDelete('set null'), + ) + .addColumn('role', 'varchar', (col) => col.notNull()) + .addColumn('content', 'text', (col) => col) + .addColumn('tool_calls', 'jsonb', (col) => col) + .addColumn('metadata', 'jsonb', (col) => col) + .addColumn('tsv', sql`tsvector`, (col) => col) + .addColumn('created_at', 'timestamptz', (col) => + col.notNull().defaultTo(sql`now()`), + ) + .addColumn('updated_at', 'timestamptz', (col) => + col.notNull().defaultTo(sql`now()`), + ) + .addColumn('deleted_at', 'timestamptz', (col) => col) + .execute(); + + await db.schema + .createIndex('idx_ai_chat_messages_chat_id') + .ifNotExists() + .on('ai_chat_messages') + .columns(['chat_id', 'id']) + .execute(); + + await db.schema + .createIndex('idx_ai_chat_messages_tsv') + .ifNotExists() + .on('ai_chat_messages') + .using('GIN') + .column('tsv') + .execute(); + + //ts-vector + await sql` + CREATE OR REPLACE FUNCTION ai_chat_messages_tsvector_trigger() RETURNS trigger AS $$ + BEGIN + NEW.tsv := to_tsvector('english', f_unaccent(substring(coalesce(NEW.content, ''), 1, 100000))); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + `.execute(db); + + await sql` + CREATE OR REPLACE TRIGGER ai_chat_messages_tsvector_update + BEFORE INSERT OR UPDATE ON ai_chat_messages + FOR EACH ROW EXECUTE FUNCTION ai_chat_messages_tsvector_trigger(); + `.execute(db); + + await db.schema + .alterTable('attachments') + .addColumn('ai_chat_id', 'uuid', (col) => col) + .execute(); + + await db.schema + .createIndex('idx_attachments_ai_chat_id') + .ifNotExists() + .on('attachments') + .column('ai_chat_id') + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('idx_attachments_ai_chat_id').execute(); + await db.schema.alterTable('attachments').dropColumn('ai_chat_id').execute(); + + await sql`DROP TRIGGER IF EXISTS ai_chat_messages_tsvector_update ON ai_chat_messages`.execute( + db, + ); + await sql`DROP FUNCTION IF EXISTS ai_chat_messages_tsvector_trigger`.execute( + db, + ); + await db.schema.dropTable('ai_chat_messages').execute(); + await db.schema.dropTable('ai_chats').execute(); +} diff --git a/apps/server/src/database/repos/attachment/attachment.repo.ts b/apps/server/src/database/repos/attachment/attachment.repo.ts index 5824ce5f92..bf2b5ecb5b 100644 --- a/apps/server/src/database/repos/attachment/attachment.repo.ts +++ b/apps/server/src/database/repos/attachment/attachment.repo.ts @@ -7,6 +7,7 @@ import { InsertableAttachment, UpdatableAttachment, } from '@docmost/db/types/entity.types'; +import { AttachmentType } from '../../../core/attachment/attachment.constants'; @Injectable() export class AttachmentRepo { @@ -23,6 +24,7 @@ export class AttachmentRepo { 'creatorId', 'pageId', 'spaceId', + 'aiChatId', 'workspaceId', 'createdAt', 'updatedAt', @@ -44,6 +46,21 @@ export class AttachmentRepo { .executeTakeFirst(); } + async findByIdWithContent( + attachmentId: string, + opts?: { + trx?: KyselyTransaction; + }, + ): Promise { + const db = dbOrTx(this.db, opts?.trx); + + return db + .selectFrom('attachments') + .select([...this.baseFields, 'textContent']) + .where('id', '=', attachmentId) + .executeTakeFirst(); + } + async insertAttachment( insertableAttachment: InsertableAttachment, trx?: KyselyTransaction, @@ -72,6 +89,21 @@ export class AttachmentRepo { .execute(); } + async findByAiChatId( + aiChatId: string, + opts?: { + trx?: KyselyTransaction; + }, + ): Promise { + const db = dbOrTx(this.db, opts?.trx); + + return db + .selectFrom('attachments') + .select(this.baseFields) + .where('aiChatId', '=', aiChatId) + .execute(); + } + updateAttachmentsByPageId( updatableAttachment: UpdatableAttachment, pageIds: string[], @@ -97,6 +129,25 @@ export class AttachmentRepo { .executeTakeFirst(); } + async claimAttachmentsForChat( + attachmentIds: string[], + aiChatId: string, + creatorId: string, + workspaceId: string, + ): Promise { + if (attachmentIds.length === 0) return; + + await this.db + .updateTable('attachments') + .set({ aiChatId }) + .where('id', 'in', attachmentIds) + .where('creatorId', '=', creatorId) + .where('workspaceId', '=', workspaceId) + .where('type', '=', AttachmentType.Chat) + .where('aiChatId', 'is', null) + .execute(); + } + async deleteAttachmentById(attachmentId: string): Promise { await this.db .deleteFrom('attachments') diff --git a/apps/server/src/database/types/db.d.ts b/apps/server/src/database/types/db.d.ts index fe4d5a13a9..3f4081e1ed 100644 --- a/apps/server/src/database/types/db.d.ts +++ b/apps/server/src/database/types/db.d.ts @@ -43,6 +43,7 @@ export interface ApiKeys { } export interface Attachments { + aiChatId: string | null; createdAt: Generated; creatorId: string; deletedAt: Timestamp | null; @@ -429,6 +430,31 @@ export interface PagePermissions { updatedAt: Generated; } +export interface AiChats { + id: Generated; + workspaceId: string; + creatorId: string; + title: string | null; + createdAt: Generated; + updatedAt: Generated; + deletedAt: Timestamp | null; +} + +export interface AiChatMessages { + id: Generated; + chatId: string; + workspaceId: string; + userId: string | null; + role: string; + content: string | null; + toolCalls: Json | null; + metadata: Json | null; + tsv: string | null; + createdAt: Generated; + updatedAt: Generated; + deletedAt: Timestamp | null; +} + export interface UserSessions { id: Generated; userId: string; @@ -445,6 +471,8 @@ export interface UserSessions { } export interface DB { + aiChats: AiChats; + aiChatMessages: AiChatMessages; apiKeys: ApiKeys; attachments: Attachments; audit: Audit; diff --git a/apps/server/src/database/types/entity.types.ts b/apps/server/src/database/types/entity.types.ts index d23e7475e0..8d4f482a58 100644 --- a/apps/server/src/database/types/entity.types.ts +++ b/apps/server/src/database/types/entity.types.ts @@ -1,5 +1,7 @@ import { Insertable, Selectable, Updateable } from 'kysely'; import { + AiChats, + AiChatMessages, Attachments, Comments, Groups, @@ -29,6 +31,21 @@ import { } from './db'; import { PageEmbeddings } from '@docmost/db/types/embeddings.types'; +// AI Chat +export type AiChat = Selectable; +export type InsertableAiChat = Insertable; +export type UpdatableAiChat = Updateable>; + +// AI Chat Message +// `tsv` is an internal tsvector column maintained by a trigger for +// full-text search. It is omitted from the public type so it never leaks +// into HTTP responses or the chat history fed to the language model. +export type AiChatMessage = Omit, 'tsv'>; +export type InsertableAiChatMessage = Omit< + Insertable, + 'tsv' +>; + // Workspace export type Workspace = Selectable; export type InsertableWorkspace = Insertable; diff --git a/apps/server/src/ee b/apps/server/src/ee index dc7ae0e3b0..d3bc4c5160 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit dc7ae0e3b066df842248b2295ccfad190a0c5a93 +Subproject commit d3bc4c5160fec9c4fabf769180f0ff00ef12042b diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index b1b27d40dc..ceb2eae7c9 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -252,6 +252,13 @@ export class EnvironmentService { return this.configService.get('AI_COMPLETION_MODEL'); } + getAiChatModel(): string { + return ( + this.configService.get('AI_CHAT_MODEL') || + this.configService.get('AI_COMPLETION_MODEL') + ); + } + getAiEmbeddingDimension(): number { return parseInt( this.configService.get('AI_EMBEDDING_DIMENSION'), diff --git a/apps/server/src/integrations/export/export.service.ts b/apps/server/src/integrations/export/export.service.ts index d93f9ba030..3f8da1bf97 100644 --- a/apps/server/src/integrations/export/export.service.ts +++ b/apps/server/src/integrations/export/export.service.ts @@ -353,7 +353,7 @@ export class ExportService { if (attachmentIds.length > 0) { const attachments = await this.db .selectFrom('attachments') - .selectAll() + .select(['id', 'fileName', 'filePath']) .where('id', 'in', attachmentIds) .where('spaceId', '=', spaceId) .execute(); diff --git a/apps/server/src/integrations/queue/constants/queue.constants.ts b/apps/server/src/integrations/queue/constants/queue.constants.ts index 92d15426a5..6d92f09c1e 100644 --- a/apps/server/src/integrations/queue/constants/queue.constants.ts +++ b/apps/server/src/integrations/queue/constants/queue.constants.ts @@ -17,6 +17,7 @@ export enum QueueJob { ATTACHMENT_INDEX_CONTENT = 'attachment-index-content', ATTACHMENT_INDEXING = 'attachment-indexing', DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments', + DELETE_AI_CHAT_ATTACHMENTS = 'delete-ai-chat-attachments', DELETE_USER_AVATARS = 'delete-user-avatars', diff --git a/apps/server/src/integrations/throttle/throttle.module.ts b/apps/server/src/integrations/throttle/throttle.module.ts index 8f080e1d9d..42dd0ec4a7 100644 --- a/apps/server/src/integrations/throttle/throttle.module.ts +++ b/apps/server/src/integrations/throttle/throttle.module.ts @@ -4,6 +4,7 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis' import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentModule } from '../environment/environment.module'; import { parseRedisUrl } from '../../common/helpers'; +import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names'; import Redis from 'ioredis'; @Module({ @@ -14,7 +15,10 @@ import Redis from 'ioredis'; const redisConfig = parseRedisUrl(environmentService.getRedisUrl()); return { - throttlers: [{ name: 'auth', ttl: 60_000, limit: 10 }], + throttlers: [ + { name: AUTH_THROTTLER, ttl: 60_000, limit: 10 }, + { name: AI_CHAT_THROTTLER, ttl: 60_000, limit: 25 }, + ], errorMessage: 'Too many requests', storage: new ThrottlerStorageRedisService( new Redis({ diff --git a/apps/server/src/integrations/throttle/throttler-names.ts b/apps/server/src/integrations/throttle/throttler-names.ts new file mode 100644 index 0000000000..388ba29d70 --- /dev/null +++ b/apps/server/src/integrations/throttle/throttler-names.ts @@ -0,0 +1,2 @@ +export const AUTH_THROTTLER = 'auth'; +export const AI_CHAT_THROTTLER = 'ai-chat'; diff --git a/apps/server/src/integrations/throttle/user-throttler.guard.ts b/apps/server/src/integrations/throttle/user-throttler.guard.ts new file mode 100644 index 0000000000..35744c0940 --- /dev/null +++ b/apps/server/src/integrations/throttle/user-throttler.guard.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { ThrottlerGuard } from '@nestjs/throttler'; + +type AuthedRequest = { user?: { id?: string } }; + +@Injectable() +export class UserThrottlerGuard extends ThrottlerGuard { + protected async getTracker(req: AuthedRequest): Promise { + const userId = req.user?.id; + if (userId) return `user:${userId}`; + return super.getTracker(req as Parameters[0]); + } +} diff --git a/packages/editor-ext/src/lib/markdown/utils/marked.utils.ts b/packages/editor-ext/src/lib/markdown/utils/marked.utils.ts index 04dc1978d5..7556aa4f00 100644 --- a/packages/editor-ext/src/lib/markdown/utils/marked.utils.ts +++ b/packages/editor-ext/src/lib/markdown/utils/marked.utils.ts @@ -37,6 +37,8 @@ marked.use({ extensions: [calloutExtension, mathBlockExtension, mathInlineExtension], }); +marked.setOptions({ breaks: true }); + export function markdownToHtml( markdownInput: string, ): string | Promise { @@ -46,8 +48,5 @@ export function markdownToHtml( .replace(YAML_FONT_MATTER_REGEX, "") .trimStart(); - return marked - .options({ breaks: true }) - .parse(markdown) - .toString(); + return marked.parse(markdown).toString(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff37ec7826..cfdfcdcbc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -589,6 +589,9 @@ importers: cookie: specifier: ^1.1.1 version: 1.1.1 + fast-bm25: + specifier: 0.0.5 + version: 0.0.5(typescript@5.9.3) fastify-ip: specifier: ^2.0.0 version: 2.0.0 @@ -601,6 +604,9 @@ importers: ioredis: specifier: ^5.10.1 version: 5.10.1 + js-tiktoken: + specifier: ^1.0.21 + version: 1.0.21 jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 @@ -6874,6 +6880,12 @@ packages: exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + fast-bm25@0.0.5: + resolution: {integrity: sha512-6HTiLmPkgeqcPJHccN0pXdqnA7OzhaEQZTFzWnfjIyPoX5sGVKUUpfRc2K2o6zMwK+g009miRhADYn/f2Ax0Mg==} + engines: {node: '>=14.0.0'} + peerDependencies: + typescript: ^5.6.3 + fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} @@ -8966,6 +8978,9 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + porter2@1.1.0: + resolution: {integrity: sha512-Io2cLEdZn0O1dH60pRsjmr/cH/qJJ/j6Cjubz8wQWi0b6vPdQIUxSBQKyx9d+8CN7fSnY+5uOU3rErMFjNqcLw==} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -17795,6 +17810,11 @@ snapshots: exsolve@1.0.7: {} + fast-bm25@0.0.5(typescript@5.9.3): + dependencies: + porter2: 1.1.0 + typescript: 5.9.3 + fast-copy@4.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -20144,6 +20164,8 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 + porter2@1.1.0: {} + possible-typed-array-names@1.0.0: {} postcss-js@4.0.1(postcss@8.5.8): From d42091ccb1ff29d6ef88e4c5d0e4d6461edd10f8 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:06:25 +0100 Subject: [PATCH 06/22] feat: favorites (#2103) * feat: favorites and templates(ee) * rename migrations * fix sidebar * cleanup tabs * fix * turn off templates * cleanup * uuid validation --- .../public/locales/en-US/translation.json | 8 + apps/client/src/App.tsx | 9 + .../src/components/common/recent-changes.tsx | 114 +++--- .../components/layouts/global/app-header.tsx | 43 +- .../layouts/global/global-app-shell.tsx | 50 ++- .../layouts/global/global-sidebar.module.css | 89 +++++ .../layouts/global/global-sidebar.tsx | 158 ++++++++ .../destination-picker-modal.tsx | 67 ++++ .../destination-picker.module.css | 128 ++++++ .../destination-picker/destination-picker.tsx | 168 ++++++++ .../destination-picker.types.ts | 23 ++ .../ui/destination-picker/page-children.tsx | 83 ++++ .../ui/destination-picker/page-row.tsx | 89 +++++ .../ui/destination-picker/space-row.tsx | 108 +++++ .../ee/ai-chat/components/ai-chat-sidebar.tsx | 46 +-- .../ee/ai-chat/utils/group-chats-by-age.ts | 45 +++ apps/client/src/ee/features.ts | 1 + .../components/allow-member-templates.tsx | 70 ++++ .../client/src/ee/security/pages/security.tsx | 1 + .../components/create-template-modal.tsx | 118 ++++++ .../components/readonly-template-editor.tsx | 49 +++ .../components/template-card.module.css | 67 ++++ .../ee/template/components/template-card.tsx | 100 +++++ .../ee/template/components/template-meta.tsx | 37 ++ .../components/template-preview-modal.tsx | 65 +++ .../components/use-template-modal.tsx | 59 +++ .../template/pages/template-editor.module.css | 68 ++++ .../src/ee/template/pages/template-editor.tsx | 373 ++++++++++++++++++ .../src/ee/template/pages/template-list.tsx | 213 ++++++++++ .../src/ee/template/queries/template-query.ts | 167 ++++++++ .../ee/template/services/template-service.ts | 46 +++ .../src/ee/template/types/template.types.ts | 18 + .../components/slash-menu/menu-items.ts | 5 +- .../features/editor/extensions/extensions.ts | 25 +- .../editor/extensions/slash-command.ts | 1 + .../favorite/components/star-button.tsx | 76 ++++ .../favorite/queries/favorite-query.ts | 78 ++++ .../favorite/services/favorite-service.ts | 31 ++ .../features/favorite/types/favorite.types.ts | 32 ++ .../src/features/home/atoms/home-tab-atom.ts | 3 + .../home/components/created-by-me.tsx | 126 ++++++ .../home/components/favorites-pages.tsx | 124 ++++++ .../features/home/components/home-tabs.tsx | 35 +- .../components/header/page-header-menu.tsx | 34 ++ .../src/features/page/queries/page-query.ts | 29 +- .../features/page/services/page-service.ts | 13 +- .../page/tree/components/space-tree.tsx | 22 ++ .../src/features/page/types/page.types.ts | 1 + .../components/sidebar/space-sidebar.tsx | 13 +- .../space/components/space-grid.module.css | 17 + .../features/space/components/space-grid.tsx | 13 +- .../space/components/space-home-tabs.tsx | 37 +- .../spaces-page/all-spaces-list.tsx | 2 + .../spaces-page/favorite-spaces-grid.tsx | 90 +++++ .../src/features/space/queries/space-query.ts | 5 +- .../workspace/types/workspace.types.ts | 6 + apps/client/src/hooks/use-time-ago.tsx | 4 +- apps/client/src/lib/app-route.ts | 1 + apps/client/src/pages/dashboard/home.tsx | 2 +- .../src/pages/favorites/favorites-page.tsx | 139 +++++++ apps/client/src/pages/space/space-home.tsx | 2 +- apps/client/src/pages/spaces/spaces.tsx | 5 +- apps/server/src/common/features.ts | 1 + apps/server/src/core/core.module.ts | 2 + .../src/core/favorite/dto/favorite.dto.ts | 28 ++ .../core/favorite/dto/list-favorites.dto.ts | 8 + .../src/core/favorite/favorite.controller.ts | 136 +++++++ .../src/core/favorite/favorite.module.ts | 10 + .../favorite/services/favorite.service.ts | 110 ++++++ .../core/group/services/group-user.service.ts | 8 + .../src/core/group/services/group.service.ts | 8 + .../src/core/page/dto/created-by-user.dto.ts | 11 + .../src/core/page/dto/sidebar-page.dto.ts | 1 - apps/server/src/core/page/page.controller.ts | 24 ++ .../src/core/page/services/page.service.ts | 29 +- .../space/services/space-member.service.ts | 8 + .../server/src/core/space/space.controller.ts | 36 +- .../workspace/dto/update-workspace.dto.ts | 4 + .../workspace/services/workspace.service.ts | 27 +- apps/server/src/database/database.module.ts | 6 + .../migrations/20260412T135891-templates.ts | 76 ++++ .../migrations/20260412T162318-favorites.ts | 63 +++ .../database/repos/favorite/favorite.repo.ts | 216 ++++++++++ .../src/database/repos/page/page.repo.ts | 29 ++ .../database/repos/space/space-member.repo.ts | 26 ++ .../database/repos/template/template.repo.ts | 160 ++++++++ .../repos/workspace/workspace.repo.ts | 20 + apps/server/src/database/types/db.d.ts | 32 ++ .../server/src/database/types/entity.types.ts | 12 + apps/server/src/ee | 2 +- 90 files changed, 4557 insertions(+), 187 deletions(-) create mode 100644 apps/client/src/components/layouts/global/global-sidebar.module.css create mode 100644 apps/client/src/components/layouts/global/global-sidebar.tsx create mode 100644 apps/client/src/components/ui/destination-picker/destination-picker-modal.tsx create mode 100644 apps/client/src/components/ui/destination-picker/destination-picker.module.css create mode 100644 apps/client/src/components/ui/destination-picker/destination-picker.tsx create mode 100644 apps/client/src/components/ui/destination-picker/destination-picker.types.ts create mode 100644 apps/client/src/components/ui/destination-picker/page-children.tsx create mode 100644 apps/client/src/components/ui/destination-picker/page-row.tsx create mode 100644 apps/client/src/components/ui/destination-picker/space-row.tsx create mode 100644 apps/client/src/ee/ai-chat/utils/group-chats-by-age.ts create mode 100644 apps/client/src/ee/security/components/allow-member-templates.tsx create mode 100644 apps/client/src/ee/template/components/create-template-modal.tsx create mode 100644 apps/client/src/ee/template/components/readonly-template-editor.tsx create mode 100644 apps/client/src/ee/template/components/template-card.module.css create mode 100644 apps/client/src/ee/template/components/template-card.tsx create mode 100644 apps/client/src/ee/template/components/template-meta.tsx create mode 100644 apps/client/src/ee/template/components/template-preview-modal.tsx create mode 100644 apps/client/src/ee/template/components/use-template-modal.tsx create mode 100644 apps/client/src/ee/template/pages/template-editor.module.css create mode 100644 apps/client/src/ee/template/pages/template-editor.tsx create mode 100644 apps/client/src/ee/template/pages/template-list.tsx create mode 100644 apps/client/src/ee/template/queries/template-query.ts create mode 100644 apps/client/src/ee/template/services/template-service.ts create mode 100644 apps/client/src/ee/template/types/template.types.ts create mode 100644 apps/client/src/features/favorite/components/star-button.tsx create mode 100644 apps/client/src/features/favorite/queries/favorite-query.ts create mode 100644 apps/client/src/features/favorite/services/favorite-service.ts create mode 100644 apps/client/src/features/favorite/types/favorite.types.ts create mode 100644 apps/client/src/features/home/atoms/home-tab-atom.ts create mode 100644 apps/client/src/features/home/components/created-by-me.tsx create mode 100644 apps/client/src/features/home/components/favorites-pages.tsx create mode 100644 apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx create mode 100644 apps/client/src/pages/favorites/favorites-page.tsx create mode 100644 apps/server/src/core/favorite/dto/favorite.dto.ts create mode 100644 apps/server/src/core/favorite/dto/list-favorites.dto.ts create mode 100644 apps/server/src/core/favorite/favorite.controller.ts create mode 100644 apps/server/src/core/favorite/favorite.module.ts create mode 100644 apps/server/src/core/favorite/services/favorite.service.ts create mode 100644 apps/server/src/core/page/dto/created-by-user.dto.ts create mode 100644 apps/server/src/database/migrations/20260412T135891-templates.ts create mode 100644 apps/server/src/database/migrations/20260412T162318-favorites.ts create mode 100644 apps/server/src/database/repos/favorite/favorite.repo.ts create mode 100644 apps/server/src/database/repos/template/template.repo.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index b83b3548bb..7f3eb11cfe 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -7,6 +7,7 @@ "Add members": "Add members", "Add to groups": "Add to groups", "Add space members": "Add space members", + "Add to favorites": "Add to favorites", "Admin": "Admin", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.", "Are you sure you want to delete this page?": "Are you sure you want to delete this page?", @@ -74,6 +75,9 @@ "Failed to import pages": "Failed to import pages", "Failed to load page. An error occurred.": "Failed to load page. An error occurred.", "Failed to update data": "Failed to update data", + "Favorite spaces": "Favorite spaces", + "Favorite spaces appear here": "Favorite spaces appear here", + "Favorites": "Favorites", "Full access": "Full access", "Full page width": "Full page width", "Full width": "Full width", @@ -92,6 +96,7 @@ "Invite by email": "Invite by email", "Invite members": "Invite members", "Invite new members": "Invite new members", + "Invite People": "Invite People", "Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.", "Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access", "Join the workspace": "Join the workspace", @@ -139,6 +144,7 @@ "Profile": "Profile", "Recently updated": "Recently updated", "Remove": "Remove", + "Remove from favorites": "Remove from favorites", "Remove group member": "Remove group member", "Remove space member": "Remove space member", "Restore": "Restore", @@ -175,6 +181,7 @@ "Successfully imported": "Successfully imported", "Successfully restored": "Successfully restored", "System settings": "System settings", + "Templates": "Templates", "Theme": "Theme", "To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.", "Toggle full page width": "Toggle full page width", @@ -478,6 +485,7 @@ "Replace (Enter)": "Replace (Enter)", "Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)", "Replace all": "Replace all", + "View all": "View all", "View all spaces": "View all spaces", "Error": "Error", "Failed to disable MFA": "Failed to disable MFA", diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 382660c750..8c24b3d1a6 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -38,6 +38,9 @@ import UserApiKeys from "@/ee/api-key/pages/user-api-keys"; import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys"; import AiSettings from "@/ee/ai/pages/ai-settings.tsx"; import AuditLogs from "@/ee/audit/pages/audit-logs.tsx"; +import TemplateList from "@/ee/template/pages/template-list"; +import TemplateEditor from "@/ee/template/pages/template-editor"; +import FavoritesPage from "@/pages/favorites/favorites-page"; import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx"; import VerifyEmail from "@/ee/pages/verify-email.tsx"; @@ -85,6 +88,12 @@ export default function App() { } /> } /> } /> + } /> + } /> + } + /> } /> } /> p.items) ?? []; if (isLoading) { return ; @@ -33,58 +35,72 @@ export default function RecentChanges({ spaceId }: Props) { return {t("Failed to fetch recent pages")}; } - return pages && pages.items.length > 0 ? ( - - - - {pages.items.map((page) => ( - - - - - {page.icon || ( - - - - )} - - - {page.title || t("Untitled")} - - - - - {!spaceId && ( + return pages.length > 0 ? ( + <> + +
+ + {pages.map((page) => ( + - + + {page.icon || ( + + + + )} + + + {page.title || t("Untitled")} + + + + + {!spaceId && ( + + + {page?.space.name} + + + )} + + - {page?.space.name} - + {formattedDate(page.updatedAt)} + - )} - - - {formattedDate(page.updatedAt)} - - - - ))} - -
-
+ + ))} + + + + {hasNextPage && ( + + )} + ) : ( ( @@ -67,29 +64,25 @@ export function AppHeader() { <> - {!hideSidebar && ( - <> - - - + + + - - - - - )} + + + diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index 066298988f..64bd3dde7e 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -16,6 +16,7 @@ import Aside from "@/components/layouts/global/aside.tsx"; import classes from "./app-shell.module.css"; import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx"; import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; +import GlobalSidebar from "@/components/layouts/global/global-sidebar.tsx"; export default function GlobalAppShell({ children, @@ -74,24 +75,20 @@ export default function GlobalAppShell({ const isSettingsRoute = location.pathname.startsWith("/settings"); const isSpaceRoute = location.pathname.startsWith("/s/"); const isAiRoute = location.pathname.startsWith("/ai"); - const isHomeRoute = location.pathname.startsWith("/home"); - const isSpacesRoute = location.pathname === "/spaces"; const isPageRoute = location.pathname.includes("/p/"); - const hideSidebar = isHomeRoute || isSpacesRoute; + const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute; return ( - {!hideSidebar && ( - - {!isAiRoute &&
} - {isSpaceRoute && } - {isSettingsRoute && } - {isAiRoute && } - - )} + + {isSpaceRoute && ( +
+ )} + {isSpaceRoute && } + {isSettingsRoute && } + {isAiRoute && } + {showGlobalSidebar && } + {isSettingsRoute ? ( - {children} + {children} ) : ( children )} diff --git a/apps/client/src/components/layouts/global/global-sidebar.module.css b/apps/client/src/components/layouts/global/global-sidebar.module.css new file mode 100644 index 0000000000..37cb60b6f1 --- /dev/null +++ b/apps/client/src/components/layouts/global/global-sidebar.module.css @@ -0,0 +1,89 @@ +.navbar { + height: 100%; + width: 100%; + padding: var(--mantine-spacing-md); + display: flex; + flex-direction: column; +} + +.section { + padding-bottom: var(--mantine-spacing-xs); +} + +.link { + cursor: pointer; + display: flex; + align-items: center; + text-decoration: none; + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + padding-left: var(--mantine-spacing-xs); + min-height: 30px; + border-radius: var(--mantine-radius-sm); + font-weight: 500; + user-select: none; + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-6) + ); + color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + } + + &[data-active] { + &, + & :hover { + background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); + color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + } + } +} + +.linkIcon { + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + margin-right: var(--mantine-spacing-sm); + width: rem(16px); + height: rem(16px); +} + +.sectionHeader { + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + font-size: var(--mantine-font-size-xs); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3)); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.spacer { + flex: 1; +} + +.bottomSection { + padding-top: var(--mantine-spacing-xs); + border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); +} + +.spaceItem { + cursor: pointer; + display: flex; + align-items: center; + gap: var(--mantine-spacing-sm); + text-decoration: none; + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + padding-left: var(--mantine-spacing-xs); + min-height: 30px; + border-radius: var(--mantine-radius-sm); + font-weight: 500; + user-select: none; + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-6) + ); + color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + } +} diff --git a/apps/client/src/components/layouts/global/global-sidebar.tsx b/apps/client/src/components/layouts/global/global-sidebar.tsx new file mode 100644 index 0000000000..4d86a2649a --- /dev/null +++ b/apps/client/src/components/layouts/global/global-sidebar.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from "react"; +import { ScrollArea, Text, Divider, Modal } from "@mantine/core"; +import { + IconHome, + IconClock, + IconStar, + IconLayoutGrid, + IconSettings, + IconUserPlus, +} from "@tabler/icons-react"; +import { Link, useLocation } from "react-router-dom"; +import classes from "./global-sidebar.module.css"; +import { useTranslation } from "react-i18next"; +import { useAtom } from "jotai"; +import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom"; +import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar"; +import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query"; +import { getSpaceUrl } from "@/lib/config"; +import { useDisclosure } from "@mantine/hooks"; +import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form"; +import { CustomAvatar } from "@/components/ui/custom-avatar"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types"; + +const mainNavItems = [ + { label: "Home", icon: IconHome, path: "/home" }, + { label: "Favorites", icon: IconStar, path: "/favorites" }, + { label: "Spaces", icon: IconLayoutGrid, path: "/spaces" }, +]; + +export default function GlobalSidebar() { + const { t } = useTranslation(); + const location = useLocation(); + const [active, setActive] = useState(location.pathname); + const [mobileSidebarOpened] = useAtom(mobileSidebarAtom); + const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom); + const { data: favoriteSpacesData } = useFavoritesQuery("space"); + const favoriteSpaces = favoriteSpacesData?.pages.flatMap((p) => p.items) ?? []; + const sortedFavoriteSpaces = [...favoriteSpaces] + .filter((fav) => fav.space) + .sort((a, b) => { + const cmp = (a.space!.name ?? "").localeCompare(b.space!.name ?? "", undefined, { sensitivity: "base" }); + return cmp !== 0 ? cmp : a.id.localeCompare(b.id); + }); + const [inviteOpened, { open: openInvite, close: closeInvite }] = + useDisclosure(false); + + useEffect(() => { + setActive(location.pathname); + }, [location.pathname]); + + const handleNavClick = () => { + if (mobileSidebarOpened) { + toggleMobileSidebar(); + } + }; + + return ( +
+ +
+ {mainNavItems.map((item) => ( + + + {t(item.label)} + + ))} +
+ + +
+ {t("Favorite spaces")} + {sortedFavoriteSpaces.length === 0 ? ( + + {t("Favorite spaces appear here")} + + ) : ( + <> + {sortedFavoriteSpaces.slice(0, 10).map((fav) => ( + + + + {fav.space!.name} + + + ))} + {sortedFavoriteSpaces.length > 10 && ( + + + {t("View all")} + + + )} + + )} +
+ +
+ + + + + + + + + +
+ ); +} diff --git a/apps/client/src/components/ui/destination-picker/destination-picker-modal.tsx b/apps/client/src/components/ui/destination-picker/destination-picker-modal.tsx new file mode 100644 index 0000000000..04e9ef7dd9 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/destination-picker-modal.tsx @@ -0,0 +1,67 @@ +import { useState, useEffect } from "react"; +import { Modal, Button, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { DestinationPicker } from "./destination-picker"; +import { + DestinationPickerModalProps, + DestinationSelection, +} from "./destination-picker.types"; + +export function DestinationPickerModal({ + opened, + onClose, + title, + actionLabel, + onSelect, + loading, + excludePageId, + pageLimit, +}: DestinationPickerModalProps) { + const { t } = useTranslation(); + const [selection, setSelection] = useState(null); + + useEffect(() => { + if (!opened) { + setSelection(null); + } + }, [opened]); + + return ( + e.stopPropagation()} + > + + + + {title} + + + + + + + + + + + + + ); +} diff --git a/apps/client/src/components/ui/destination-picker/destination-picker.module.css b/apps/client/src/components/ui/destination-picker/destination-picker.module.css new file mode 100644 index 0000000000..ec598ac504 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/destination-picker.module.css @@ -0,0 +1,128 @@ +.searchInput { + margin-bottom: var(--mantine-spacing-sm); +} + +.scrollArea { + max-height: 50vh; +} + +.row { + padding: 6px 12px; + border-radius: var(--mantine-radius-sm); + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: background-color 150ms ease; + user-select: none; + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-6) + ); + } +} + +.selected { + background-color: light-dark( + var(--mantine-color-blue-0), + var(--mantine-color-dark-5) + ); + border-left: 2px solid var(--mantine-primary-color-filled); +} + +.spaceRow { + composes: row; + font-weight: 500; +} + +.pageRow { + composes: row; + font-weight: 400; +} + +.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.chevron { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--mantine-radius-sm); + flex-shrink: 0; + transition: transform 150ms ease; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + } +} + +.chevronExpanded { + transform: rotate(90deg); +} + +.loadMore { + text-align: center; + padding: 6px; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + font-size: var(--mantine-font-size-sm); + cursor: pointer; + + @mixin hover { + text-decoration: underline; + } +} + +.selectedIndicator { + padding: 8px 12px; + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + margin-top: var(--mantine-spacing-xs); +} + +.emptyState { + padding: 12px; + text-align: center; + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); +} + +.searchResult { + composes: row; +} + +.pageTitle { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--mantine-font-size-sm); +} + +.spaceName { + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + font-size: var(--mantine-font-size-xs); + flex-shrink: 0; +} + +.iconWrapper { + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 16px; + line-height: 1; +} diff --git a/apps/client/src/components/ui/destination-picker/destination-picker.tsx b/apps/client/src/components/ui/destination-picker/destination-picker.tsx new file mode 100644 index 0000000000..1ddc747dd8 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/destination-picker.tsx @@ -0,0 +1,168 @@ +import { useState, useCallback } from "react"; +import { TextInput, ScrollArea, Loader } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { IconSearch, IconFile } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useGetSpacesQuery } from "@/features/space/queries/space-query"; +import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query"; +import { ISpace } from "@/features/space/types/space.types"; +import { IPage } from "@/features/page/types/page.types"; +import { DestinationSelection } from "./destination-picker.types"; +import { SpaceRow } from "./space-row"; +import classes from "./destination-picker.module.css"; + +type DestinationPickerProps = { + onSelectionChange: (selection: DestinationSelection | null) => void; + excludePageId?: string; + pageLimit?: number; +}; + +export function DestinationPicker({ + onSelectionChange, + excludePageId, + pageLimit = 15, +}: DestinationPickerProps) { + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = useState(""); + const [selection, setSelection] = useState(null); + const [debouncedQuery] = useDebouncedValue(searchQuery, 300); + + const { data: spacesData, isLoading: spacesLoading } = useGetSpacesQuery({ + limit: 100, + }); + + const searchEnabled = debouncedQuery && debouncedQuery.length >= 2; + + const { data: searchData, isLoading: searchLoading } = + useSearchSuggestionsQuery({ + query: searchEnabled ? debouncedQuery : "", + includePages: true, + limit: 20, + }); + + const isSearching = !!searchEnabled; + + const selectedId = + selection?.type === "space" ? selection.spaceId : selection?.pageId ?? null; + + const updateSelection = useCallback( + (next: DestinationSelection | null) => { + setSelection(next); + onSelectionChange(next); + }, + [onSelectionChange], + ); + + const handleSearchResultClick = (page: Partial) => { + if (!page.space || !page.id) return; + + updateSelection({ + type: "page", + spaceId: page.space.id, + pageId: page.id, + page, + space: page.space, + }); + setSearchQuery(""); + }; + + const handleSelectSpace = useCallback( + (space: ISpace) => { + updateSelection({ type: "space", spaceId: space.id, space }); + }, + [updateSelection], + ); + + const handleSelectPage = useCallback( + (page: Partial, space: ISpace) => { + if (!page.id) return; + updateSelection({ + type: "page", + spaceId: page.spaceId ?? space.id, + pageId: page.id, + page, + space, + }); + }, + [updateSelection], + ); + + return ( + <> + } + placeholder={t("Search pages and spaces...")} + variant="filled" + value={searchQuery} + onChange={(e) => setSearchQuery(e.currentTarget.value)} + className={classes.searchInput} + /> + + + {isSearching ? ( + searchLoading ? ( +
+ +
+ ) : searchData?.pages && searchData.pages.length > 0 ? ( + searchData.pages.map( + (page) => + page && ( +
handleSearchResultClick(page)} + > +
+ {page.icon ? ( + page.icon + ) : ( + + )} +
+
+ {page.title || t("Untitled")} +
+ {page.space && ( +
+ {page.space.name} +
+ )} +
+ ), + ) + ) : ( +
{t("No results found")}
+ ) + ) : spacesLoading ? ( +
+ +
+ ) : ( + spacesData?.items?.map((space) => ( + + )) + )} +
+ + {selection && ( +
+ {selection.type === "space" + ? selection.space.name + : `${selection.space.name} / ${selection.page.title || t("Untitled")}`} +
+ )} + + ); +} diff --git a/apps/client/src/components/ui/destination-picker/destination-picker.types.ts b/apps/client/src/components/ui/destination-picker/destination-picker.types.ts new file mode 100644 index 0000000000..005259b3a9 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/destination-picker.types.ts @@ -0,0 +1,23 @@ +import { ISpace } from "@/features/space/types/space.types"; +import { IPage } from "@/features/page/types/page.types"; + +export type DestinationSelection = + | { type: "space"; spaceId: string; space: ISpace } + | { + type: "page"; + spaceId: string; + pageId: string; + page: Partial; + space: Partial; + }; + +export type DestinationPickerModalProps = { + opened: boolean; + onClose: () => void; + title: string; + actionLabel: string; + onSelect: (selection: DestinationSelection) => void | Promise; + loading?: boolean; + excludePageId?: string; + pageLimit?: number; +}; diff --git a/apps/client/src/components/ui/destination-picker/page-children.tsx b/apps/client/src/components/ui/destination-picker/page-children.tsx new file mode 100644 index 0000000000..9db5fd7153 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/page-children.tsx @@ -0,0 +1,83 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Loader } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { getSidebarPages } from "@/features/page/services/page-service"; +import { IPage } from "@/features/page/types/page.types"; +import { IPagination } from "@/lib/types"; +import { PageRow } from "./page-row"; +import classes from "./destination-picker.module.css"; + +type PageChildrenProps = { + spaceId: string; + pageId?: string; + depth: number; + limit: number; + selectedId: string | null; + excludePageId?: string; + onSelectPage: (page: Partial) => void; +}; + +export function PageChildren({ + spaceId, + pageId, + depth, + limit, + selectedId, + excludePageId, + onSelectPage, +}: PageChildrenProps) { + const { t } = useTranslation(); + + const { data, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({ + queryKey: ["destination-pages", spaceId, pageId ?? "root"], + queryFn: ({ pageParam }) => + getSidebarPages({ + spaceId, + pageId, + limit, + cursor: pageParam, + }), + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage: IPagination) => + lastPage.meta?.nextCursor ?? undefined, + }); + + const pages = data?.pages.flatMap((page) => page.items) ?? []; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (pages.length === 0) { + return ( +
+ {pageId ? t("No pages inside") : t("No pages in this space")} +
+ ); + } + + return ( + <> + {pages.map((page) => ( + + ))} + {hasNextPage && ( +
fetchNextPage()}> + {t("Load more")} +
+ )} + + ); +} diff --git a/apps/client/src/components/ui/destination-picker/page-row.tsx b/apps/client/src/components/ui/destination-picker/page-row.tsx new file mode 100644 index 0000000000..58c7a0a87e --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/page-row.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { IconChevronRight, IconFile } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { IPage } from "@/features/page/types/page.types"; +import { PageChildren } from "./page-children"; +import classes from "./destination-picker.module.css"; + +type PageRowProps = { + page: Partial; + depth: number; + limit: number; + selectedId: string | null; + excludePageId?: string; + onSelect: (page: Partial) => void; +}; + +export function PageRow({ + page, + depth, + limit, + selectedId, + excludePageId, + onSelect, +}: PageRowProps) { + const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + + const isExcluded = page.id === excludePageId; + const isSelected = page.id === selectedId; + + const rowClasses = [ + classes.pageRow, + isSelected && classes.selected, + isExcluded && classes.disabled, + ] + .filter(Boolean) + .join(" "); + + return ( + <> +
!isExcluded && onSelect(page)} + > + {page.hasChildren ? ( +
{ + e.stopPropagation(); + setExpanded(!expanded); + }} + > + +
+ ) : ( +
+ )} + +
+ {page.icon ? ( + page.icon + ) : ( + + )} +
+ +
+ {page.title || t("Untitled")} +
+
+ + {expanded && page.hasChildren && ( + + )} + + ); +} diff --git a/apps/client/src/components/ui/destination-picker/space-row.tsx b/apps/client/src/components/ui/destination-picker/space-row.tsx new file mode 100644 index 0000000000..59273af7e8 --- /dev/null +++ b/apps/client/src/components/ui/destination-picker/space-row.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { Tooltip } from "@mantine/core"; +import { IconChevronRight, IconLock } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { ISpace } from "@/features/space/types/space.types"; +import { IPage } from "@/features/page/types/page.types"; +import { SpaceRole } from "@/lib/types"; +import { CustomAvatar } from "@/components/ui/custom-avatar"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types"; +import { PageChildren } from "./page-children"; +import classes from "./destination-picker.module.css"; + +type SpaceRowProps = { + space: ISpace; + limit: number; + selectedId: string | null; + excludePageId?: string; + onSelectSpace: (space: ISpace) => void; + onSelectPage: (page: Partial, space: ISpace) => void; +}; + +export function SpaceRow({ + space, + limit, + selectedId, + excludePageId, + onSelectSpace, + onSelectPage, +}: SpaceRowProps) { + const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + + const writable = + !!space.membership?.role && space.membership.role !== SpaceRole.READER; + const isSelected = space.id === selectedId; + + const rowClasses = [ + classes.spaceRow, + isSelected && classes.selected, + !writable && classes.disabled, + ] + .filter(Boolean) + .join(" "); + + const rowContent = ( +
writable && onSelectSpace(space)} + > + {writable ? ( +
{ + e.stopPropagation(); + setExpanded(!expanded); + }} + > + +
+ ) : ( +
+ )} + + + +
{space.name}
+ + {!writable && ( + + )} +
+ ); + + return ( + <> + {writable ? ( + rowContent + ) : ( + +
{rowContent}
+
+ )} + + {expanded && writable && ( + onSelectPage(page, space)} + /> + )} + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx index e3df3e52b8..5d790329db 100644 --- a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx +++ b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar.tsx @@ -11,53 +11,9 @@ import { useSearchChatsQuery, } from "../queries/ai-chat-query"; import AiChatSidebarItem from "./ai-chat-sidebar-item"; -import type { AiChat } from "../types/ai-chat.types"; +import { groupChatsByAge } from "../utils/group-chats-by-age"; import classes from "../styles/chat-sidebar.module.css"; -type ChatGroup = { key: string; label: string; chats: AiChat[] }; - -function groupChatsByAge( - chats: AiChat[], - t: (key: string) => string, -): ChatGroup[] { - if (chats.length === 0) return []; - - const now = new Date(); - const startOfToday = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate(), - ).getTime(); - const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000; - const startOfLast7 = startOfToday - 7 * 24 * 60 * 60 * 1000; - const startOfLast30 = startOfToday - 30 * 24 * 60 * 60 * 1000; - - const buckets: Record = { - today: { key: "today", label: t("Today"), chats: [] }, - yesterday: { key: "yesterday", label: t("Yesterday"), chats: [] }, - last7: { key: "last7", label: t("Previous 7 days"), chats: [] }, - last30: { key: "last30", label: t("Previous 30 days"), chats: [] }, - older: { key: "older", label: t("Older"), chats: [] }, - }; - - for (const chat of chats) { - const ts = new Date(chat.updatedAt).getTime(); - if (ts >= startOfToday) buckets.today.chats.push(chat); - else if (ts >= startOfYesterday) buckets.yesterday.chats.push(chat); - else if (ts >= startOfLast7) buckets.last7.chats.push(chat); - else if (ts >= startOfLast30) buckets.last30.chats.push(chat); - else buckets.older.chats.push(chat); - } - - return [ - buckets.today, - buckets.yesterday, - buckets.last7, - buckets.last30, - buckets.older, - ].filter((b) => b.chats.length > 0); -} - export default function AiChatSidebar() { const { t } = useTranslation(); const navigate = useNavigate(); diff --git a/apps/client/src/ee/ai-chat/utils/group-chats-by-age.ts b/apps/client/src/ee/ai-chat/utils/group-chats-by-age.ts new file mode 100644 index 0000000000..d2b1904f0a --- /dev/null +++ b/apps/client/src/ee/ai-chat/utils/group-chats-by-age.ts @@ -0,0 +1,45 @@ +import type { AiChat } from "../types/ai-chat.types"; + +export type ChatGroup = { key: string; label: string; chats: AiChat[] }; + +export function groupChatsByAge( + chats: AiChat[], + t: (key: string) => string, +): ChatGroup[] { + if (chats.length === 0) return []; + + const now = new Date(); + const startOfToday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ).getTime(); + const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000; + const startOfLast7 = startOfToday - 7 * 24 * 60 * 60 * 1000; + const startOfLast30 = startOfToday - 30 * 24 * 60 * 60 * 1000; + + const buckets: Record = { + today: { key: "today", label: t("Today"), chats: [] }, + yesterday: { key: "yesterday", label: t("Yesterday"), chats: [] }, + last7: { key: "last7", label: t("Previous 7 days"), chats: [] }, + last30: { key: "last30", label: t("Previous 30 days"), chats: [] }, + older: { key: "older", label: t("Older"), chats: [] }, + }; + + for (const chat of chats) { + const ts = new Date(chat.updatedAt).getTime(); + if (ts >= startOfToday) buckets.today.chats.push(chat); + else if (ts >= startOfYesterday) buckets.yesterday.chats.push(chat); + else if (ts >= startOfLast7) buckets.last7.chats.push(chat); + else if (ts >= startOfLast30) buckets.last30.chats.push(chat); + else buckets.older.chats.push(chat); + } + + return [ + buckets.today, + buckets.yesterday, + buckets.last7, + buckets.last30, + buckets.older, + ].filter((b) => b.chats.length > 0); +} diff --git a/apps/client/src/ee/features.ts b/apps/client/src/ee/features.ts index 4cd802fa36..a9ab8b0d53 100644 --- a/apps/client/src/ee/features.ts +++ b/apps/client/src/ee/features.ts @@ -16,5 +16,6 @@ export const Feature = { AUDIT_LOGS: 'audit:logs', RETENTION: 'retention', SHARING_CONTROLS: 'sharing:controls', + TEMPLATES: 'templates', VIEWER_COMMENTS: 'comment:viewer', } as const; diff --git a/apps/client/src/ee/security/components/allow-member-templates.tsx b/apps/client/src/ee/security/components/allow-member-templates.tsx new file mode 100644 index 0000000000..f547d1644a --- /dev/null +++ b/apps/client/src/ee/security/components/allow-member-templates.tsx @@ -0,0 +1,70 @@ +import { Group, Text, Switch, Tooltip } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { Feature } from "@/ee/features"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts"; + +export default function AllowMemberTemplates() { + const { t } = useTranslation(); + + return ( + +
+ {t("Allow members to create templates")} + + {t( + "Allow non-admin members to create and manage templates in their spaces.", + )} + +
+ + +
+ ); +} + +function AllowMemberTemplatesToggle() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState( + workspace?.settings?.templates?.allowMemberTemplates === true, + ); + const hasSecuritySettings = useHasFeature(Feature.SECURITY_SETTINGS); + const upgradeLabel = useUpgradeLabel(); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ + allowMemberTemplates: value, + }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + + + ); +} diff --git a/apps/client/src/ee/security/pages/security.tsx b/apps/client/src/ee/security/pages/security.tsx index 568e3bec29..254809c740 100644 --- a/apps/client/src/ee/security/pages/security.tsx +++ b/apps/client/src/ee/security/pages/security.tsx @@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next"; import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx"; import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx"; import TrashRetention from "@/ee/security/components/trash-retention.tsx"; + import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; diff --git a/apps/client/src/ee/template/components/create-template-modal.tsx b/apps/client/src/ee/template/components/create-template-modal.tsx new file mode 100644 index 0000000000..00fb7746ed --- /dev/null +++ b/apps/client/src/ee/template/components/create-template-modal.tsx @@ -0,0 +1,118 @@ +import { useState } from "react"; +import { + Modal, + TextInput, + Select, + Button, + Stack, + Group, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useCreateTemplateMutation } from "../queries/template-query"; +import { useGetSpacesQuery } from "@/features/space/queries/space-query"; +import useUserRole from "@/hooks/use-user-role"; + +type CreateTemplateModalProps = { + opened: boolean; + onClose: () => void; +}; + +export default function CreateTemplateModal({ + opened, + onClose, +}: CreateTemplateModalProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { isAdmin: isWorkspaceAdmin } = useUserRole(); + const createMutation = useCreateTemplateMutation(); + const { data: spaces } = useGetSpacesQuery({ limit: 100 }); + + const [title, setTitle] = useState(""); + const [spaceId, setSpaceId] = useState(null); + + const scopeOptions = [ + ...(isWorkspaceAdmin + ? [ + { group: t("Workspace"), items: [{ value: "", label: t("Global") }] }, + ] + : []), + ...(spaces?.items?.length + ? [ + { + group: t("Spaces"), + items: spaces.items.map((s) => ({ value: s.id, label: s.name })), + }, + ] + : []), + ]; + + const handleCreate = async () => { + if (!title.trim()) return; + + try { + const result = await createMutation.mutateAsync({ + title: title.trim(), + spaceId: spaceId || undefined, + }); + + handleClose(); + navigate(`/templates/${result.id}`); + } catch { + // error notification handled by mutation's onError + } + }; + + const handleClose = () => { + setTitle(""); + setSpaceId(null); + onClose(); + }; + + return ( + + + setTitle(e.currentTarget.value)} + data-autofocus + onKeyDown={(e) => { + if (e.key === "Enter" && title.trim() && !createMutation.isPending) { + handleCreate(); + } + }} + /> + + + setDraftSpaceId(val || null) + } + searchable + size="sm" + comboboxProps={{ withinPortal: false }} + /> + + + + + + + + + + +
+ + +
+
+ + handleIconChange(emoji.native) + } + icon={ + icon ? ( + {icon} + ) : ( + + ) + } + removeEmojiAction={() => + handleIconChange(null) + } + readOnly={false} + actionIconProps={icon ? { size: "3rem", variant: "transparent" } : undefined} + /> +
+ + handleTitleChange(e.currentTarget.value) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + editor?.commands.focus("start"); + } + }} + /> + {existingTemplate && ( + + )} +
+ +
+ + + ); +} diff --git a/apps/client/src/ee/template/pages/template-list.tsx b/apps/client/src/ee/template/pages/template-list.tsx new file mode 100644 index 0000000000..3491403ed2 --- /dev/null +++ b/apps/client/src/ee/template/pages/template-list.tsx @@ -0,0 +1,213 @@ +import { useState } from "react"; +import { + Container, + Title, + Group, + Button, + SimpleGrid, + Select, + Text, + Center, + Skeleton, + Card, +} from "@mantine/core"; +import { modals } from "@mantine/modals"; +import { IconPlus } from "@tabler/icons-react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useDisclosure } from "@mantine/hooks"; +import { getAppName } from "@/lib/config"; +import { + useGetTemplatesQuery, + useDeleteTemplateMutation, +} from "@/ee/template/queries/template-query"; +import TemplateCard from "@/ee/template/components/template-card"; +import { ITemplate } from "@/ee/template/types/template.types"; +import { useGetSpacesQuery } from "@/features/space/queries/space-query"; +import UseTemplateModal from "@/ee/template/components/use-template-modal"; +import TemplatePreviewModal from "@/ee/template/components/template-preview-modal"; +import useUserRole from "@/hooks/use-user-role"; +import CreateTemplateModal from "@/ee/template/components/create-template-modal"; + +export default function TemplateList() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { isAdmin: isWorkspaceAdmin } = useUserRole(); + const [spaceFilter, setSpaceFilter] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState( + null, + ); + const [useModalOpened, { open: openUseModal, close: closeUseModal }] = + useDisclosure(false); + const [previewOpened, { open: openPreview, close: closePreview }] = + useDisclosure(false); + const [createModalOpened, { open: openCreateModal, close: closeCreateModal }] = + useDisclosure(false); + + const { + data, + isLoading, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = useGetTemplatesQuery({ + spaceId: spaceFilter || undefined, + }); + + const templates = data?.pages.flatMap((p) => p.items) ?? []; + + const { data: spaces } = useGetSpacesQuery({ limit: 100 }); + const deleteTemplateMutation = useDeleteTemplateMutation(); + + const spaceOptions = [ + { value: "", label: t("All templates") }, + ...(spaces?.items?.map((s) => ({ value: s.id, label: s.name })) || []), + ]; + + const spaceNameMap = new Map( + spaces?.items?.map((s) => [s.id, s.name]) || [], + ); + + const handlePreview = (template: ITemplate) => { + setSelectedTemplate(template); + openPreview(); + }; + + const handleUse = (template: ITemplate) => { + setSelectedTemplate(template); + closePreview(); + openUseModal(); + }; + + const handleEdit = (template: ITemplate) => { + navigate(`/templates/${template.id}`); + }; + + const handleDelete = (template: ITemplate) => { + modals.openConfirmModal({ + title: t("Are you sure you want to delete this template?"), + centered: true, + labels: { confirm: t("Delete"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => deleteTemplateMutation.mutate(template.id), + }); + }; + + return ( + <> + + + {t("Templates")} - {getAppName()} + + + + + + {t("Templates")} + {isWorkspaceAdmin && ( + + )} + + + +