Compare commits

...

11 Commits

Author SHA1 Message Date
Aaryan Khandelwal 801a40acd5 fix: web app lint issues 2025-08-28 17:40:14 +05:30
Nikhil ddeabeeeb1 [WEB-4720] fix: mongo connection class to initialize mongo db #7652 2025-08-28 13:57:16 +05:30
sriram veeraghanta 0e6fbaee3a [WEB-4790] fix: moved typescript parser to the base eslint config (#7658)
* fix: moved typescript parser to the base eslint config

* fix: eslint warning

* fix: type config setting

* fix: convert live eslint to cjs
2025-08-27 21:03:20 +05:30
Anmol Singh Bhatia cfe710d492 [WEB-4779] chore: app sidebar wrapper component for consistency and reusability (#7655)
* chore: app sidebar ui enhancements and code refactor

* chore: code refactor

* chore: code refactor
2025-08-27 20:46:43 +05:30
Aaron 4a3c172992 feat(repo): add diff checks in CI flows (#7657) 2025-08-27 16:31:23 +05:30
Vipin Chaudhary 5d1ad8a183 [WIKI-550] fix: emoji modal for touch device (#7651)
* fix: emoji modal for touch device

* refactor: editor from props

* fix : update is touch device plugin
2025-08-27 00:39:47 +05:30
Vipin Chaudhary f95a07d8c8 [WIKI-436] fix: check for code in alignment #7649 2025-08-27 00:39:26 +05:30
Bavisetti Narayan 0af75897f5 [WEB-4780] chore: changed the html validation (#7648)
* chore: changed the html validation

* chore: added requirements for nh3

* chore: removed the json validations
2025-08-27 00:38:25 +05:30
Vamsi Krishna 3602ff6930 [WEB-4781] fix: add peek view get to store #7654 2025-08-26 18:24:08 +05:30
Prateek Shourya 6d1275d58c [WEB-4761] fix: resolve circular import between IssueService and local db (#7653)
- Move local db import to dynamic import to break circular dependency
2025-08-26 16:48:32 +05:30
Vipin Chaudhary cf7b288f93 [WIKI-345] regression: update block menu options (#7647)
* chore: update flagged and disabled extension

* chore :build fix
2025-08-26 14:18:26 +05:30
97 changed files with 453 additions and 645 deletions
+21 -12
View File
@@ -35,6 +35,10 @@ on:
- preview
- canary
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TARGET_BRANCH: ${{ github.ref_name }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
@@ -268,15 +272,14 @@ jobs:
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
name: Build-Push AIO Docker Image
runs-on: ubuntu-22.04
needs: [
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy
]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -285,7 +288,7 @@ jobs:
id: prepare_aio_assets
run: |
cd deployments/aio/community
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
else
@@ -324,7 +327,14 @@ jobs:
upload_build_assets:
name: Upload Build Assets
runs-on: ubuntu-22.04
needs: [branch_build_setup, branch_build_push_admin, branch_build_push_web, branch_build_push_space, branch_build_push_live, branch_build_push_api, branch_build_push_proxy]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -397,4 +407,3 @@ jobs:
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
${{ github.workspace }}/deployments/cli/community/variables.env
${{ github.workspace }}/deployments/swarm/community/swarm.sh
@@ -3,11 +3,21 @@ name: Build and lint API
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
types: ["opened", "synchronize", "ready_for_review", "review_requested", "reopened"]
branches:
- "preview"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
paths:
- "apps/api/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-api:
name: Lint API
@@ -3,21 +3,18 @@ name: Build and lint web apps
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
branches:
- "preview"
types:
[
"opened",
"synchronize",
"ready_for_review",
"review_requested",
"reopened",
]
paths:
- "**.tsx?"
- "**.jsx?"
- "**.css"
- "**.json"
- "!apps/api/**"
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-lint:
@@ -27,11 +24,15 @@ jobs:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v4
@@ -44,11 +45,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint web apps
run: pnpm run check:lint
- name: Lint Affected
run: pnpm turbo run check:lint --affected
- name: Check format
run: pnpm run check:format
- name: Check Affected format
run: pnpm turbo run check:format --affected
- name: Build apps
run: pnpm run build
- name: Build Affected
run: pnpm turbo run build --affected
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
// components
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(() => {
const InstanceEmailPage: React.FC = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -29,7 +29,7 @@ const InstanceEmailPage = observer(() => {
message: "Email feature has been disabled",
type: TOAST_TYPE.SUCCESS,
});
} catch (error) {
} catch (_error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
@@ -25,9 +25,8 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableMagicLogin))}
onChange={() => {
Boolean(parseInt(enableMagicLogin)) === true
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGithubConfig))}
onChange={() => {
Boolean(parseInt(enableGithubConfig)) === true
? updateConfig("IS_GITHUB_ENABLED", "0")
: updateConfig("IS_GITHUB_ENABLED", "1");
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
Boolean(parseInt(enableGoogleConfig)) === true
? updateConfig("IS_GOOGLE_ENABLED", "0")
: updateConfig("IS_GOOGLE_ENABLED", "1");
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
}}
size="sm"
disabled={disabled}
@@ -25,9 +25,8 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableEmailPassword))}
onChange={() => {
Boolean(parseInt(enableEmailPassword)) === true
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
}}
size="sm"
disabled={disabled}
+1 -1
View File
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
});
});
await this.instanceService.disableEmail();
} catch (error) {
} catch (_error) {
console.error("Error disabling the email");
this.instanceConfigurations = instanceConfigurations;
}
+12 -9
View File
@@ -24,7 +24,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -89,20 +88,24 @@ class IssueSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid HTML passed")
# Validate description content for security
if data.get("description"):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if data.get("description_html"):
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if data.get("description_binary"):
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if data.get("assignees", []):
+7 -17
View File
@@ -12,7 +12,6 @@ from plane.db.models import (
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
)
from .base import BaseSerializer
@@ -200,27 +199,18 @@ class ProjectSerializer(BaseSerializer):
)
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+12 -9
View File
@@ -23,7 +23,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
from plane.app.permissions import ROLE
@@ -76,20 +75,24 @@ class DraftIssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
+12 -9
View File
@@ -43,7 +43,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -128,20 +127,24 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
+3 -14
View File
@@ -7,7 +7,6 @@ from .base import BaseSerializer
from plane.utils.content_validator import (
validate_binary_data,
validate_html_content,
validate_json_content,
)
from plane.db.models import (
Page,
@@ -229,23 +228,13 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
return value
# Use the validation function from utils
is_valid, error_message = validate_html_content(value)
is_valid, error_message, sanitized_html = validate_html_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
# Return sanitized HTML if available, otherwise return original
return sanitized_html if sanitized_html is not None else value
def validate_description(self, value):
"""Validate the JSON description"""
if not value:
return value
# Use the validation function from utils
is_valid, error_message = validate_json_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
def update(self, instance, validated_data):
"""Update the page instance with validated data"""
+10 -20
View File
@@ -15,7 +15,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -65,27 +64,18 @@ class ProjectSerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
str(data["description_html"])
)
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+12 -9
View File
@@ -26,7 +26,6 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.url import contains_url
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -319,20 +318,24 @@ class StickySerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
return data
+4
View File
@@ -465,3 +465,7 @@ if ENABLE_DRF_SPECTACULAR:
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
INSTALLED_APPS.append("drf_spectacular")
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
# MongoDB Settings
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
+3
View File
@@ -118,4 +118,7 @@ class MongoConnection:
Returns:
bool: True if MongoDB is configured and connected, False otherwise
"""
if cls._client is None:
cls._instance = cls()
return cls._client is not None and cls._db is not None
+10 -9
View File
@@ -30,7 +30,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -290,20 +289,22 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
return data
+11 -270
View File
@@ -1,36 +1,11 @@
# Python imports
import base64
import json
import re
import nh3
from plane.utils.exception_logger import log_exception
# Maximum allowed size for binary data (10MB)
MAX_SIZE = 10 * 1024 * 1024
# Maximum recursion depth to prevent stack overflow
MAX_RECURSION_DEPTH = 20
# Dangerous text patterns that could indicate XSS or script injection
DANGEROUS_TEXT_PATTERNS = [
r"<script[^>]*>.*?</script>",
r"javascript\s*:",
r"data\s*:\s*text/html",
r"eval\s*\(",
r"document\s*\.",
r"window\s*\.",
r"location\s*\.",
]
# Dangerous attribute patterns for HTML attributes
DANGEROUS_ATTR_PATTERNS = [
r"javascript\s*:",
r"data\s*:\s*text/html",
r"eval\s*\(",
r"alert\s*\(",
r"document\s*\.",
r"window\s*\.",
]
# Suspicious patterns for binary data content
SUSPICIOUS_BINARY_PATTERNS = [
"<html",
@@ -41,70 +16,6 @@ SUSPICIOUS_BINARY_PATTERNS = [
"<iframe",
]
# Malicious HTML patterns for content validation
MALICIOUS_HTML_PATTERNS = [
# Script tags with any content
r"<script[^>]*>",
r"</script>",
# JavaScript URLs in various attributes
r'(?:href|src|action)\s*=\s*["\']?\s*javascript:',
# Data URLs with text/html (potential XSS)
r'(?:href|src|action)\s*=\s*["\']?\s*data:text/html',
# Dangerous event handlers with JavaScript-like content
r'on(?:load|error|click|focus|blur|change|submit|reset|select|resize|scroll|unload|beforeunload|hashchange|popstate|storage|message|offline|online)\s*=\s*["\']?[^"\']*(?:javascript|alert|eval|document\.|window\.|location\.|history\.)[^"\']*["\']?',
# Object and embed tags that could load external content
r"<(?:object|embed)[^>]*(?:data|src)\s*=",
# Base tag that could change relative URL resolution
r"<base[^>]*href\s*=",
# Dangerous iframe sources
r'<iframe[^>]*src\s*=\s*["\']?(?:javascript:|data:text/html)',
# Meta refresh redirects
r'<meta[^>]*http-equiv\s*=\s*["\']?refresh["\']?',
# Link tags - simplified patterns
r'<link[^>]*rel\s*=\s*["\']?stylesheet["\']?',
r'<link[^>]*href\s*=\s*["\']?https?://',
r'<link[^>]*href\s*=\s*["\']?//',
r'<link[^>]*href\s*=\s*["\']?(?:data:|javascript:)',
# Style tags with external imports
r"<style[^>]*>.*?@import.*?(?:https?://|//)",
# Link tags with dangerous rel types
r'<link[^>]*rel\s*=\s*["\']?(?:import|preload|prefetch|dns-prefetch|preconnect)["\']?',
# Forms with action attributes
r"<form[^>]*action\s*=",
]
# Dangerous JavaScript patterns for event handlers
DANGEROUS_JS_PATTERNS = [
r"alert\s*\(",
r"eval\s*\(",
r"document\s*\.",
r"window\s*\.",
r"location\s*\.",
r"fetch\s*\(",
r"XMLHttpRequest",
r"innerHTML\s*=",
r"outerHTML\s*=",
r"document\.write",
r"script\s*>",
]
# HTML self-closing tags that don't need closing tags
SELF_CLOSING_TAGS = {
"img",
"br",
"hr",
"input",
"meta",
"link",
"area",
"base",
"col",
"embed",
"source",
"track",
"wbr",
}
def validate_binary_data(data):
"""
@@ -149,191 +60,21 @@ def validate_binary_data(data):
return True, None
def validate_html_content(html_content):
def validate_html_content(html_content: str):
"""
Validate that HTML content is safe and doesn't contain malicious patterns.
Args:
html_content (str): The HTML content to validate
Returns:
tuple: (is_valid: bool, error_message: str or None)
Sanitize HTML content using nh3.
Returns a tuple: (is_valid, error_message, clean_html)
"""
if not html_content:
return True, None # Empty is OK
return True, None, None
# Size check - 10MB limit (consistent with binary validation)
if len(html_content.encode("utf-8")) > MAX_SIZE:
return False, "HTML content exceeds maximum size limit (10MB)"
# Check for specific malicious patterns (simplified and more reliable)
for pattern in MALICIOUS_HTML_PATTERNS:
if re.search(pattern, html_content, re.IGNORECASE | re.DOTALL):
return (
False,
f"HTML content contains potentially malicious patterns: {pattern}",
)
# Additional check for inline event handlers that contain suspicious content
# This is more permissive - only blocks if the event handler contains actual dangerous code
event_handler_pattern = r'on\w+\s*=\s*["\']([^"\']*)["\']'
event_matches = re.findall(event_handler_pattern, html_content, re.IGNORECASE)
for handler_content in event_matches:
for js_pattern in DANGEROUS_JS_PATTERNS:
if re.search(js_pattern, handler_content, re.IGNORECASE):
return (
False,
f"HTML content contains dangerous JavaScript in event handler: {handler_content[:100]}",
)
return True, None
def validate_json_content(json_content):
"""
Validate that JSON content is safe and doesn't contain malicious patterns.
Args:
json_content (dict): The JSON content to validate
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
if not json_content:
return True, None # Empty is OK
return False, "HTML content exceeds maximum size limit (10MB)", None
try:
# Size check - 10MB limit (consistent with other validations)
json_str = json.dumps(json_content)
if len(json_str.encode("utf-8")) > MAX_SIZE:
return False, "JSON content exceeds maximum size limit (10MB)"
# Basic structure validation for page description JSON
if isinstance(json_content, dict):
# Check for expected page description structure
# This is based on ProseMirror/Tiptap JSON structure
if "type" in json_content and json_content.get("type") == "doc":
# Valid document structure
if "content" in json_content and isinstance(
json_content["content"], list
):
# Recursively check content for suspicious patterns
is_valid, error_msg = _validate_json_content_array(
json_content["content"]
)
if not is_valid:
return False, error_msg
elif "type" not in json_content and "content" not in json_content:
# Allow other JSON structures but validate for suspicious content
is_valid, error_msg = _validate_json_content_recursive(json_content)
if not is_valid:
return False, error_msg
else:
return False, "JSON description must be a valid object"
except (TypeError, ValueError) as e:
return False, "Invalid JSON structure"
clean_html = nh3.clean(html_content)
return True, None, clean_html
except Exception as e:
return False, "Failed to validate JSON content"
return True, None
def _validate_json_content_array(content, depth=0):
"""
Validate JSON content array for suspicious patterns.
Args:
content (list): Array of content nodes to validate
depth (int): Current recursion depth (default: 0)
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
# Check recursion depth to prevent stack overflow
if depth > MAX_RECURSION_DEPTH:
return False, f"Maximum recursion depth ({MAX_RECURSION_DEPTH}) exceeded"
if not isinstance(content, list):
return True, None
for node in content:
if isinstance(node, dict):
# Check text content for suspicious patterns (more targeted)
if node.get("type") == "text" and "text" in node:
text_content = node["text"]
for pattern in DANGEROUS_TEXT_PATTERNS:
if re.search(pattern, text_content, re.IGNORECASE):
return (
False,
"JSON content contains suspicious script patterns in text",
)
# Check attributes for suspicious content (more targeted)
if "attrs" in node and isinstance(node["attrs"], dict):
for attr_name, attr_value in node["attrs"].items():
if isinstance(attr_value, str):
# Only check specific attributes that could be dangerous
if attr_name.lower() in [
"href",
"src",
"action",
"onclick",
"onload",
"onerror",
]:
for pattern in DANGEROUS_ATTR_PATTERNS:
if re.search(pattern, attr_value, re.IGNORECASE):
return (
False,
f"JSON content contains dangerous pattern in {attr_name} attribute",
)
# Recursively check nested content
if "content" in node and isinstance(node["content"], list):
is_valid, error_msg = _validate_json_content_array(
node["content"], depth + 1
)
if not is_valid:
return False, error_msg
return True, None
def _validate_json_content_recursive(obj, depth=0):
"""
Recursively validate JSON object for suspicious content.
Args:
obj: JSON object (dict, list, or primitive) to validate
depth (int): Current recursion depth (default: 0)
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
# Check recursion depth to prevent stack overflow
if depth > MAX_RECURSION_DEPTH:
return False, f"Maximum recursion depth ({MAX_RECURSION_DEPTH}) exceeded"
if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, str):
# Check for dangerous patterns using module constants
for pattern in DANGEROUS_TEXT_PATTERNS:
if re.search(pattern, value, re.IGNORECASE):
return (
False,
"JSON content contains suspicious script patterns",
)
elif isinstance(value, (dict, list)):
is_valid, error_msg = _validate_json_content_recursive(value, depth + 1)
if not is_valid:
return False, error_msg
elif isinstance(obj, list):
for item in obj:
is_valid, error_msg = _validate_json_content_recursive(item, depth + 1)
if not is_valid:
return False, error_msg
return True, None
log_exception(e)
return False, "Failed to sanitize HTML", None
+2
View File
@@ -69,3 +69,5 @@ opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
# OpenAPI Specification
drf-spectacular==0.28.0
# html sanitizer
nh3==0.2.18
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/server.js"],
};
-5
View File
@@ -1,5 +0,0 @@
{
"root": true,
"extends": ["@plane/eslint-config/server.js"],
"parser": "@typescript-eslint/parser"
}
+4 -7
View File
@@ -1,20 +1,17 @@
// Third-party libraries
import { Redis } from "ioredis";
// Hocuspocus extensions and core
import { Database } from "@hocuspocus/extension-database";
import { Extension } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
import { Extension } from "@hocuspocus/server";
import { Redis } from "ioredis";
// core helpers and utilities
import { manualLogger } from "@/core/helpers/logger.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
// core libraries
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
// plane live libraries
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
// types
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
export const getExtensions: () => Promise<Extension[]> = async () => {
const extensions: Extension[] = [
+5 -5
View File
@@ -1,12 +1,12 @@
import { Server } from "@hocuspocus/server";
import { v4 as uuidv4 } from "uuid";
// lib
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
// editor types
import { TUserDetails } from "@plane/editor";
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
// lib
import { handleAuthentication } from "@/core/lib/authentication.js";
// types
import { type HocusPocusServerContext } from "@/core/types/common.js";
+2 -2
View File
@@ -1,7 +1,7 @@
// services
import { UserService } from "@/core/services/user.service.js";
// core helpers
import { manualLogger } from "@/core/helpers/logger.js";
// services
import { UserService } from "@/core/services/user.service.js";
const userService = new UserService();
+2 -2
View File
@@ -1,13 +1,13 @@
import compression from "compression";
import cors from "cors";
import expressWs from "express-ws";
import express, { Request, Response } from "express";
import expressWs from "express-ws";
import helmet from "helmet";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
+10 -1
View File
@@ -1,4 +1,13 @@
.next/*
out/*
public/*
core/local-db/worker/wa-sqlite/src/*
core/local-db/worker/wa-sqlite/src/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
@@ -1,36 +1,24 @@
import { FC, useEffect, useRef } from "react";
import { FC } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
// plane helpers
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
import { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper";
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
import { SidebarProjectsList } from "@/components/workspace/sidebar/projects-list";
import { SidebarQuickActions } from "@/components/workspace/sidebar/quick-actions";
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useFavorite } from "@/hooks/store/use-favorite";
import { useUserPermissions } from "@/hooks/store/user";
import { useAppRail } from "@/hooks/use-app-rail";
import useSize from "@/hooks/use-window-size";
// plane web components
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
export const AppSidebar: FC = observer(() => {
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
const { groupedFavorites } = useFavorite();
const windowSize = useSize();
// refs
const ref = useRef<HTMLDivElement>(null);
// derived values
const canPerformWorkspaceMemberActions = allowPermissions(
@@ -38,55 +26,17 @@ export const AppSidebar: FC = observer(() => {
EUserPermissionsLevel.WORKSPACE
);
useOutsideClickDetector(ref, () => {
if (sidebarCollapsed === false) {
if (window.innerWidth < 768) {
toggleSidebar();
}
}
});
useEffect(() => {
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize]);
const isFavoriteEmpty = isEmpty(groupedFavorites);
return (
<>
<div className="flex flex-col gap-3 px-3">
{/* Workspace switcher and settings */}
{!shouldRenderAppRail && <SidebarDropdown />}
{isAppRailEnabled && (
<div className="flex items-center justify-between gap-2">
<span className="text-md text-custom-text-200 font-medium pt-1">Projects</span>
<div className="flex items-center gap-2">
<AppSidebarToggleButton />
</div>
</div>
)}
{/* Quick actions */}
<SidebarQuickActions />
</div>
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
<SidebarMenuItems />
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
<WorkspaceEditionBadge />
<div className="flex items-center gap-2">
{!shouldRenderAppRail && <HelpMenu />}
{!isAppRailEnabled && <AppSidebarToggleButton />}
</div>
</div>
</>
<SidebarWrapper title="Projects" quickActions={<SidebarQuickActions />}>
<SidebarMenuItems />
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</SidebarWrapper>
);
});
@@ -80,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const payload: any = { ...data };
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
updateIssue && (await updateIssue(issue.project_id, issue.id, payload));
await updateIssue?.(issue.project_id, issue.id, payload);
};
const isAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
@@ -112,8 +112,8 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const issueDetails = getIssueById(issueId);
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
const handleIssuePeekOverview = (e: any) => {
e.stopPropagation(true);
const handleIssuePeekOverview = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.stopPropagation();
e.preventDefault();
handleRedirection(workspaceSlug, issueDetails, isMobile);
};
@@ -7,7 +7,7 @@ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-sc
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes, TIssue } from "@plane/types";
//constants
//hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@@ -42,7 +42,7 @@ export type KanbanStoreType =
export interface IBaseKanBanLayout {
QuickActions: FC<IQuickActionProps>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
isCompletedCycle?: boolean;
viewId?: string | undefined;
@@ -238,7 +238,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
},
})
);
}, [cardRef?.current, issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
}, [issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
if (!issue) return null;
@@ -1,7 +1,6 @@
import { MutableRefObject } from "react";
import { observer } from "mobx-react";
// i18n
import { useTranslation } from "@plane/i18n";
// plane imports
import {
GroupByColumnTypes,
IGroupByColumn,
@@ -14,7 +13,6 @@ import {
TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types";
// constants
import { ContentWrapper } from "@plane/ui";
// components
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
@@ -92,8 +90,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
subGroupIndex = 0,
isEpic = false,
} = props;
// i18n
const { t } = useTranslation();
// store hooks
const storeType = useIssueStoreType();
const issueKanBanView = useKanbanView();
@@ -78,7 +78,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
title: "Success!",
message: "Work items added to the cycle successfully.",
});
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -7,7 +7,7 @@ import { TSelectionHelper } from "@/hooks/use-multiple-select";
import { IssueBlockRoot } from "./block-root";
import { TRenderQuickActions } from "./list-view-types";
interface Props {
type Props = {
issueIds: TGroupedIssues | any;
issuesMap: TIssueMap;
groupId: string;
@@ -20,7 +20,7 @@ interface Props {
canDropOverIssue: boolean;
selectionHelpers: TSelectionHelper;
isEpic?: boolean;
}
};
export const IssueBlocksList: FC<Props> = (props) => {
const {
@@ -81,7 +81,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
title: "Success!",
message: "Work items added to the cycle successfully.",
});
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -192,7 +192,9 @@ export const ListGroup = observer((props: Props) => {
const sourceGroupId = source?.data?.groupId as string | undefined;
const currentGroupId = group.id;
sourceGroupId && handleWorkFlowState(sourceGroupId, currentGroupId);
if (sourceGroupId) {
handleWorkFlowState(sourceGroupId, currentGroupId);
}
const sourceIndex = getGroupIndex(sourceGroupId);
const currentIndex = getGroupIndex(currentGroupId);
@@ -230,15 +232,7 @@ export const ListGroup = observer((props: Props) => {
},
})
);
}, [
groupRef?.current,
group,
orderBy,
getGroupIndex,
setDragColumnOrientation,
setIsDraggingOverColumn,
isWorkflowDropDisabled,
]);
}, [group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn, isWorkflowDropDisabled]);
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
@@ -3,7 +3,7 @@
import { FC, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Link2, MoveDiagonal, MoveRight } from "lucide-react";
import { Link2, type LucideIcon, MoveDiagonal, MoveRight } from "lucide-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@@ -12,6 +12,7 @@ import {
CenterPanelIcon,
CustomSelect,
FullScreenPanelIcon,
type ISvgIcons,
SidePanelIcon,
TOAST_TYPE,
Tooltip,
@@ -33,7 +34,7 @@ import { NameDescriptionUpdateStatus } from "../issue-update-status";
export type TPeekModes = "side-peek" | "modal" | "full-screen";
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
const PEEK_OPTIONS: { key: TPeekModes; icon: LucideIcon | React.FC<ISvgIcons>; i18n_title: string }[] = [
{
key: "side-peek",
icon: SidePanelIcon,
@@ -4,7 +4,7 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { PROJECT_SETTINGS_TRACKER_ELEMENTS, PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import type { IIssueLabel } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
@@ -154,7 +154,7 @@ export const LabelDndHOC = observer((props: Props) => {
},
})
);
}, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
}, [dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
const isMakeChild = instruction == "make-child";
@@ -110,7 +110,7 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
await fetchModuleDetails(workspaceSlug, projectId, moduleId);
}
setLoader(false);
} catch (error) {
} catch {
setLoader(false);
setPlotType(moduleId, plotType);
}
@@ -16,7 +16,7 @@ import {
} from "@plane/constants";
// plane types
import { useTranslation } from "@plane/i18n";
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
import { ILinkDetails, IModule, ModuleLink, TModuleStatus } from "@plane/types";
// plane ui
import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
// components
@@ -270,7 +270,7 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
</span>
}
value={value}
onChange={(value: any) => {
onChange={(value: TModuleStatus) => {
submitChanges({ status: value });
}}
disabled={!isEditingAllowed || isArchived}
@@ -27,7 +27,7 @@ type Props = {
};
export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
const { invitations, totalSteps, stepChange, finishOnboarding } = props;
const { invitations, totalSteps: _totalSteps, stepChange, finishOnboarding } = props;
// states
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
// store hooks
@@ -262,7 +262,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
});
export const InviteMembers: React.FC<Props> = (props) => {
const { finishOnboarding, totalSteps, workspace } = props;
const { finishOnboarding, totalSteps: _totalSteps, workspace } = props;
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
@@ -21,7 +21,7 @@ import { Listbox } from "@headlessui/react";
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { EOnboardingSteps, IWorkspace } from "@plane/types";
import { EOnboardingSteps } from "@plane/types";
// ui
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants
@@ -30,7 +30,6 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUser, useUserProfile } from "@/hooks/store/user";
// services
import { WorkspaceService } from "@/plane-web/services";
// components
@@ -1,15 +1,15 @@
"use client";
// icons
import { FileText, Layers } from "lucide-react";
import { ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
// types
import { TTourSteps } from "./root";
import { FileText, Layers, type LucideIcon } from "lucide-react";
// plane imports
import { ContrastIcon, DiceIcon, type ISvgIcons, LayersIcon } from "@plane/ui";
// local imports
import type { TTourSteps } from "./root";
const sidebarOptions: {
key: TTourSteps;
label: string;
Icon: any;
Icon: LucideIcon | React.FC<ISvgIcons>;
}[] = [
{
key: "work-items",
@@ -27,7 +27,7 @@ import { PageEditorToolbarRoot } from "./toolbar";
export type TPageRootHandlers = {
create: (payload: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
fetchDescriptionBinary: () => Promise<any>;
fetchDescriptionBinary: () => Promise<ArrayBuffer>;
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
getRedirectionLink: (pageId: string) => string;
updateDescription: (document: TDocumentPayload) => Promise<void>;
@@ -77,7 +77,7 @@ export const PageForm: React.FC<Props> = (props) => {
</>
</span>
}
onChange={(val: any) => {
onChange={(val) => {
let logoValue = {};
if (val?.type === "emoji")
@@ -56,7 +56,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
<>
{userProfileActivity ? (
<ul role="list">
{userProfileActivity.results.map((activityItem: any) => {
{userProfileActivity.results.map((activityItem) => {
if (activityItem.field === "comment")
return (
<div key={activityItem.id} className="mt-2">
@@ -98,7 +98,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
editable={false}
id={activityItem.id}
initialValue={
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
(activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value) ?? ""
}
containerClassName="text-xs bg-custom-background-100"
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
@@ -155,7 +155,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
) : (
<Link
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
href={`/${activityItem.workspace_detail?.slug}/profile/${activityItem.actor_detail.id}`}
className="inline"
>
<span className="text-gray font-medium">
@@ -0,0 +1,72 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useAppRail } from "@/hooks/use-app-rail";
import useSize from "@/hooks/use-window-size";
// plane web components
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
type TSidebarWrapperProps = {
title: string;
children: React.ReactNode;
quickActions?: React.ReactNode;
};
export const SidebarWrapper: FC<TSidebarWrapperProps> = observer((props) => {
const { children, title, quickActions } = props;
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
const windowSize = useSize();
// refs
const ref = useRef<HTMLDivElement>(null);
useOutsideClickDetector(ref, () => {
if (sidebarCollapsed === false && window.innerWidth < 768) {
toggleSidebar();
}
});
useEffect(() => {
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize]);
return (
<div ref={ref} className="flex flex-col h-full w-full">
<div className="flex flex-col gap-3 px-3">
{/* Workspace switcher and settings */}
{!shouldRenderAppRail && <SidebarDropdown />}
{isAppRailEnabled && (
<div className="flex items-center justify-between gap-2">
<span className="text-md text-custom-text-200 font-medium pt-1">{title}</span>
<div className="flex items-center gap-2">
<AppSidebarToggleButton />
</div>
</div>
)}
{/* Quick actions */}
{quickActions}
</div>
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
{children}
</div>
{/* Help Section */}
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
<WorkspaceEditionBadge />
<div className="flex items-center gap-2">
{!shouldRenderAppRail && <HelpMenu />}
{!isAppRailEnabled && <AppSidebarToggleButton />}
</div>
</div>
</div>
);
});
@@ -1,5 +1,4 @@
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import orderBy from "lodash/orderBy";
import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types";
export type TargetData = {
@@ -66,7 +66,7 @@ export const UserMenuRoot = observer((props: Props) => {
return (
<Menu as="div" className="relative flex-shrink-0">
{({ open, close }: { open: boolean; close: () => void }) => {
{({ open }: { open: boolean; close: () => void }) => {
// Update local state directly
if (isUserMenuOpen !== open) {
setIsUserMenuOpen(open);
+3 -3
View File
@@ -17,7 +17,7 @@ import {
List,
ListOrdered,
ListTodo,
LucideIcon,
type LucideIcon,
Strikethrough,
Table,
TextQuote,
@@ -25,7 +25,7 @@ import {
} from "lucide-react";
// plane imports
import type { TCommandExtraProps, TEditorCommands, TEditorFontStyle } from "@plane/editor";
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
import { type ISvgIcons, MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
import { convertRemToPixel } from "@plane/utils";
type TEditorTypes = "lite" | "document" | "sticky";
@@ -191,7 +191,7 @@ export const TOOLBAR_ITEMS: {
export const EDITOR_FONT_STYLES: {
key: TEditorFontStyle;
label: string;
icon: any;
icon: LucideIcon | React.FC<ISvgIcons>;
}[] = [
{
key: "sans-serif",
+1 -1
View File
@@ -90,7 +90,7 @@ export const useGroupIssuesDragNDrop = (
delete data[moduleKey];
}
updateIssue && updateIssue(projectId, issueId, data).catch(() => setToast(errorToastProps));
updateIssue?.(projectId, issueId, data).catch(() => setToast(errorToastProps));
};
const handleOnDrop = async (source: GroupDropLocation, destination: GroupDropLocation) => {
@@ -18,7 +18,7 @@ export const useIntersectionObserver = (
const observer = new IntersectionObserver(
(entries) => {
if (entries[entries.length - 1].isIntersecting) {
callback && callback();
callback?.();
}
},
{
+4 -4
View File
@@ -1,22 +1,22 @@
import { useState, useEffect, useCallback } from "react";
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
export const getValueFromLocalStorage = (key: string, defaultValue: unknown) => {
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
} catch {
window.localStorage.removeItem(key);
return defaultValue;
}
};
export const setValueIntoLocalStorage = (key: string, value: any) => {
export const setValueIntoLocalStorage = (key: string, value: unknown) => {
if (typeof window === undefined || typeof window === "undefined") return false;
try {
window.localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
} catch {
return false;
}
};
@@ -32,7 +32,7 @@ const useReloadConfirmations = (isActive = true, message?: string, defaultShowAl
// show confirm dialog
const isLeaving = confirm(alertMessage);
if (isLeaving) {
onLeave && onLeave();
onLeave?.();
} else {
event.preventDefault();
event.stopPropagation();
+1 -2
View File
@@ -18,6 +18,7 @@ import { runQuery } from "./utils/query-executor";
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
import { createTables } from "./utils/tables";
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
import type { DBClass } from "./worker/db";
const DB_VERSION = 1.3;
const PAGE_SIZE = 500;
@@ -70,7 +71,6 @@ export class Storage {
};
private initializeWorker = async (workspaceSlug: string) => {
const { DBClass } = await import("./worker/db");
const worker = new Worker(new URL("./worker/db.ts", import.meta.url));
const MyWorker = Comlink.wrap<typeof DBClass>(worker);
@@ -367,7 +367,6 @@ export class Storage {
issueResults = getGroupedIssueResults(issueResults);
}
}
const groupCount = group_by ? Object.keys(issueResults).length : undefined;
// const subGroupCount = sub_group_by ? Object.keys(issueResults[Object.keys(issueResults)[0]]).length : undefined;
const groupingEnd = performance.now();
+15 -12
View File
@@ -1,4 +1,4 @@
// types
// plane imports
import { API_BASE_URL } from "@plane/constants";
import {
EIssueServiceType,
@@ -12,12 +12,8 @@ import {
type TIssuesResponse,
type TIssueSubIssues,
} from "@plane/types";
// helpers
import { getIssuesShouldFallbackToServer } from "@plane/utils";
import { persistence } from "@/local-db/storage.sqlite";
// services
import { addIssuesBulk, deleteIssueFromLocal, updateIssue } from "@/local-db/utils/load-issues";
import { APIService } from "@/services/api.service";
export class IssueService extends APIService {
@@ -85,7 +81,7 @@ export class IssueService extends APIService {
if (getIssuesShouldFallbackToServer(queries) || this.serviceType !== EIssueServiceType.ISSUES) {
return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config);
}
const { persistence } = await import("@/local-db/storage.sqlite");
const response = await persistence.getIssues(workspaceSlug, projectId, queries, config);
return response as TIssuesResponse;
}
@@ -118,9 +114,10 @@ export class IssueService extends APIService {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/`, {
params: queries,
})
.then((response) => {
.then(async (response) => {
// skip issue update when the service type is epic
if (response.data && this.serviceType === EIssueServiceType.ISSUES) {
const { updateIssue } = await import("@/local-db/utils/load-issues");
updateIssue({ ...response.data, is_local_update: 1 });
}
// add is_epic flag when the service type is epic
@@ -138,8 +135,9 @@ export class IssueService extends APIService {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/list/`, {
params: { issues: issueIds.join(",") },
})
.then((response) => {
.then(async (response) => {
if (response?.data && Array.isArray(response?.data) && this.serviceType === EIssueServiceType.ISSUES) {
const { addIssuesBulk } = await import("@/local-db/utils/load-issues");
addIssuesBulk(response.data);
}
return response?.data;
@@ -246,6 +244,7 @@ export class IssueService extends APIService {
async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
if (this.serviceType === EIssueServiceType.ISSUES) {
const { deleteIssueFromLocal } = await import("@/local-db/utils/load-issues");
deleteIssueFromLocal(issuesId);
}
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issuesId}/`)
@@ -354,8 +353,9 @@ export class IssueService extends APIService {
async bulkOperations(workspaceSlug: string, projectId: string, data: TBulkOperationsPayload): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-operation-issues/`, data)
.then((response) => {
.then(async (response) => {
if (this.serviceType === EIssueServiceType.ISSUES) {
const { persistence } = await import("@/local-db/storage.sqlite");
persistence.syncIssues(projectId);
}
return response?.data;
@@ -373,8 +373,9 @@ export class IssueService extends APIService {
}
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data)
.then((response) => {
.then(async (response) => {
if (this.serviceType === EIssueServiceType.ISSUES) {
const { persistence } = await import("@/local-db/storage.sqlite");
persistence.syncIssues(projectId);
}
return response?.data;
@@ -394,8 +395,9 @@ export class IssueService extends APIService {
archived_at: string;
}> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-archive-issues/`, data)
.then((response) => {
.then(async (response) => {
if (this.serviceType === EIssueServiceType.ISSUES) {
const { persistence } = await import("@/local-db/storage.sqlite");
persistence.syncIssues(projectId);
}
return response?.data;
@@ -476,9 +478,10 @@ export class IssueService extends APIService {
return this.get(`/api/workspaces/${workspaceSlug}/work-items/${project_identifier}-${issue_sequence}/`, {
params: queries,
})
.then((response) => {
.then(async (response) => {
// skip issue update when the service type is epic
if (response.data && this.serviceType === EIssueServiceType.ISSUES) {
const { updateIssue } = await import("@/local-db/utils/load-issues");
updateIssue({ ...response.data, is_local_update: 1 });
}
// add is_epic flag when the service type is epic
@@ -95,6 +95,7 @@ export interface IIssueDetail
attachmentDeleteModalId: string | null;
// computed
isAnyModalOpen: boolean;
isPeekOpen: boolean;
// helper actions
getIsIssuePeeked: (issueId: string) => boolean;
// actions
@@ -188,6 +189,7 @@ export abstract class IssueDetail implements IIssueDetail {
lastWidgetAction: observable.ref,
// computed
isAnyModalOpen: computed,
isPeekOpen: computed,
// action
setPeekIssue: action,
setIssueLinkData: action,
@@ -235,6 +237,10 @@ export abstract class IssueDetail implements IIssueDetail {
);
}
get isPeekOpen() {
return !!this.peekIssue;
}
// helper actions
getIsIssuePeeked = (issueId: string) => this.peekIssue?.issueId === issueId;
+4 -2
View File
@@ -98,9 +98,11 @@ export class UserSettingsStore implements IUserSettingsStore {
} else if (workspaceSlug) {
await persistence.initialize(workspaceSlug);
persistence.syncWorkspace();
projectId && persistence.syncIssues(projectId);
if (projectId) {
persistence.syncIssues(projectId);
}
}
} catch (e) {
} catch {
console.warn("error while toggling local DB");
runInAction(() => {
this.canUseLocalDB = currentLocalDBValue;
+4
View File
@@ -0,0 +1,4 @@
node_modules
build/*
dist/*
out/*
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+4
View File
@@ -0,0 +1,4 @@
node_modules
build/*
dist/*
out/*
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
@@ -4,7 +4,7 @@ import type { IEditorProps } from "@/types";
export type TCoreAdditionalExtensionsProps = Pick<
IEditorProps,
"disabledExtensions" | "flaggedExtensions" | "fileHandler"
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "embedHandler"
>;
export const CoreEditorAdditionalExtensions = (props: TCoreAdditionalExtensionsProps): Extensions => {
@@ -98,9 +98,9 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
editor={editor}
editorContainerClassName={cn(editorContainerClassName, "document-editor")}
id={id}
isTouchDevice={!!isTouchDevice}
flaggedExtensions={flaggedExtensions}
disabledExtensions={disabledExtensions}
isTouchDevice={!!isTouchDevice}
/>
);
};
@@ -18,8 +18,8 @@ type Props = {
isLoading?: boolean;
isTouchDevice: boolean;
tabIndex?: number;
flaggedExtensions?: IEditorProps["flaggedExtensions"];
disabledExtensions?: IEditorProps["disabledExtensions"];
flaggedExtensions: IEditorProps["flaggedExtensions"];
disabledExtensions: IEditorProps["disabledExtensions"];
};
export const PageRenderer = (props: Props) => {
@@ -55,6 +55,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
isActive: () => editorState.right,
},
];
if (editorState.code) return null;
return (
<div className="flex gap-0.5 px-2">
@@ -16,7 +16,9 @@ import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
import emojiRegex from "emoji-regex";
import { isEmojiSupported } from "is-emoji-supported";
// helpers
import { CORE_EXTENSIONS } from "@/constants/extension";
import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match";
import { getExtensionStorage } from "@/helpers/get-extension-storage";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
@@ -342,6 +344,10 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
},
addProseMirrorPlugins() {
const isTouchDevice = !!getExtensionStorage(this.editor, CORE_EXTENSIONS.UTILITY).isTouchDevice;
if (isTouchDevice) {
return [];
}
return [
Suggestion({
editor: this.editor,
@@ -3,7 +3,7 @@ import { gitHubEmojis, shortcodeToEmoji } from "@tiptap/extension-emoji";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { Emoji } from "./emoji";
import suggestion from "./suggestion";
import { emojiSuggestion } from "./suggestion";
export const EmojiExtension = Emoji.extend({
addStorage() {
@@ -25,6 +25,6 @@ export const EmojiExtension = Emoji.extend({
},
}).configure({
emojis: gitHubEmojis,
suggestion: suggestion,
suggestion: emojiSuggestion,
enableEmoticons: true,
});
@@ -10,7 +10,7 @@ import { EmojiItem, EmojiList, EmojiListRef } from "./components/emojis-list";
const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"];
const emojiSuggestion: EmojiOptions["suggestion"] = {
export const emojiSuggestion: EmojiOptions["suggestion"] = {
items: ({ editor, query }: { editor: Editor; query: string }): EmojiItem[] => {
const { emojis } = getExtensionStorage(editor, CORE_EXTENSIONS.EMOJI);
const { isSupported } = getExtensionStorage(editor, CORE_EXTENSIONS.EMOJI);
@@ -116,5 +116,3 @@ const emojiSuggestion: EmojiOptions["suggestion"] = {
};
},
};
export default emojiSuggestion;
+5 -4
View File
@@ -5,6 +5,7 @@ const project = resolve(process.cwd(), "tsconfig.json");
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["prettier", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["react", "react-hooks", "@typescript-eslint", "import"],
globals: {
React: true,
@@ -43,10 +44,10 @@ module.exports = {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-useless-empty-export": "error",
+6 -5
View File
@@ -3,6 +3,8 @@ const project = resolve(process.cwd(), "tsconfig.json");
module.exports = {
extends: ["next", "prettier", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["react", "@typescript-eslint", "import"],
globals: {
React: "readonly",
JSX: "readonly",
@@ -11,7 +13,6 @@ module.exports = {
node: true,
browser: true,
},
plugins: ["react", "@typescript-eslint", "import"],
settings: {
"import/resolver": {
typescript: {
@@ -42,10 +43,10 @@ module.exports = {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-useless-empty-export": "error",
+29 -5
View File
@@ -3,6 +3,7 @@ const project = resolve(process.cwd(), "tsconfig.json");
module.exports = {
extends: ["prettier", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
env: {
node: true,
es6: true,
@@ -25,10 +26,33 @@ module.exports = {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
}
"import/order": [
"warn",
{
groups: ["builtin", "external", "internal", "parent", "sibling"],
pathGroups: [
{
pattern: "@plane/**",
group: "external",
position: "after",
},
{
pattern: "@/**",
group: "internal",
position: "before",
},
],
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
},
};
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+2 -2
View File
@@ -5,7 +5,7 @@ export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
} catch (_error) {
window.localStorage.removeItem(key);
return defaultValue;
}
@@ -16,7 +16,7 @@ export const setValueIntoLocalStorage = (key: string, value: any) => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
} catch (_error) {
return false;
}
};
-5
View File
@@ -1,9 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
-2
View File
@@ -1,8 +1,6 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js", "plugin:storybook/recommended"],
parser: "@typescript-eslint/parser",
rules: {
"import/order": [
"warn",
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
-5
View File
@@ -1,9 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+1 -1
View File
@@ -60,7 +60,7 @@ export const AuthForm: React.FC<AuthFormProps> = ({
});
const [passwordStrength, setPasswordStrength] = useState<E_PASSWORD_STRENGTH>(E_PASSWORD_STRENGTH.EMPTY);
const [passwordsMatch, setPasswordsMatch] = useState(false);
const [_passwordsMatch, setPasswordsMatch] = useState(false);
const handleInputChange = (field: keyof AuthFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
+1
View File
@@ -8,6 +8,7 @@ import { cn } from "./utils";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
const currentYear = new Date().getFullYear();
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
+10 -10
View File
@@ -846,7 +846,7 @@ importers:
version: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-react:
specifier: ^7.33.2
version: 7.37.5(eslint@8.57.1)
version: 7.37.3(eslint@8.57.1)
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@8.57.1)
@@ -1071,7 +1071,7 @@ importers:
version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/node@22.17.2)(typescript@5.8.3)))
autoprefixer:
specifier: ^10.4.14
version: 10.4.21(postcss@8.5.6)
version: 10.4.20(postcss@8.5.6)
postcss:
specifier: ^8.4.38
version: 8.5.6
@@ -1243,7 +1243,7 @@ importers:
version: 18.3.1
autoprefixer:
specifier: ^10.4.19
version: 10.4.21(postcss@8.5.6)
version: 10.4.20(postcss@8.5.6)
postcss-cli:
specifier: ^11.0.0
version: 11.0.1(jiti@1.21.7)(postcss@8.5.6)
@@ -3865,8 +3865,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
autoprefixer@10.4.21:
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
@@ -4738,8 +4738,8 @@ packages:
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
eslint-plugin-react@7.37.5:
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
eslint-plugin-react@7.37.3:
resolution: {integrity: sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
@@ -10722,7 +10722,7 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
autoprefixer@10.4.21(postcss@8.5.6):
autoprefixer@10.4.20(postcss@8.5.6):
dependencies:
browserslist: 4.25.2
caniuse-lite: 1.0.30001735
@@ -11607,7 +11607,7 @@ snapshots:
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react: 7.37.3(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
optionalDependencies:
typescript: 5.8.3
@@ -11715,7 +11715,7 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-plugin-react@7.37.5(eslint@8.57.1):
eslint-plugin-react@7.37.3(eslint@8.57.1):
dependencies:
array-includes: 3.1.9
array.prototype.findlast: 1.2.5