Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 015f8cbfe5 | |||
| 42b38db131 |
@@ -35,10 +35,6 @@ 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 }}
|
||||
@@ -272,14 +268,15 @@ 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
|
||||
@@ -288,7 +285,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
|
||||
@@ -327,14 +324,7 @@ 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
|
||||
@@ -407,3 +397,4 @@ jobs:
|
||||
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
|
||||
${{ github.workspace }}/deployments/cli/community/variables.env
|
||||
${{ github.workspace }}/deployments/swarm/community/swarm.sh
|
||||
|
||||
|
||||
@@ -3,21 +3,11 @@ 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,18 +3,21 @@ name: Build and lint web apps
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
branches: ["preview"]
|
||||
types:
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "ready_for_review"
|
||||
- "review_requested"
|
||||
- "reopened"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
[
|
||||
"opened",
|
||||
"synchronize",
|
||||
"ready_for_review",
|
||||
"review_requested",
|
||||
"reopened",
|
||||
]
|
||||
paths:
|
||||
- "**.tsx?"
|
||||
- "**.jsx?"
|
||||
- "**.css"
|
||||
- "**.json"
|
||||
- "!apps/api/**"
|
||||
|
||||
jobs:
|
||||
build-and-lint:
|
||||
@@ -24,15 +27,11 @@ 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: 50
|
||||
filter: blob:none
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -45,11 +44,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint Affected
|
||||
run: pnpm turbo run check:lint --affected
|
||||
- name: Lint web apps
|
||||
run: pnpm run check:lint
|
||||
|
||||
- name: Check Affected format
|
||||
run: pnpm turbo run check:format --affected
|
||||
- name: Check format
|
||||
run: pnpm run check:format
|
||||
|
||||
- name: Build Affected
|
||||
run: pnpm turbo run build --affected
|
||||
- name: Build apps
|
||||
run: pnpm run build
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,5 @@
|
||||
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: React.FC = observer(() => {
|
||||
const InstanceEmailPage = observer(() => {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
|
||||
|
||||
@@ -29,7 +29,7 @@ const InstanceEmailPage: React.FC = 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,8 +25,9 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
|
||||
Boolean(parseInt(enableMagicLogin)) === true
|
||||
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
|
||||
Boolean(parseInt(enableGithubConfig)) === true
|
||||
? updateConfig("IS_GITHUB_ENABLED", "0")
|
||||
: updateConfig("IS_GITHUB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
|
||||
Boolean(parseInt(enableGitlabConfig)) === true
|
||||
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
|
||||
Boolean(parseInt(enableGoogleConfig)) === true
|
||||
? updateConfig("IS_GOOGLE_ENABLED", "0")
|
||||
: updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -25,8 +25,9 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
|
||||
Boolean(parseInt(enableEmailPassword)) === true
|
||||
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
|
||||
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -88,24 +89,20 @@ class IssueSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Invalid HTML passed")
|
||||
|
||||
# Validate description content for security
|
||||
if data.get("description_html"):
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if data.get("description"):
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
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
|
||||
raise serializers.ValidationError({"description": error_msg})
|
||||
|
||||
if data.get("description_html"):
|
||||
is_valid, error_msg = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
if data.get("description_binary"):
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if data.get("assignees", []):
|
||||
|
||||
@@ -12,6 +12,7 @@ from plane.db.models import (
|
||||
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
)
|
||||
from .base import BaseSerializer
|
||||
|
||||
@@ -199,18 +200,27 @@ 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, sanitized_html = validate_html_content(
|
||||
is_valid, error_msg = validate_json_content(data["description_html"])
|
||||
else:
|
||||
is_valid, error_msg = 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(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
@@ -75,24 +76,20 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
if "description" in attrs and attrs["description"]:
|
||||
is_valid, error_msg = validate_json_content(attrs["description"])
|
||||
if not is_valid:
|
||||
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
|
||||
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"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
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": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
|
||||
@@ -43,6 +43,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -127,24 +128,20 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
if "description" in attrs and attrs["description"]:
|
||||
is_valid, error_msg = validate_json_content(attrs["description"])
|
||||
if not is_valid:
|
||||
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
|
||||
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"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
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": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -228,13 +229,23 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
|
||||
return value
|
||||
|
||||
# Use the validation function from utils
|
||||
is_valid, error_message, sanitized_html = validate_html_content(value)
|
||||
is_valid, error_message = validate_html_content(value)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(error_message)
|
||||
|
||||
# Return sanitized HTML if available, otherwise return original
|
||||
return sanitized_html if sanitized_html is not None else value
|
||||
return 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"""
|
||||
|
||||
@@ -15,6 +15,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -64,18 +65,27 @@ class ProjectSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and 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 "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(
|
||||
{"error": "html content is not 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"])
|
||||
)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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,
|
||||
)
|
||||
|
||||
@@ -318,24 +319,20 @@ class StickySerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if "description" in data and data["description"]:
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
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
|
||||
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"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
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": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_json_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
@@ -289,22 +290,20 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
if "description" in data and data["description"]:
|
||||
is_valid, error_msg = validate_json_content(data["description"])
|
||||
if not is_valid:
|
||||
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
|
||||
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"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError({"description_html": error_msg})
|
||||
|
||||
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": "Invalid binary data"})
|
||||
raise serializers.ValidationError({"description_binary": error_msg})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
# Python imports
|
||||
import base64
|
||||
import nh3
|
||||
from plane.utils.exception_logger import log_exception
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
# 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",
|
||||
@@ -16,6 +41,70 @@ 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):
|
||||
"""
|
||||
@@ -60,21 +149,191 @@ def validate_binary_data(data):
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_html_content(html_content: str):
|
||||
def validate_html_content(html_content):
|
||||
"""
|
||||
Sanitize HTML content using nh3.
|
||||
Returns a tuple: (is_valid, error_message, clean_html)
|
||||
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)
|
||||
"""
|
||||
if not html_content:
|
||||
return True, None, None
|
||||
return True, None # Empty is OK
|
||||
|
||||
# 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)", None
|
||||
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
|
||||
|
||||
try:
|
||||
clean_html = nh3.clean(html_content)
|
||||
return True, None, clean_html
|
||||
# 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"
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False, "Failed to sanitize HTML", None
|
||||
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
|
||||
|
||||
@@ -69,5 +69,3 @@ opentelemetry-instrumentation-django==0.49b1
|
||||
opentelemetry-exporter-otlp==1.28.1
|
||||
# OpenAPI Specification
|
||||
drf-spectacular==0.28.0
|
||||
# html sanitizer
|
||||
nh3==0.2.18
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/server.js"],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@plane/eslint-config/server.js"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
// 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[] = [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Server } from "@hocuspocus/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// 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";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
// types
|
||||
import { type HocusPocusServerContext } from "@/core/types/common.js";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
// services
|
||||
import { UserService } from "@/core/services/user.service.js";
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import compression from "compression";
|
||||
import cors from "cors";
|
||||
import express, { Request, Response } from "express";
|
||||
import expressWs from "express-ws";
|
||||
import express, { Request, Response } from "express";
|
||||
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";
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
+1
-10
@@ -1,13 +1,4 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useRef } 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 { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper";
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
|
||||
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
|
||||
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(
|
||||
@@ -26,17 +38,55 @@ 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 (
|
||||
<SidebarWrapper title="Projects" quickActions={<SidebarQuickActions />}>
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</SidebarWrapper>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
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,4 +1,4 @@
|
||||
// plane imports
|
||||
// types
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import {
|
||||
EIssueServiceType,
|
||||
@@ -12,8 +12,12 @@ 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 {
|
||||
@@ -81,7 +85,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;
|
||||
}
|
||||
@@ -114,10 +118,9 @@ export class IssueService extends APIService {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((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
|
||||
@@ -135,9 +138,8 @@ export class IssueService extends APIService {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/list/`, {
|
||||
params: { issues: issueIds.join(",") },
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((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;
|
||||
@@ -244,7 +246,6 @@ 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}/`)
|
||||
@@ -353,9 +354,8 @@ 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(async (response) => {
|
||||
.then((response) => {
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
const { persistence } = await import("@/local-db/storage.sqlite");
|
||||
persistence.syncIssues(projectId);
|
||||
}
|
||||
return response?.data;
|
||||
@@ -373,9 +373,8 @@ export class IssueService extends APIService {
|
||||
}
|
||||
): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data)
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
const { persistence } = await import("@/local-db/storage.sqlite");
|
||||
persistence.syncIssues(projectId);
|
||||
}
|
||||
return response?.data;
|
||||
@@ -395,9 +394,8 @@ export class IssueService extends APIService {
|
||||
archived_at: string;
|
||||
}> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-archive-issues/`, data)
|
||||
.then(async (response) => {
|
||||
.then((response) => {
|
||||
if (this.serviceType === EIssueServiceType.ISSUES) {
|
||||
const { persistence } = await import("@/local-db/storage.sqlite");
|
||||
persistence.syncIssues(projectId);
|
||||
}
|
||||
return response?.data;
|
||||
@@ -478,10 +476,9 @@ export class IssueService extends APIService {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/work-items/${project_identifier}-${issue_sequence}/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then(async (response) => {
|
||||
.then((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,7 +95,6 @@ export interface IIssueDetail
|
||||
attachmentDeleteModalId: string | null;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
isPeekOpen: boolean;
|
||||
// helper actions
|
||||
getIsIssuePeeked: (issueId: string) => boolean;
|
||||
// actions
|
||||
@@ -189,7 +188,6 @@ export abstract class IssueDetail implements IIssueDetail {
|
||||
lastWidgetAction: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
isPeekOpen: computed,
|
||||
// action
|
||||
setPeekIssue: action,
|
||||
setIssueLinkData: action,
|
||||
@@ -237,10 +235,6 @@ export abstract class IssueDetail implements IIssueDetail {
|
||||
);
|
||||
}
|
||||
|
||||
get isPeekOpen() {
|
||||
return !!this.peekIssue;
|
||||
}
|
||||
|
||||
// helper actions
|
||||
getIsIssuePeeked = (issueId: string) => this.peekIssue?.issueId === issueId;
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const DRAG_HANDLE_ADDITIONAL_SELECTORS = [];
|
||||
@@ -4,7 +4,7 @@ import type { IEditorProps } from "@/types";
|
||||
|
||||
export type TCoreAdditionalExtensionsProps = Pick<
|
||||
IEditorProps,
|
||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "embedHandler"
|
||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler"
|
||||
>;
|
||||
|
||||
export const CoreEditorAdditionalExtensions = (props: TCoreAdditionalExtensionsProps): Extensions => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import type { AnyExtension } from "@tiptap/core";
|
||||
import { SlashCommands } from "@/extensions";
|
||||
// plane editor types
|
||||
import type { TEmbedConfig } from "@/plane-editor/types";
|
||||
// types
|
||||
import type { IEditorProps, TExtensions, TUserDetails } from "@/types";
|
||||
|
||||
@@ -10,7 +8,6 @@ export type TDocumentEditorAdditionalExtensionsProps = Pick<
|
||||
IEditorProps,
|
||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler"
|
||||
> & {
|
||||
embedConfig: TEmbedConfig | undefined;
|
||||
isEditable: boolean;
|
||||
provider?: HocuspocusProvider;
|
||||
userDetails: TUserDetails;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// helpers
|
||||
import type { TEditorRefHelperArgs } from "@/helpers/editor-ref";
|
||||
// local imports
|
||||
import type { TAdditionalEditorRefApiMethods } from "../types/editor";
|
||||
|
||||
export const getAdditionalEditorRefHelpers = (_args: TEditorRefHelperArgs): TAdditionalEditorRefApiMethods => ({});
|
||||
@@ -0,0 +1,7 @@
|
||||
export type TAdditionalEditorCommands = never;
|
||||
|
||||
export type TAdditionalCommandExtraProps = {};
|
||||
|
||||
export type TAdditionalEditorRefApiMethods = {};
|
||||
|
||||
export type IEditorPropsExtended = {};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./issue-embed";
|
||||
@@ -1,17 +0,0 @@
|
||||
export type TEmbedConfig = {
|
||||
issue?: TIssueEmbedConfig;
|
||||
};
|
||||
|
||||
export type TReadOnlyEmbedConfig = TEmbedConfig;
|
||||
|
||||
export type TIssueEmbedConfig = {
|
||||
widgetCallback: ({
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}: {
|
||||
issueId: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string | undefined;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
@@ -6,8 +6,6 @@ import { cn } from "@plane/utils";
|
||||
import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { WorkItemEmbedExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -21,13 +19,12 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
bubbleMenuEnabled = true,
|
||||
containerClassName,
|
||||
documentLoaderClassName,
|
||||
extensions: externalExtensions = [],
|
||||
extensions,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
editorClassName = "",
|
||||
editorProps,
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
forwardedRef,
|
||||
@@ -47,27 +44,12 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const allExtensions = [...externalExtensions];
|
||||
|
||||
if (embedHandler?.issue) {
|
||||
allExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return allExtensions;
|
||||
}, [externalExtensions, embedHandler.issue]);
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
editable,
|
||||
editorClassName,
|
||||
editorProps,
|
||||
embedHandler,
|
||||
extensions,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { HeadingListExtension, WorkItemEmbedExtension, SideMenuExtension } from "@/extensions";
|
||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -25,7 +25,6 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
forwardedRef,
|
||||
@@ -39,13 +38,6 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
} = props;
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const additionalExtensions: Extensions = [];
|
||||
if (embedHandler?.issue) {
|
||||
additionalExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
additionalExtensions.push(
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
@@ -54,7 +46,6 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
HeadingListExtension,
|
||||
...DocumentEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
embedConfig: embedHandler,
|
||||
flaggedExtensions,
|
||||
isEditable: editable,
|
||||
fileHandler,
|
||||
@@ -82,7 +73,6 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
initialValue: value,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
embedHandler,
|
||||
});
|
||||
|
||||
const editorContainerClassName = getEditorClassNames({
|
||||
@@ -98,9 +88,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) => {
|
||||
|
||||
@@ -41,7 +41,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
embedHandler,
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
@@ -66,7 +65,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
embedHandler,
|
||||
});
|
||||
|
||||
const editorContainerClassName = getEditorClassNames({
|
||||
|
||||
@@ -55,7 +55,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
isActive: () => editorState.right,
|
||||
},
|
||||
];
|
||||
if (editorState.code) return null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-0.5 px-2">
|
||||
|
||||
@@ -21,7 +21,6 @@ import { CustomMentionExtensionConfig } from "./mentions/extension-config";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
@@ -101,5 +100,3 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensionsWithoutProps,
|
||||
];
|
||||
|
||||
export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig];
|
||||
|
||||
@@ -16,9 +16,7 @@ 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> {
|
||||
@@ -344,10 +342,6 @@ 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 { emojiSuggestion } from "./suggestion";
|
||||
import suggestion from "./suggestion";
|
||||
|
||||
export const EmojiExtension = Emoji.extend({
|
||||
addStorage() {
|
||||
@@ -25,6 +25,6 @@ export const EmojiExtension = Emoji.extend({
|
||||
},
|
||||
}).configure({
|
||||
emojis: gitHubEmojis,
|
||||
suggestion: emojiSuggestion,
|
||||
suggestion: suggestion,
|
||||
enableEmoticons: true,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { EmojiItem, EmojiList, EmojiListRef } from "./components/emojis-list";
|
||||
|
||||
const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"];
|
||||
|
||||
export const emojiSuggestion: EmojiOptions["suggestion"] = {
|
||||
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,3 +116,5 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default emojiSuggestion;
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
// plane editor extensions
|
||||
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
// types
|
||||
import type { IEditorProps, TEmbedConfig } from "@/types";
|
||||
import type { IEditorProps } from "@/types";
|
||||
// local imports
|
||||
import { CustomImageExtension } from "./custom-image/extension";
|
||||
import { EmojiExtension } from "./emoji/extension";
|
||||
@@ -45,11 +45,9 @@ type TArguments = Pick<
|
||||
| "mentionHandler"
|
||||
| "placeholder"
|
||||
| "tabIndex"
|
||||
| "embedHandler"
|
||||
> & {
|
||||
enableHistory: boolean;
|
||||
editable: boolean;
|
||||
embedHandler?: TEmbedConfig;
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
@@ -62,7 +60,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
embedHandler,
|
||||
editable,
|
||||
} = args;
|
||||
|
||||
@@ -118,7 +115,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
disabledExtensions,
|
||||
flaggedExtensions,
|
||||
fileHandler,
|
||||
embedHandler,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ export * from "./mentions";
|
||||
export * from "./slash-commands";
|
||||
export * from "./table";
|
||||
export * from "./typography";
|
||||
export * from "./work-item-embed";
|
||||
export * from "./core-without-props";
|
||||
export * from "./custom-color";
|
||||
export * from "./enter-key";
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
export const WorkItemEmbedExtensionConfig = Node.create({
|
||||
name: CORE_EXTENSIONS.WORK_ITEM_EMBED,
|
||||
group: "block",
|
||||
atom: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
entity_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
project_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
workspace_identifier: {
|
||||
default: undefined,
|
||||
},
|
||||
id: {
|
||||
default: undefined,
|
||||
},
|
||||
entity_name: {
|
||||
default: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "issue-embed-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["issue-embed-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from "@tiptap/react";
|
||||
// local imports
|
||||
import { WorkItemEmbedExtensionConfig } from "./extension-config";
|
||||
|
||||
type Props = {
|
||||
widgetCallback: ({
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}: {
|
||||
issueId: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string | undefined;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const WorkItemEmbedExtension = (props: Props) =>
|
||||
WorkItemEmbedExtensionConfig.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((issueProps: NodeViewProps) => (
|
||||
<NodeViewWrapper>
|
||||
{props.widgetCallback({
|
||||
issueId: issueProps.node.attrs.entity_identifier,
|
||||
projectId: issueProps.node.attrs.project_identifier,
|
||||
workspaceSlug: issueProps.node.attrs.workspace_identifier,
|
||||
})}
|
||||
</NodeViewWrapper>
|
||||
));
|
||||
},
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./extension";
|
||||
@@ -9,18 +9,20 @@ import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
import { CORE_EDITOR_META } from "@/constants/meta";
|
||||
// types
|
||||
import type { EditorRefApi, TEditorCommands } from "@/types";
|
||||
// plane editor imports
|
||||
import { getAdditionalEditorRefHelpers } from "@/plane-editor/helpers/editor-ref";
|
||||
// local imports
|
||||
import { getParagraphCount } from "./common";
|
||||
import { getExtensionStorage } from "./get-extension-storage";
|
||||
import { insertContentAtSavedSelection } from "./insert-content-at-cursor-position";
|
||||
import { scrollSummary, scrollToNodeViaDOMCoordinates } from "./scroll-to-node";
|
||||
|
||||
type TArgs = {
|
||||
export type TEditorRefHelperArgs = {
|
||||
editor: Editor | null;
|
||||
provider: HocuspocusProvider | undefined;
|
||||
};
|
||||
|
||||
export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
|
||||
export const getEditorRefHelpers = (args: TEditorRefHelperArgs): EditorRefApi => {
|
||||
const { editor, provider } = args;
|
||||
|
||||
return {
|
||||
@@ -238,5 +240,6 @@ export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
|
||||
Y.applyUpdate(document, value);
|
||||
},
|
||||
undo: () => editor?.commands.undo(),
|
||||
...getAdditionalEditorRefHelpers(args),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,14 +4,11 @@ import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-pros
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
import {
|
||||
CoreEditorExtensionsWithoutProps,
|
||||
DocumentEditorExtensionsWithoutProps,
|
||||
} from "@/extensions/core-without-props";
|
||||
import { CoreEditorExtensionsWithoutProps } from "@/extensions/core-without-props";
|
||||
|
||||
// editor extension configs
|
||||
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
|
||||
// editor schemas
|
||||
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
@@ -20,7 +20,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
editable,
|
||||
editorClassName = "",
|
||||
editorProps = {},
|
||||
embedHandler,
|
||||
extensions = [],
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
@@ -80,7 +79,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
);
|
||||
|
||||
const editor = useEditor({
|
||||
embedHandler,
|
||||
disabledExtensions,
|
||||
id,
|
||||
editable,
|
||||
@@ -99,7 +97,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
...extensions,
|
||||
...DocumentEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
embedConfig: embedHandler,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
isEditable: editable,
|
||||
|
||||
@@ -32,7 +32,6 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
onAssetChange,
|
||||
onChange,
|
||||
onEditorFocus,
|
||||
embedHandler,
|
||||
onTransaction,
|
||||
placeholder,
|
||||
provider,
|
||||
@@ -64,7 +63,6 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
embedHandler,
|
||||
}),
|
||||
...extensions,
|
||||
],
|
||||
|
||||
@@ -4,7 +4,9 @@ import { EditorView } from "@tiptap/pm/view";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions/side-menu";
|
||||
// plane editor imports
|
||||
import { DRAG_HANDLE_ADDITIONAL_SELECTORS } from "@/plane-editor/constants/common";
|
||||
|
||||
const verticalEllipsisIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>';
|
||||
@@ -17,11 +19,10 @@ const generalSelectors = [
|
||||
"h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block",
|
||||
"[data-type=horizontalRule]",
|
||||
"table:not(.table-drag-preview)",
|
||||
".issue-embed",
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-callout-component",
|
||||
".editor-embed-component",
|
||||
...DRAG_HANDLE_ADDITIONAL_SELECTORS,
|
||||
].join(", ");
|
||||
|
||||
const maxScrollSpeed = 20;
|
||||
|
||||
@@ -2,9 +2,15 @@ import type { Content, Extensions, JSONContent, RawCommands } from "@tiptap/core
|
||||
import type { MarkType, NodeType } from "@tiptap/pm/model";
|
||||
import type { Selection } from "@tiptap/pm/state";
|
||||
import type { EditorProps, EditorView } from "@tiptap/pm/view";
|
||||
import type { NodeViewProps as TNodeViewProps } from "@tiptap/react";
|
||||
// extension types
|
||||
import type { TTextAlign } from "@/extensions";
|
||||
import type { TTextAlign } from "@/extensions/text-align";
|
||||
// plane editor imports
|
||||
import type {
|
||||
IEditorPropsExtended,
|
||||
TAdditionalCommandExtraProps,
|
||||
TAdditionalEditorCommands,
|
||||
TAdditionalEditorRefApiMethods,
|
||||
} from "@/plane-editor/types/editor";
|
||||
// types
|
||||
import type {
|
||||
IMarking,
|
||||
@@ -13,7 +19,6 @@ import type {
|
||||
TDocumentEventEmitter,
|
||||
TDocumentEventsServer,
|
||||
TEditorAsset,
|
||||
TEmbedConfig,
|
||||
TExtensions,
|
||||
TFileHandler,
|
||||
TMentionHandler,
|
||||
@@ -43,22 +48,17 @@ export type TEditorCommands =
|
||||
| "image"
|
||||
| "divider"
|
||||
| "link"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout"
|
||||
| "attachment"
|
||||
| "emoji"
|
||||
| "external-embed";
|
||||
| TAdditionalEditorCommands;
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
savedSelection: Selection | null;
|
||||
};
|
||||
attachment: {
|
||||
savedSelection: Selection | null;
|
||||
};
|
||||
"text-color": {
|
||||
color: string | undefined;
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export type TCommandExtraProps = {
|
||||
"text-align": {
|
||||
alignment: TTextAlign;
|
||||
};
|
||||
};
|
||||
} & TAdditionalCommandExtraProps;
|
||||
|
||||
// Create a utility type that maps a command to its extra props or an empty object if none are defined
|
||||
export type TCommandWithProps<T extends TEditorCommands> = T extends keyof TCommandExtraProps
|
||||
@@ -127,7 +127,7 @@ export type EditorRefApi = {
|
||||
setFocusAtPosition: (position: number) => void;
|
||||
setProviderDocument: (value: Uint8Array) => void;
|
||||
undo: () => void;
|
||||
};
|
||||
} & TAdditionalEditorRefApiMethods;
|
||||
|
||||
// editor props
|
||||
export type IEditorProps = {
|
||||
@@ -140,7 +140,6 @@ export type IEditorProps = {
|
||||
editorClassName?: string;
|
||||
editorProps?: EditorProps;
|
||||
extensions?: Extensions;
|
||||
embedHandler?: TEmbedConfig;
|
||||
flaggedExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
@@ -150,14 +149,14 @@ export type IEditorProps = {
|
||||
isTouchDevice?: boolean;
|
||||
mentionHandler: TMentionHandler;
|
||||
onAssetChange?: (assets: TEditorAsset[]) => void;
|
||||
onEditorFocus?: () => void;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
onEditorFocus?: () => void;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
onTransaction?: () => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
value?: string | null;
|
||||
};
|
||||
} & IEditorPropsExtended;
|
||||
|
||||
export type ILiteTextEditorProps = IEditorProps;
|
||||
|
||||
@@ -170,7 +169,6 @@ export type ICollaborativeDocumentEditorProps = Omit<IEditorProps, "initialValue
|
||||
documentLoaderClassName?: string;
|
||||
dragDropEnabled?: boolean;
|
||||
editable: boolean;
|
||||
embedHandler: TEmbedConfig;
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
user: TUserDetails;
|
||||
@@ -178,7 +176,6 @@ export type ICollaborativeDocumentEditorProps = Omit<IEditorProps, "initialValue
|
||||
|
||||
export type IDocumentEditorProps = Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> & {
|
||||
aiHandler?: TAIHandler;
|
||||
embedHandler: TEmbedConfig;
|
||||
user?: TUserDetails;
|
||||
value: Content;
|
||||
};
|
||||
@@ -194,5 +191,3 @@ export type EditorEvents = {
|
||||
destroy: never;
|
||||
ready: { height: number };
|
||||
};
|
||||
|
||||
export type NodeViewProps = TNodeViewProps;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export type TEmbedItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
icon: React.ReactNode;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
@@ -13,7 +13,6 @@ type TCoreHookProps = Pick<
|
||||
| "handleEditorReady"
|
||||
| "isTouchDevice"
|
||||
| "onEditorFocus"
|
||||
| "embedHandler"
|
||||
>;
|
||||
|
||||
export type TEditorHookProps = TCoreHookProps &
|
||||
@@ -51,7 +50,4 @@ export type TCollaborativeEditorHookProps = TCoreHookProps &
|
||||
| "placeholder"
|
||||
| "tabIndex"
|
||||
> &
|
||||
Pick<
|
||||
ICollaborativeDocumentEditorProps,
|
||||
"dragDropEnabled" | "embedHandler" | "realtimeConfig" | "serverHandler" | "user"
|
||||
>;
|
||||
Pick<ICollaborativeDocumentEditorProps, "dragDropEnabled" | "realtimeConfig" | "serverHandler" | "user">;
|
||||
|
||||
@@ -3,10 +3,8 @@ export * from "./asset";
|
||||
export * from "./collaboration";
|
||||
export * from "./config";
|
||||
export * from "./editor";
|
||||
export * from "./embed";
|
||||
export * from "./extensions";
|
||||
export * from "./hook";
|
||||
export * from "./mention";
|
||||
export * from "./slash-commands-suggestion";
|
||||
export * from "@/plane-editor/types";
|
||||
export * from "./document-collaborative-events";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "src/ce/extensions";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "src/ce/types";
|
||||
@@ -5,7 +5,6 @@ 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,
|
||||
@@ -44,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",
|
||||
|
||||
@@ -3,8 +3,6 @@ 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",
|
||||
@@ -13,6 +11,7 @@ module.exports = {
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
plugins: ["react", "@typescript-eslint", "import"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
@@ -43,10 +42,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",
|
||||
|
||||
@@ -3,7 +3,6 @@ const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
module.exports = {
|
||||
extends: ["prettier", "plugin:@typescript-eslint/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
@@ -26,33 +25,10 @@ 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @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",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { TProjectPublishSettings } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing project publish operations within plane core application.
|
||||
* Extends APIService to handle HTTP requests to the project publish-related endpoints.
|
||||
* @extends {APIService}
|
||||
* @remarks This service is only available for plane core
|
||||
*/
|
||||
export abstract class CoreProjectPublishService extends APIService {
|
||||
constructor(BASE_URL: string) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
|
||||
async retrieve(workspaceSlug: string, projectID: string): Promise<TProjectPublishSettings> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/projects/${projectID}/anchor/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveSettings(anchor: string): Promise<TProjectPublishSettings> {
|
||||
return this.get(`/api/public/anchor/${anchor}/settings/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CoreProjectPublishService } from "./core.service";
|
||||
|
||||
export abstract class ExtendedProjectPublishService extends CoreProjectPublishService {
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { ExtendedProjectPublishService } from "./extended.service";
|
||||
|
||||
export class ProjectPublishService extends ExtendedProjectPublishService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { IProjectView } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class CoreProjectViewService extends APIService {
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(workspaceSlug: string, projectId: string, viewId: string, data: Partial<IProjectView>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async destroy(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async list(workspaceSlug: string, projectId: string): Promise<IProjectView[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieve(workspaceSlug: string, projectId: string, viewId: string): Promise<IProjectView> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorksItems(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/issues/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async addToFavorites(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/`, { view: viewId })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeFromFavorites(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CoreProjectViewService } from "./core.service";
|
||||
|
||||
export class ExtendedProjectViewService extends CoreProjectViewService {
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { ExtendedProjectViewService } from "./extended.service";
|
||||
|
||||
export class ProjectViewService extends ExtendedProjectViewService {
|
||||
constructor(baseUrl?: string) {
|
||||
super(baseUrl || API_BASE_URL);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { TProject, TProjectAnalyticsCountParams, TProjectAnalyticsCount } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class CoreProjectService extends APIService {
|
||||
constructor(BASE_URL: string) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, data: Partial<TProject>): Promise<TProject> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async checkIdentifierAvailability(workspaceSlug: string, identifier: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/project-identifiers/`, {
|
||||
params: {
|
||||
name: identifier,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
list(workspaceSlug: string): Promise<TProject[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
retrieve(workspaceSlug: string, projectId: string): Promise<TProject> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
stats(workspaceSlug: string, params?: TProjectAnalyticsCountParams): Promise<TProjectAnalyticsCount[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/project-stats/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
update(workspaceSlug: string, projectId: string, data: Partial<TProject>): Promise<TProject> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
destroy(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
addToFavorites(workspaceSlug: string, project: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`, { project })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
removeFromFavorites(workspaceSlug: string, projectId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/user-favorite-projects/${projectId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CoreProjectService } from "./core.service";
|
||||
|
||||
export class ExtendedProjectService extends CoreProjectService {
|
||||
constructor(BASE_URL: string) {
|
||||
super(BASE_URL);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,2 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { ExtendedProjectService } from "./extended.service";
|
||||
|
||||
export class ProjectService extends ExtendedProjectService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
}
|
||||
export * from "./view.service";
|
||||
export * from "./sites-publish.service";
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { TProjectPublishSettings } from "@plane/types";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing project publish operations within plane sites application.
|
||||
* Extends APIService to handle HTTP requests to the project publish-related endpoints.
|
||||
* @extends {APIService}
|
||||
* @remarks This service is only available for plane sites
|
||||
*/
|
||||
export class SitesProjectPublishService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves publish settings for a specific anchor.
|
||||
* @param {string} anchor - The anchor identifier
|
||||
* @returns {Promise<TProjectPublishSettings>} The publish settings
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async retrieveSettingsByAnchor(anchor: string): Promise<TProjectPublishSettings> {
|
||||
return this.get(`/api/public/anchor/${anchor}/settings/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves publish settings for a specific project.
|
||||
* @param {string} workspaceSlug - The workspace slug
|
||||
* @param {string} projectID - The project identifier
|
||||
* @returns {Promise<TProjectPublishSettings>} The publish settings
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async retrieveSettingsByProjectId(workspaceSlug: string, projectID: string): Promise<TProjectPublishSettings> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/projects/${projectID}/anchor/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// api services
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class ProjectViewService extends APIService {
|
||||
/**
|
||||
* Creates an instance of ProjectViewService
|
||||
* @param {string} baseUrl - The base URL for API requests
|
||||
*/
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { IState } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
export class CoreStateService extends APIService {
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
async anchorStates(anchor: string): Promise<IState[]> {
|
||||
return this.get(`/api/public/anchor/${anchor}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async list(workspaceSlug: string, projectId: string): Promise<IState[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieve(workspaceSlug: string, projectId: string, stateId: string): Promise<IState> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, projectId: string, data: Partial<IState>): Promise<IState> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(workspaceSlug: string, projectId: string, stateId: string, data: Partial<IState>): Promise<IState> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async destroy(workspaceSlug: string, projectId: string, stateId: string): Promise<IState> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CoreStateService } from "./core.service";
|
||||
|
||||
export class ExtendedStateService extends CoreStateService {
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { ExtendedStateService } from "./extended.service";
|
||||
|
||||
export class StateService extends ExtendedStateService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
}
|
||||
export * from "./sites-state.service";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { IState } from "@plane/types";
|
||||
// api service
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing states within plane sites application.
|
||||
* Extends APIService to handle HTTP requests to the state-related endpoints.
|
||||
* @extends {APIService}
|
||||
* @remarks This service is only available for plane sites
|
||||
*/
|
||||
export class SitesStateService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of states for a specific anchor.
|
||||
* @param {string} anchor - The anchor identifier
|
||||
* @returns {Promise<IState[]>} The list of states
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async list(anchor: string): Promise<IState[]> {
|
||||
return this.get(`/api/public/anchor/${anchor}/states/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user