Compare commits

..

1 Commits

Author SHA1 Message Date
gakshita fda8c728b5 chore: replaced listbox with our custom implementation 2025-08-18 19:47:21 +05:30
1445 changed files with 15559 additions and 23229 deletions
-45
View File
@@ -16,48 +16,3 @@ out/
**/out/
dist/
**/dist/
# Logs
npm-debug.log*
pnpm-debug.log*
.pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS junk
.DS_Store
Thumbs.db
# Editor settings
.vscode
.idea
# Coverage and test output
coverage/
**/coverage/
*.lcov
.junit/
test-results/
# Caches and build artifacts
.cache/
**/.cache/
storybook-static/
*storybook.log
*.tsbuildinfo
# Local env and secrets
.env.local
.env.development.local
.env.test.local
.env.production.local
.secrets
tmp/
temp/
# Database/cache dumps
*.rdb
*.rdb.gz
# Misc
*.pem
*.key
@@ -4,14 +4,7 @@ on:
workflow_dispatch:
pull_request:
branches: ["preview"]
types:
[
"opened",
"synchronize",
"ready_for_review",
"review_requested",
"reopened",
]
types: ["opened", "synchronize", "ready_for_review", "review_requested", "reopened"]
paths:
- "**.tsx?"
- "**.jsx?"
@@ -38,17 +31,13 @@ jobs:
with:
node-version-file: ".nvmrc"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Build web apps
run: yarn run build
- name: Lint web apps
run: pnpm run check:lint
run: yarn run ci:lint
- name: Check format
run: pnpm run check:format
- name: Build apps
run: pnpm run build
+3 -7
View File
@@ -24,13 +24,11 @@ out/
.DS_Store
*.pem
.history
tsconfig.tsbuildinfo
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-debug.log*
# Local env files
@@ -62,7 +60,6 @@ node_modules/
assets/dist/
npm-debug.log
yarn-error.log
pnpm-debug.log
# Editor directories and files
.idea
@@ -78,9 +75,10 @@ package-lock.json
# lock files
package-lock.json
pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
.secrets
tmp/
@@ -97,5 +95,3 @@ dev-editor
# Redis
*.rdb
*.rdb.gz
storybook-static
-34
View File
@@ -1,34 +0,0 @@
# Enforce pnpm workspace behavior and allow Turbo's lifecycle hooks if scripts are disabled
# This repo uses pnpm with workspaces.
# Prefer linking local workspace packages when available
prefer-workspace-packages=true
link-workspace-packages=true
shared-workspace-lockfile=true
# Make peer installs smoother across the monorepo
auto-install-peers=true
strict-peer-dependencies=false
# If scripts are disabled (e.g., CI with --ignore-scripts), allowlisted packages can still run their hooks
# Turbo occasionally performs postinstall tasks for optimal performance
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=typescript
# Reproducible installs across CI and dev
prefer-frozen-lockfile=true
# Prefer resolving to highest versions in monorepo to reduce duplication
resolution-mode=highest
# Speed up native module builds by caching side effects
side-effects-cache=true
# Speed up local dev by reusing local store when possible
prefer-offline=true
# Ensure workspace protocol is used when adding internal deps
save-workspace-protocol=true
+1
View File
@@ -0,0 +1 @@
nodeLinker: node-modules
+1 -1
View File
@@ -73,7 +73,7 @@ docker compose -f docker-compose-local.yml up
4. Start web apps:
```bash
pnpm dev
yarn dev
```
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
+2 -2
View File
@@ -2,5 +2,5 @@
.vercel
.tubro
out/
dist/
build/
dis/
build/
+5 -14
View File
@@ -1,11 +1,5 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
# Setup pnpm package manager with corepack and configure global bin directory for caching
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
@@ -13,8 +7,7 @@ FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG TURBO_VERSION=2.5.6
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=admin --docker
@@ -29,13 +22,11 @@ WORKDIR /app
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install --network-timeout 500000
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -58,7 +49,7 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
RUN pnpm turbo run build --filter=admin
RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
@@ -100,4 +91,4 @@ ENV TURBO_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["node", "apps/admin/server.js"]
CMD ["node", "apps/admin/server.js"]
+3 -3
View File
@@ -5,8 +5,8 @@ WORKDIR /app
COPY . .
RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install
RUN yarn global add turbo
RUN yarn install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
@@ -14,4 +14,4 @@ EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
CMD ["pnpm", "dev", "--filter=admin"]
CMD ["yarn", "dev", "--filter=admin"]
+12 -12
View File
@@ -18,13 +18,15 @@
},
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/services": "workspace:*",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/constants": "*",
"@plane/hooks": "*",
"@plane/propel": "*",
"@plane/services": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
"axios": "1.11.0",
"lodash": "^4.17.21",
@@ -37,15 +39,13 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "7.51.5",
"sharp": "^0.33.5",
"swr": "^2.2.4",
"uuid": "^9.0.1"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@types/lodash": "^4.17.6",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
@@ -45,10 +45,6 @@ class ProjectCreateSerializer(BaseSerializer):
"archive_in",
"close_in",
"timezone",
"logo_props",
"external_source",
"external_id",
"is_issue_type_enabled",
]
read_only_fields = [
+1 -3
View File
@@ -89,9 +89,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentDetailAPIEndpoint.as_view(
http_method_names=["get", "patch", "delete"]
),
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]),
name="issue-attachment",
),
]
+1 -1
View File
@@ -4,7 +4,7 @@ from plane.api.views import ProjectMemberAPIEndpoint, WorkspaceMemberAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberAPIEndpoint.as_view(http_method_names=["get"]),
name="project-members",
),
+3 -84
View File
@@ -75,7 +75,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
from plane.utils.host import base_host
from plane.bgtasks.webhook_task import model_activity
from plane.app.permissions import ROLE
from plane.utils.openapi import (
work_item_docs,
label_docs,
@@ -145,22 +145,6 @@ from plane.utils.openapi import (
)
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
def user_has_issue_permission(
user_id, project_id, issue=None, allowed_roles=None, allow_creator=True
):
if allow_creator and issue is not None and user_id == issue.created_by_id:
return True
qs = ProjectMember.objects.filter(
project_id=project_id,
member_id=user_id,
is_active=True,
)
if allowed_roles is not None:
qs = qs.filter(role__in=allowed_roles)
return qs.exists()
class WorkspaceIssueAPIEndpoint(BaseAPIView):
"""
@@ -347,10 +331,6 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
)
)
total_issue_queryset = Issue.issue_objects.filter(
project_id=project_id, workspace__slug=slug
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
@@ -410,7 +390,6 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
return self.paginate(
request=request,
queryset=(issue_queryset),
total_count_queryset=total_issue_queryset,
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
).data,
@@ -1803,6 +1782,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
model = FileAsset
permission_classes = [ProjectEntityPermission]
use_read_replica = True
@issue_attachment_docs(
@@ -1885,22 +1865,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
Generate presigned URL for uploading file attachments to a work item.
Validates file type and size before creating the attachment record.
"""
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the user is creator or admin,member then allow the upload
if not user_has_issue_permission(
request.user.id,
project_id=project_id,
issue=issue,
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value],
allow_creator=True,
):
return Response(
{"error": "You are not allowed to upload this attachment"},
status=status.HTTP_403_FORBIDDEN,
)
name = request.data.get("name")
type = request.data.get("type", False)
size = request.data.get("size")
@@ -2025,6 +1989,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
"""Issue Attachment Detail Endpoint"""
serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
model = FileAsset
use_read_replica = True
@@ -2047,22 +2012,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
Soft delete an attachment from a work item by marking it as deleted.
Records deletion activity and triggers metadata cleanup.
"""
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the request user is creator or admin then delete the attachment
if not user_has_issue_permission(
request.user,
project_id=project_id,
issue=issue,
allowed_roles=[ROLE.ADMIN.value],
allow_creator=True,
):
return Response(
{"error": "You are not allowed to delete this attachment"},
status=status.HTTP_403_FORBIDDEN,
)
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
@@ -2125,19 +2074,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
Retrieve details of a specific attachment.
"""
# if the user is part of the project then allow the download
if not user_has_issue_permission(
request.user,
project_id=project_id,
issue=None,
allowed_roles=None,
allow_creator=False,
):
return Response(
{"error": "You are not allowed to download this attachment"},
status=status.HTTP_403_FORBIDDEN,
)
# Get the asset
asset = FileAsset.objects.get(
id=pk, workspace__slug=slug, project_id=project_id
@@ -2192,23 +2128,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
Mark an attachment as uploaded after successful file transfer to storage.
Triggers activity logging and metadata extraction.
"""
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the user is creator or admin then allow the upload
if not user_has_issue_permission(
request.user,
project_id=project_id,
issue=issue,
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value],
allow_creator=True,
):
return Response(
{"error": "You are not allowed to upload this attachment"},
status=status.HTTP_403_FORBIDDEN,
)
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
+1 -6
View File
@@ -908,14 +908,9 @@ class IssueLiteSerializer(DynamicBaseSerializer):
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True)
is_intake = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
"is_intake",
]
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
read_only_fields = fields
+15 -37
View File
@@ -3,7 +3,7 @@ import json
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery, Count
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -69,31 +69,25 @@ class IssueArchiveViewSet(BaseViewSet):
)
)
.annotate(
link_count=Subquery(
IssueLink.objects.filter(issue=OuterRef("id"))
.values("issue")
.annotate(count=Count("id"))
.values("count")
)
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=Subquery(
FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.values("issue_id")
.annotate(count=Count("id"))
.values("count")
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Subquery(
Issue.issue_objects.filter(parent=OuterRef("id"))
.values("parent")
.annotate(count=Count("id"))
.values("count")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
@@ -107,19 +101,6 @@ class IssueArchiveViewSet(BaseViewSet):
issue_queryset = self.get_queryset().filter(**filters)
total_issue_queryset = Issue.objects.filter(
deleted_at__isnull=True,
archived_at__isnull=False,
project_id=project_id,
workspace__slug=slug,
).filter(**filters)
total_issue_queryset = (
total_issue_queryset
if show_sub_issues == "true"
else total_issue_queryset.filter(parent__isnull=True)
)
issue_queryset = (
issue_queryset
if show_sub_issues == "true"
@@ -155,7 +136,6 @@ class IssueArchiveViewSet(BaseViewSet):
request=request,
order_by=order_by_param,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
@@ -190,7 +170,6 @@ class IssueArchiveViewSet(BaseViewSet):
request=request,
order_by=order_by_param,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
@@ -217,7 +196,6 @@ class IssueArchiveViewSet(BaseViewSet):
order_by=order_by_param,
request=request,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
+16 -41
View File
@@ -51,7 +51,6 @@ from plane.db.models import (
IssueRelation,
IssueAssignee,
IssueLabel,
IntakeIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -214,33 +213,27 @@ class IssueViewSet(BaseViewSet):
)
)
.annotate(
link_count=Subquery(
IssueLink.objects.filter(issue=OuterRef("id"))
.values("issue")
.annotate(count=Count("id"))
.values("count")
)
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=Subquery(
FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.values("issue_id")
.annotate(count=Count("id"))
.values("count")
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Subquery(
Issue.issue_objects.filter(parent=OuterRef("id"))
.values("parent")
.annotate(count=Count("id"))
.values("count")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
).distinct()
@method_decorator(gzip_page)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@@ -256,10 +249,6 @@ class IssueViewSet(BaseViewSet):
issue_queryset = self.get_queryset().filter(**filters, **extra_filters)
# Custom ordering for priority and state
total_issue_queryset = Issue.issue_objects.filter(
project_id=project_id, workspace__slug=slug
).filter(**filters, **extra_filters)
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset, order_by_param=order_by_param
@@ -292,7 +281,6 @@ class IssueViewSet(BaseViewSet):
and not project.guest_view_all_features
):
issue_queryset = issue_queryset.filter(created_by=request.user)
total_issue_queryset = total_issue_queryset.filter(created_by=request.user)
if group_by:
if sub_group_by:
@@ -308,7 +296,6 @@ class IssueViewSet(BaseViewSet):
request=request,
order_by=order_by_param,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
@@ -342,7 +329,6 @@ class IssueViewSet(BaseViewSet):
request=request,
order_by=order_by_param,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
@@ -368,7 +354,6 @@ class IssueViewSet(BaseViewSet):
order_by=order_by_param,
request=request,
queryset=issue_queryset,
total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
@@ -1224,7 +1209,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
# Fetch the issue
issue = (
Issue.objects.filter(project_id=project.id)
Issue.issue_objects.filter(project_id=project.id)
.filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
@@ -1316,16 +1301,6 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
)
)
)
.annotate(
is_intake=Exists(
IntakeIssue.objects.filter(
issue=OuterRef("id"),
status__in=[-2, 0],
workspace__slug=slug,
project_id=project.id,
)
)
)
).first()
# Check if the issue exists
+1 -2
View File
@@ -59,10 +59,9 @@ class IssueSearchEndpoint(BaseAPIView):
)
related_issue_ids = [item for sublist in related_issue_ids for item in sublist]
related_issue_ids.append(issue_id)
if issue:
issues = issues.exclude(pk__in=related_issue_ids)
issues = issues.filter(~Q(pk=issue_id), ~Q(pk__in=related_issue_ids))
return issues
+15
View File
@@ -0,0 +1,15 @@
from django.utils import timezone
from datetime import timedelta
from plane.db.models import APIActivityLog
from celery import shared_task
@shared_task
def delete_api_logs():
# Get the logs older than 30 days to delete
logs_to_delete = APIActivityLog.objects.filter(
created_at__lte=timezone.now() - timedelta(days=30)
)
# Delete the logs
logs_to_delete._raw_delete(logs_to_delete.db)
-423
View File
@@ -1,423 +0,0 @@
# Python imports
from datetime import timedelta
import logging
from typing import List, Dict, Any, Callable, Optional
import os
# Django imports
from django.utils import timezone
from django.db.models import F, Window, Subquery
from django.db.models.functions import RowNumber
# Third party imports
from celery import shared_task
from pymongo.errors import BulkWriteError
from pymongo.collection import Collection
from pymongo.operations import InsertOne
# Module imports
from plane.db.models import (
EmailNotificationLog,
PageVersion,
APIActivityLog,
IssueDescriptionVersion,
)
from plane.settings.mongo import MongoConnection
from plane.utils.exception_logger import log_exception
logger = logging.getLogger("plane.worker")
BATCH_SIZE = 1000
def get_mongo_collection(collection_name: str) -> Optional[Collection]:
"""Get MongoDB collection if available, otherwise return None."""
if not MongoConnection.is_configured():
logger.info("MongoDB not configured")
return None
try:
mongo_collection = MongoConnection.get_collection(collection_name)
logger.info(f"MongoDB collection '{collection_name}' connected successfully")
return mongo_collection
except Exception as e:
logger.error(f"Failed to get MongoDB collection: {str(e)}")
log_exception(e)
return None
def flush_to_mongo_and_delete(
mongo_collection: Optional[Collection],
buffer: List[Dict[str, Any]],
ids_to_delete: List[int],
model,
mongo_available: bool,
) -> None:
"""
Inserts a batch of records into MongoDB and deletes the corresponding rows from PostgreSQL.
"""
if not buffer:
logger.debug("No records to flush - buffer is empty")
return
logger.info(
f"Starting batch flush: {len(buffer)} records, {len(ids_to_delete)} IDs to delete"
)
mongo_archival_failed = False
# Try to insert into MongoDB if available
if mongo_collection and mongo_available:
try:
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
except BulkWriteError as bwe:
logger.error(f"MongoDB bulk write error: {str(bwe)}")
log_exception(bwe)
mongo_archival_failed = True
# If MongoDB is available and archival failed, log the error and return
if mongo_available and mongo_archival_failed:
logger.error(f"MongoDB archival failed for {len(buffer)} records")
return
# Delete from PostgreSQL - delete() returns (count, {model: count})
delete_result = model.all_objects.filter(id__in=ids_to_delete).delete()
deleted_count = (
delete_result[0] if delete_result and isinstance(delete_result, tuple) else 0
)
logger.info(f"Batch flush completed: {deleted_count} records deleted")
def process_cleanup_task(
queryset_func: Callable,
transform_func: Callable[[Dict], Dict],
model,
task_name: str,
collection_name: str,
):
"""
Generic function to process cleanup tasks.
Args:
queryset_func: Function that returns the queryset to process
transform_func: Function to transform each record for MongoDB
model: Django model class
task_name: Name of the task for logging
collection_name: MongoDB collection name
"""
logger.info(f"Starting {task_name} cleanup task")
# Get MongoDB collection
mongo_collection = get_mongo_collection(collection_name)
mongo_available = mongo_collection is not None
# Get queryset
queryset = queryset_func()
# Process records in batches
buffer: List[Dict[str, Any]] = []
ids_to_delete: List[int] = []
total_processed = 0
total_batches = 0
for record in queryset:
# Transform record for MongoDB
buffer.append(transform_func(record))
ids_to_delete.append(record["id"])
# Flush batch when it reaches BATCH_SIZE
if len(buffer) >= BATCH_SIZE:
total_batches += 1
flush_to_mongo_and_delete(
mongo_collection=mongo_collection,
buffer=buffer,
ids_to_delete=ids_to_delete,
model=model,
mongo_available=mongo_available,
)
total_processed += len(buffer)
buffer.clear()
ids_to_delete.clear()
# Process final batch if any records remain
if buffer:
total_batches += 1
flush_to_mongo_and_delete(
mongo_collection=mongo_collection,
buffer=buffer,
ids_to_delete=ids_to_delete,
model=model,
mongo_available=mongo_available,
)
total_processed += len(buffer)
logger.info(
f"{task_name} cleanup task completed",
extra={
"total_records_processed": total_processed,
"total_batches": total_batches,
"mongo_available": mongo_available,
"collection_name": collection_name,
},
)
# Transform functions for each model
def transform_api_log(record: Dict) -> Dict:
"""Transform API activity log record."""
return {
"id": record["id"],
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"token_identifier": record["token_identifier"],
"path": record["path"],
"method": record["method"],
"query_params": record.get("query_params"),
"headers": record.get("headers"),
"body": record.get("body"),
"response_code": record["response_code"],
"response_body": record["response_body"],
"ip_address": record["ip_address"],
"user_agent": record["user_agent"],
"created_by_id": record["created_by_id"],
}
def transform_email_log(record: Dict) -> Dict:
"""Transform email notification log record."""
return {
"id": record["id"],
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"receiver_id": record["receiver_id"],
"triggered_by_id": record["triggered_by_id"],
"entity_identifier": record["entity_identifier"],
"entity_name": record["entity_name"],
"data": record["data"],
"processed_at": (
str(record["processed_at"]) if record.get("processed_at") else None
),
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
"entity": record["entity"],
"old_value": record["old_value"],
"new_value": record["new_value"],
"created_by_id": record["created_by_id"],
}
def transform_page_version(record: Dict) -> Dict:
"""Transform page version record."""
return {
"id": record["id"],
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"page_id": record["page_id"],
"workspace_id": record["workspace_id"],
"owned_by_id": record["owned_by_id"],
"description_html": record["description_html"],
"description_binary": record["description_binary"],
"description_stripped": record["description_stripped"],
"description_json": record["description_json"],
"sub_pages_data": record["sub_pages_data"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
),
}
def transform_issue_description_version(record: Dict) -> Dict:
"""Transform issue description version record."""
return {
"id": record["id"],
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"issue_id": record["issue_id"],
"workspace_id": record["workspace_id"],
"project_id": record["project_id"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"owned_by_id": record["owned_by_id"],
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
),
"description_binary": record["description_binary"],
"description_html": record["description_html"],
"description_stripped": record["description_stripped"],
"description_json": record["description_json"],
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
}
# Queryset functions for each cleanup task
def get_api_logs_queryset():
"""Get API logs older than cutoff days."""
cutoff_days = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 30))
cutoff_time = timezone.now() - timedelta(days=cutoff_days)
logger.info(f"API logs cutoff time: {cutoff_time}")
return (
APIActivityLog.all_objects.filter(created_at__lte=cutoff_time)
.values(
"id",
"created_at",
"token_identifier",
"path",
"method",
"query_params",
"headers",
"body",
"response_code",
"response_body",
"ip_address",
"user_agent",
"created_by_id",
)
.iterator(chunk_size=BATCH_SIZE)
)
def get_email_logs_queryset():
"""Get email logs older than cutoff days."""
cutoff_days = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 30))
cutoff_time = timezone.now() - timedelta(days=cutoff_days)
logger.info(f"Email logs cutoff time: {cutoff_time}")
return (
EmailNotificationLog.all_objects.filter(sent_at__lte=cutoff_time)
.values(
"id",
"created_at",
"receiver_id",
"triggered_by_id",
"entity_identifier",
"entity_name",
"data",
"processed_at",
"sent_at",
"entity",
"old_value",
"new_value",
"created_by_id",
)
.iterator(chunk_size=BATCH_SIZE)
)
def get_page_versions_queryset():
"""Get page versions beyond the maximum allowed (20 per page)."""
subq = (
PageVersion.all_objects.annotate(
row_num=Window(
expression=RowNumber(),
partition_by=[F("page_id")],
order_by=F("created_at").desc(),
)
)
.filter(row_num__gt=20)
.values("id")
)
return (
PageVersion.all_objects.filter(id__in=Subquery(subq))
.values(
"id",
"created_at",
"page_id",
"workspace_id",
"owned_by_id",
"description_html",
"description_binary",
"description_stripped",
"description_json",
"sub_pages_data",
"created_by_id",
"updated_by_id",
"deleted_at",
"last_saved_at",
)
.iterator(chunk_size=BATCH_SIZE)
)
def get_issue_description_versions_queryset():
"""Get issue description versions beyond the maximum allowed (20 per issue)."""
subq = (
IssueDescriptionVersion.all_objects.annotate(
row_num=Window(
expression=RowNumber(),
partition_by=[F("issue_id")],
order_by=F("created_at").desc(),
)
)
.filter(row_num__gt=20)
.values("id")
)
return (
IssueDescriptionVersion.all_objects.filter(id__in=Subquery(subq))
.values(
"id",
"created_at",
"issue_id",
"workspace_id",
"project_id",
"created_by_id",
"updated_by_id",
"owned_by_id",
"last_saved_at",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"deleted_at",
)
.iterator(chunk_size=BATCH_SIZE)
)
# Celery tasks - now much simpler!
@shared_task
def delete_api_logs():
"""Delete old API activity logs."""
process_cleanup_task(
queryset_func=get_api_logs_queryset,
transform_func=transform_api_log,
model=APIActivityLog,
task_name="API Activity Log",
collection_name="api_activity_logs",
)
@shared_task
def delete_email_notification_logs():
"""Delete old email notification logs."""
process_cleanup_task(
queryset_func=get_email_logs_queryset,
transform_func=transform_email_log,
model=EmailNotificationLog,
task_name="Email Notification Log",
collection_name="email_notification_logs",
)
@shared_task
def delete_page_versions():
"""Delete excess page versions."""
process_cleanup_task(
queryset_func=get_page_versions_queryset,
transform_func=transform_page_version,
model=PageVersion,
task_name="Page Version",
collection_name="page_versions",
)
@shared_task
def delete_issue_description_versions():
"""Delete excess issue description versions."""
process_cleanup_task(
queryset_func=get_issue_description_versions_queryset,
transform_func=transform_issue_description_version,
model=IssueDescriptionVersion,
task_name="Issue Description Version",
collection_name="issue_description_versions",
)
+1 -13
View File
@@ -50,21 +50,9 @@ app.conf.beat_schedule = {
"schedule": crontab(hour=2, minute=0), # UTC 02:00
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.cleanup_task.delete_api_logs",
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=2, minute=30), # UTC 02:30
},
"check-every-day-to-delete-email-notification-logs": {
"task": "plane.bgtasks.cleanup_task.delete_email_notification_logs",
"schedule": crontab(hour=3, minute=0), # UTC 03:00
},
"check-every-day-to-delete-page-versions": {
"task": "plane.bgtasks.cleanup_task.delete_page_versions",
"schedule": crontab(hour=3, minute=30), # UTC 03:30
},
"check-every-day-to-delete-issue-description-versions": {
"task": "plane.bgtasks.cleanup_task.delete_issue_description_versions",
"schedule": crontab(hour=4, minute=0), # UTC 04:00
},
}
@@ -1,182 +0,0 @@
# Generated by Django 4.2.21 on 2025-08-19 11:52
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0100_profile_has_marketing_email_consent_and_more"),
]
operations = [
migrations.CreateModel(
name="Description",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"deleted_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Deleted At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("description_json", models.JSONField(blank=True, default=dict)),
("description_html", models.TextField(blank=True, default="<p></p>")),
("description_binary", models.BinaryField(null=True)),
("description_stripped", models.TextField(blank=True, null=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Description",
"verbose_name_plural": "Descriptions",
"db_table": "descriptions",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="DescriptionVersion",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"deleted_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Deleted At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("description_json", models.JSONField(blank=True, default=dict)),
("description_html", models.TextField(blank=True, default="<p></p>")),
("description_binary", models.BinaryField(null=True)),
("description_stripped", models.TextField(blank=True, null=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"description",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="db.description",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Description Version",
"verbose_name_plural": "Description Versions",
"db_table": "description_versions",
"ordering": ("-created_at",),
},
),
]
-2
View File
@@ -83,5 +83,3 @@ from .label import Label
from .device import Device, DeviceSession
from .sticky import Sticky
from .description import Description, DescriptionVersion
-56
View File
@@ -1,56 +0,0 @@
from django.db import models
from django.utils.html import strip_tags
from .workspace import WorkspaceBaseModel
class Description(WorkspaceBaseModel):
description_json = models.JSONField(default=dict, blank=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_binary = models.BinaryField(null=True)
description_stripped = models.TextField(blank=True, null=True)
class Meta:
verbose_name = "Description"
verbose_name_plural = "Descriptions"
db_table = "descriptions"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(Description, self).save(*args, **kwargs)
class DescriptionVersion(WorkspaceBaseModel):
"""
DescriptionVersion is a model used to store historical versions of a Description.
"""
description = models.ForeignKey(
"db.Description", on_delete=models.CASCADE, related_name="versions"
)
description_json = models.JSONField(default=dict, blank=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_binary = models.BinaryField(null=True)
description_stripped = models.TextField(blank=True, null=True)
class Meta:
verbose_name = "Description Version"
verbose_name_plural = "Description Versions"
db_table = "description_versions"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(DescriptionVersion, self).save(*args, **kwargs)
@@ -2,12 +2,11 @@
import json
import secrets
import os
import requests
# Django imports
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from django.conf import settings
# Module imports
from plane.license.models import Instance, InstanceEdition
@@ -21,38 +20,21 @@ class Command(BaseCommand):
# Positional argument
parser.add_argument("machine_signature", type=str, help="Machine signature")
def check_for_current_version(self):
if os.environ.get("APP_VERSION", False):
return os.environ.get("APP_VERSION")
def read_package_json(self):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
try:
with open("package.json", "r") as file:
data = json.load(file)
return data.get("version", "v0.1.0")
except Exception:
self.stdout.write("Error checking for current version")
return "v0.1.0"
def check_for_latest_version(self, fallback_version):
try:
response = requests.get(
"https://api.github.com/repos/makeplane/plane/releases/latest",
timeout=10,
)
response.raise_for_status()
data = response.json()
return data.get("tag_name", fallback_version)
except Exception:
self.stdout.write("Error checking for latest version")
return fallback_version
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
}
return payload
def handle(self, *args, **options):
# Check if the instance is registered
instance = Instance.objects.first()
current_version = self.check_for_current_version()
latest_version = self.check_for_latest_version(current_version)
# If instance is None then register this instance
if instance is None:
machine_signature = options.get("machine_signature", "machine-signature")
@@ -60,11 +42,13 @@ class Command(BaseCommand):
if not machine_signature:
raise CommandError("Machine signature is required")
payload = self.read_package_json()
instance = Instance.objects.create(
instance_name="Plane Community Edition",
instance_id=secrets.token_hex(12),
current_version=current_version,
latest_version=latest_version,
current_version=payload.get("version"),
latest_version=payload.get("version"),
last_checked_at=timezone.now(),
is_test=os.environ.get("IS_TEST", "0") == "1",
edition=InstanceEdition.PLANE_COMMUNITY.value,
@@ -73,11 +57,11 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("Instance registered"))
else:
self.stdout.write(self.style.SUCCESS("Instance already registered"))
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.current_version = current_version
instance.latest_version = latest_version
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
instance.edition = InstanceEdition.PLANE_COMMUNITY.value
instance.save()
+7 -1
View File
@@ -284,7 +284,7 @@ CELERY_IMPORTS = (
"plane.bgtasks.exporter_expired_task",
"plane.bgtasks.file_asset_task",
"plane.bgtasks.email_notification_task",
"plane.bgtasks.cleanup_task",
"plane.bgtasks.api_logs_task",
"plane.license.bgtasks.tracer",
# management tasks
"plane.bgtasks.dummy_data_task",
@@ -304,10 +304,16 @@ GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
# Posthog settings
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# instance key
INSTANCE_KEY = os.environ.get(
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
)
# Skip environment variable configuration
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
-5
View File
@@ -73,10 +73,5 @@ LOGGING = {
"handlers": ["console"],
"propagate": False,
},
"plane.mongo": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}
-121
View File
@@ -1,121 +0,0 @@
# Django imports
from django.conf import settings
import logging
# Third party imports
from pymongo import MongoClient
from pymongo.database import Database
from pymongo.collection import Collection
from typing import Optional, TypeVar, Type
T = TypeVar("T", bound="MongoConnection")
# Set up logger
logger = logging.getLogger("plane.mongo")
class MongoConnection:
"""
A singleton class that manages MongoDB connections.
This class ensures only one MongoDB connection is maintained throughout the application.
It provides methods to access the MongoDB client, database, and collections.
Attributes:
_instance (Optional[MongoConnection]): The singleton instance of this class
_client (Optional[MongoClient]): The MongoDB client instance
_db (Optional[Database]): The MongoDB database instance
"""
_instance: Optional["MongoConnection"] = None
_client: Optional[MongoClient] = None
_db: Optional[Database] = None
def __new__(cls: Type[T]) -> T:
"""
Creates a new instance of MongoConnection if one doesn't exist.
Returns:
MongoConnection: The singleton instance
"""
if cls._instance is None:
cls._instance = super(MongoConnection, cls).__new__(cls)
try:
mongo_url = getattr(settings, "MONGO_DB_URL", None)
mongo_db_database = getattr(settings, "MONGO_DB_DATABASE", None)
if not mongo_url or not mongo_db_database:
logger.warning(
"MongoDB connection parameters not configured. MongoDB functionality will be disabled."
)
return cls._instance
cls._client = MongoClient(mongo_url)
cls._db = cls._client[mongo_db_database]
# Test the connection
cls._client.server_info()
logger.info("MongoDB connection established successfully")
except Exception as e:
logger.warning(
f"Failed to initialize MongoDB connection: {str(e)}. MongoDB functionality will be disabled."
)
return cls._instance
@classmethod
def get_client(cls) -> Optional[MongoClient]:
"""
Returns the MongoDB client instance.
Returns:
Optional[MongoClient]: The MongoDB client instance or None if not configured
"""
if cls._client is None:
cls._instance = cls()
return cls._client
@classmethod
def get_db(cls) -> Optional[Database]:
"""
Returns the MongoDB database instance.
Returns:
Optional[Database]: The MongoDB database instance or None if not configured
"""
if cls._db is None:
cls._instance = cls()
return cls._db
@classmethod
def get_collection(cls, collection_name: str) -> Optional[Collection]:
"""
Returns a MongoDB collection by name.
Args:
collection_name (str): The name of the collection to retrieve
Returns:
Optional[Collection]: The MongoDB collection instance or None if not configured
"""
try:
db = cls.get_db()
if db is None:
logger.warning(
f"Cannot access collection '{collection_name}': MongoDB not configured"
)
return None
return db[collection_name]
except Exception as e:
logger.warning(f"Failed to access collection '{collection_name}': {str(e)}")
return None
@classmethod
def is_configured(cls) -> bool:
"""
Check if MongoDB is properly configured and connected.
Returns:
bool: True if MongoDB is configured and connected, False otherwise
"""
return cls._client is not None and cls._db is not None
-5
View File
@@ -83,10 +83,5 @@ LOGGING = {
"handlers": ["console"],
"propagate": False,
},
"plane.mongo": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}
+18
View File
@@ -187,6 +187,24 @@ def validate_html_content(html_content):
f"HTML content contains dangerous JavaScript in event handler: {handler_content[:100]}",
)
# Basic HTML structure validation - check for common malformed tags
try:
# Count opening and closing tags for basic structure validation
opening_tags = re.findall(r"<(\w+)[^>]*>", html_content)
closing_tags = re.findall(r"</(\w+)>", html_content)
# Filter out self-closing tags from opening tags
opening_tags_filtered = [
tag for tag in opening_tags if tag.lower() not in SELF_CLOSING_TAGS
]
# Basic check - if we have significantly more opening than closing tags, it might be malformed
if len(opening_tags_filtered) > len(closing_tags) + 10: # Allow some tolerance
return False, "HTML content appears to be malformed (unmatched tags)"
except Exception:
# If HTML parsing fails, we'll allow it
pass
return True, None
+1 -1
View File
@@ -160,7 +160,7 @@ class OffsetPaginator:
total_count = (
self.total_count_queryset.count()
if self.total_count_queryset
else queryset.count()
else results.count()
)
# Check if there are more results available after the current page
+1 -3
View File
@@ -9,8 +9,6 @@ psycopg==3.1.18
psycopg-binary==3.1.18
psycopg-c==3.1.18
dj-database-url==2.1.0
# mongo
pymongo==4.6.3
# redis
redis==5.0.4
django-redis==5.4.0
@@ -68,4 +66,4 @@ opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
# OpenAPI Specification
drf-spectacular==0.28.0
drf-spectacular==0.28.0
+3 -3
View File
@@ -4,12 +4,12 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY . .
RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install
RUN yarn global add turbo
RUN yarn install
EXPOSE 3003
ENV TURBO_TELEMETRY_DISABLED=1
VOLUME [ "/app/node_modules", "/app/live/node_modules"]
CMD ["pnpm","dev", "--filter=live"]
CMD ["yarn","dev", "--filter=live"]
+7 -17
View File
@@ -1,11 +1,5 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
# Setup pnpm package manager with corepack and configure global bin directory for caching
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# *****************************************************************************
# STAGE 1: Prune the project
# *****************************************************************************
@@ -15,10 +9,9 @@ RUN apk update
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ARG TURBO_VERSION=2.5.6
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=live --docker
RUN turbo prune live --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
@@ -32,18 +25,16 @@ WORKDIR /app
# First install dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project and its dependencies
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ENV TURBO_TELEMETRY_DISABLED=1
RUN pnpm turbo run build --filter=live
RUN yarn turbo build --filter=live
# *****************************************************************************
# STAGE 3: Run the project
@@ -53,12 +44,11 @@ FROM base AS runner
WORKDIR /app
COPY --from=installer /app/packages ./packages
COPY --from=installer /app/apps/live/dist ./apps/live/dist
COPY --from=installer /app/apps/live/node_modules ./apps/live/node_modules
COPY --from=installer /app/apps/live/dist ./live
COPY --from=installer /app/node_modules ./node_modules
ENV TURBO_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["node", "apps/live/dist/server.js"]
CMD ["node", "live/server.js"]
+5 -5
View File
@@ -24,8 +24,8 @@
"@hocuspocus/extension-logger": "^2.15.0",
"@hocuspocus/extension-redis": "^2.15.0",
"@hocuspocus/server": "^2.15.0",
"@plane/editor": "workspace:*",
"@plane/types": "workspace:*",
"@plane/editor": "*",
"@plane/types": "*",
"@tiptap/core": "^2.22.3",
"@tiptap/html": "^2.22.3",
"axios": "1.11.0",
@@ -46,15 +46,15 @@
"yjs": "^13.6.20"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@plane/eslint-config": "*",
"@plane/typescript-config": "*",
"@types/compression": "1.8.1",
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.23",
"@types/express-ws": "^3.0.5",
"@types/node": "^20.14.9",
"@types/pino-http": "^5.8.4",
"@types/uuid": "^9.0.1",
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
+2 -1
View File
@@ -1,4 +1,5 @@
import { pinoHttp } from "pino-http";
import { Logger } from "pino";
const transport = {
target: "pino-pretty",
@@ -36,4 +37,4 @@ export const logger = pinoHttp({
},
});
export const manualLogger: typeof logger.logger = logger.logger;
export const manualLogger: Logger = logger.logger;
@@ -14,7 +14,6 @@ export abstract class APIService {
this.axiosInstance = axios.create({
baseURL,
withCredentials: true,
timeout: 20000,
});
}
+2
View File
@@ -21,6 +21,8 @@
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
+2 -3
View File
@@ -2,6 +2,5 @@
.vercel
.tubro
out/
dist/
build/
node_modules/
dis/
build/
+5 -8
View File
@@ -1,19 +1,16 @@
FROM node:22-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
COPY . .
RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install
RUN yarn global add turbo
RUN yarn install
EXPOSE 3002
EXPOSE 4000
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]
CMD ["pnpm", "dev", "--filter=space"]
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
CMD ["yarn","dev", "--filter=space"]
+5 -14
View File
@@ -1,11 +1,5 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
# Setup pnpm package manager with corepack and configure global bin directory for caching
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
@@ -13,8 +7,7 @@ FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG TURBO_VERSION=2.5.6
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=space --docker
@@ -29,13 +22,11 @@ WORKDIR /app
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install --network-timeout 500000
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -58,7 +49,7 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1
RUN pnpm turbo run build --filter=space
RUN yarn turbo run build --filter=space
# *****************************************************************************
# STAGE 3: Copy the project and start it
@@ -100,4 +91,4 @@ ENV TURBO_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["node", "apps/space/server.js"]
CMD ["node", "apps/space/server.js"]
@@ -3,13 +3,11 @@
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PoweredBy } from "@/components/common/powered-by";
import { LogoSpinner, PoweredBy } from "@/components/common";
import { IssuesNavbarRoot } from "@/components/issues";
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
import { IssuesNavbarRoot } from "@/components/issues/navbar";
// hooks
import { usePublish, usePublishList } from "@/hooks/store/publish";
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
type Props = {
children: React.ReactNode;
+2 -4
View File
@@ -4,11 +4,9 @@ import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
// components
import { IssuesLayoutsRoot } from "@/components/issues/issue-layouts";
import { IssuesLayoutsRoot } from "@/components/issues";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useLabel } from "@/hooks/store/use-label";
import { useStates } from "@/hooks/store/use-state";
import { usePublish, useLabel, useStates } from "@/hooks/store";
type Props = {
params: {
+3 -3
View File
@@ -2,11 +2,11 @@
import { observer } from "mobx-react";
// components
import { UserLoggedIn } from "@/components/account/user-logged-in";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { UserLoggedIn } from "@/components/account";
import { LogoSpinner } from "@/components/common";
import { AuthView } from "@/components/views";
// hooks
import { useUser } from "@/hooks/store/use-user";
import { useUser } from "@/hooks/store";
const HomePage = observer(() => {
const { data: currentUser, isAuthenticated, isInitializing } = useUser();
+7 -10
View File
@@ -1,7 +1,6 @@
"use client";
import { FC, ReactNode } from "react";
import { ThemeProvider } from "next-themes";
// components
import { TranslationProvider } from "@plane/i18n";
import { InstanceProvider } from "@/lib/instance-provider";
@@ -16,14 +15,12 @@ export const AppProvider: FC<IAppProvider> = (props) => {
const { children } = props;
return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<StoreProvider>
<TranslationProvider>
<ToastProvider>
<InstanceProvider>{children}</InstanceProvider>
</ToastProvider>
</TranslationProvider>
</StoreProvider>
</ThemeProvider>
<StoreProvider>
<TranslationProvider>
<ToastProvider>
<InstanceProvider>{children}</InstanceProvider>
</ToastProvider>
</TranslationProvider>
</StoreProvider>
);
};
+4 -5
View File
@@ -3,11 +3,10 @@
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PoweredBy } from "@/components/common/powered-by";
import { LogoSpinner, PoweredBy } from "@/components/common";
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
// hooks
import { usePublish, usePublishList } from "@/hooks/store/publish";
import { usePublish, usePublishList } from "@/hooks/store";
// Plane web
import { ViewNavbarRoot } from "@/plane-web/components/navbar";
import { useView } from "@/plane-web/hooks/store";
@@ -19,7 +18,7 @@ type Props = {
};
};
const ViewsLayout = observer((props: Props) => {
const IssuesLayout = observer((props: Props) => {
const { children, params } = props;
// params
const { anchor } = params;
@@ -62,4 +61,4 @@ const ViewsLayout = observer((props: Props) => {
);
});
export default ViewsLayout;
export default IssuesLayout;
+4 -4
View File
@@ -3,9 +3,9 @@
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
// components
import { PoweredBy } from "@/components/common/powered-by";
import { PoweredBy } from "@/components/common";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { usePublish } from "@/hooks/store";
// plane-web
import { ViewLayoutsRoot } from "@/plane-web/components/issue-layouts/root";
@@ -15,7 +15,7 @@ type Props = {
};
};
const ViewsPage = observer((props: Props) => {
const IssuesPage = observer((props: Props) => {
const { params } = props;
const { anchor } = params;
// params
@@ -34,4 +34,4 @@ const ViewsPage = observer((props: Props) => {
);
});
export default ViewsPage;
export default IssuesPage;
@@ -1,9 +1,10 @@
import { PageNotFound } from "@/components/ui/not-found";
import type { PublishStore } from "@/store/publish/publish.store";
import { PublishStore } from "@/store/publish/publish.store";
type Props = {
peekId: string | undefined;
publishSettings: PublishStore;
};
export const ViewLayoutsRoot = (_props: Props) => <PageNotFound />;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const ViewLayoutsRoot = (props: Props) => <PageNotFound />;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PublishStore } from "@/store/publish/publish.store";
import { PublishStore } from "@/store/publish/publish.store";
type Props = {
publishSettings: PublishStore;
@@ -11,6 +11,14 @@ import { SitesAuthService } from "@plane/services";
import { IEmailCheckData } from "@plane/types";
import { OAuthOptions } from "@plane/ui";
// components
import {
AuthHeader,
AuthBanner,
AuthEmailForm,
AuthUniqueCodeForm,
AuthPasswordForm,
TermsAndConditions,
} from "@/components/account";
// helpers
import {
EAuthenticationErrorCodes,
@@ -19,7 +27,7 @@ import {
authErrorHandler,
} from "@/helpers/authentication.helper";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useInstance } from "@/hooks/store";
// types
import { EAuthModes, EAuthSteps } from "@/types/auth";
// assets
@@ -27,13 +35,6 @@ import GithubLightLogo from "/public/logos/github-black.png";
import GithubDarkLogo from "/public/logos/github-dark.svg";
import GitlabLogo from "/public/logos/gitlab-logo.svg";
import GoogleLogo from "/public/logos/google-logo.svg";
// local imports
import { TermsAndConditions } from "../terms-and-conditions";
import { AuthBanner } from "./auth-banner";
import { AuthHeader } from "./auth-header";
import { AuthEmailForm } from "./email";
import { AuthPasswordForm } from "./password";
import { AuthUniqueCodeForm } from "./unique-code";
const authService = new SitesAuthService();
@@ -1 +1,8 @@
export * from "./auth-root";
export * from "./auth-header";
export * from "./auth-banner";
export * from "./email";
export * from "./password";
export * from "./unique-code";
@@ -0,0 +1,3 @@
export * from "./auth-forms";
export * from "./terms-and-conditions";
export * from "./user-logged-in";
@@ -4,10 +4,10 @@ import { observer } from "mobx-react";
import Image from "next/image";
import { PlaneLockup } from "@plane/ui";
// components
import { PoweredBy } from "@/components/common/powered-by";
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
import { PoweredBy } from "@/components/common";
import { UserAvatar } from "@/components/issues";
// hooks
import { useUser } from "@/hooks/store/use-user";
import { useUser } from "@/hooks/store";
// assets
import UserLoggedInImage from "@/public/user-logged-in.svg";
@@ -0,0 +1,3 @@
export * from "./project-logo";
export * from "./logo-spinner";
export * from "./powered-by";
@@ -0,0 +1 @@
export * from "./mentions";
@@ -2,8 +2,7 @@ import { observer } from "mobx-react";
// helpers
import { cn } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser } from "@/hooks/store/use-user";
import { useMember, useUser } from "@/hooks/store";
type Props = {
id: string;
@@ -0,0 +1,4 @@
export * from "./embeds";
export * from "./lite-text-editor";
export * from "./rich-text-editor";
export * from "./toolbar";
@@ -3,13 +3,12 @@ import React from "react";
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
import type { MakeOptional } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
import { isCommentEmpty } from "@/helpers/string.helper";
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
// local imports
import { EditorMentionsRoot } from "./embeds/mentions";
import { IssueCommentToolbar } from "./toolbar";
type LiteTextEditorWrapperProps = MakeOptional<
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
@@ -1,15 +1,14 @@
import React, { forwardRef } from "react";
// plane imports
import { type EditorRefApi, type IRichTextEditorProps, RichTextEditorWithRef, type TFileHandler } from "@plane/editor";
import type { MakeOptional } from "@plane/types";
import { useEditorFlagging } from "ce/hooks/use-editor-flagging";
import { EditorRefApi, IRichTextEditorProps, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMember } from "@/hooks/store/use-member";
// plane web imports
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
// local imports
import { EditorMentionsRoot } from "./embeds/mentions";
// store hooks
import { useMember } from "@/hooks/store";
type RichTextEditorWrapperProps = MakeOptional<
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler">,
@@ -2,7 +2,7 @@
import React, { useEffect, useState, useCallback } from "react";
// plane imports
import { TOOLBAR_ITEMS, type ToolbarMenuItem, type EditorRefApi } from "@plane/editor";
import { TOOLBAR_ITEMS, ToolbarMenuItem, EditorRefApi } from "@plane/editor";
import { Button, Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
@@ -0,0 +1 @@
export * from "./instance-failure-view";
@@ -5,7 +5,7 @@ import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
// hooks
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssueFilter } from "@/hooks/store";
// store
import type { TIssueLayout, TIssueQueryFilters } from "@/types/issue";
// components
@@ -6,7 +6,7 @@ import { X } from "lucide-react";
import { EIconSize } from "@plane/constants";
import { StateGroupIcon } from "@plane/ui";
// hooks
import { useStates } from "@/hooks/store/use-state";
import { useStates } from "@/hooks/store";
type Props = {
handleRemove: (val: string) => void;
@@ -0,0 +1,3 @@
export * from "./dropdown";
export * from "./filter-header";
export * from "./filter-option";
@@ -1 +1,11 @@
// filters
export * from "./root";
export * from "./selection";
// properties
export * from "./state";
export * from "./priority";
export * from "./labels";
// helpers
export * from "./helpers";
@@ -1,13 +1,11 @@
"use client";
import React, { useState } from "react";
// plane imports
import { Loader } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
// types
import type { IIssueLabel } from "@/types/issue";
// local imports
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
import { IIssueLabel } from "@/types/issue";
const LabelIcons = ({ color }: { color: string }) => (
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />
@@ -2,13 +2,13 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { PriorityIcon } from "@plane/ui";
// local imports
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
// components
import { FilterHeader, FilterOption } from "./helpers";
// constants
type Props = {
appliedFilters: string[] | null;
@@ -12,7 +12,7 @@ import { FilterSelection } from "@/components/issues/filters/selection";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssueFilter } from "@/hooks/store";
// types
import { TIssueQueryFilters } from "@/types/issue";
@@ -4,10 +4,9 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { Search, X } from "lucide-react";
// types
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
// local imports
import { FilterPriority } from "./priority";
import { FilterState } from "./state";
import { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
// components
import { FilterPriority, FilterState } from ".";
type Props = {
filters: IIssueFilterOptions;
@@ -5,11 +5,10 @@ import { observer } from "mobx-react";
// ui
import { EIconSize } from "@plane/constants";
import { Loader, StateGroupIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
// hooks
import { useStates } from "@/hooks/store/use-state";
// local imports
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
import { useStates } from "@/hooks/store";
type Props = {
appliedFilters: string[] | null;
@@ -0,0 +1,2 @@
export * from "./issue-layouts";
export * from "./navbar";
@@ -1 +1,4 @@
export * from "./kanban/base-kanban-root";
export * from "./list/base-list-root";
export * from "./properties";
export * from "./root";
@@ -1,8 +1,6 @@
import { observer } from "mobx-react";
// plane imports
import type { TLoader } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { TLoader } from "@plane/types";
import { LogoSpinner } from "@/components/common";
interface Props {
children: string | React.ReactNode | React.ReactNode[];
@@ -8,7 +8,7 @@ import { IIssueDisplayProperties } from "@plane/types";
// components
import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC";
// hooks
import { useIssue } from "@/hooks/store/use-issue";
import { useIssue } from "@/hooks/store";
import { KanBan } from "./default";
@@ -3,10 +3,9 @@ import { useParams } from "next/navigation";
// plane utils
import { cn } from "@plane/utils";
// components
import { IssueEmojiReactions } from "@/components/issues/reactions/issue-emoji-reactions";
import { IssueVotes } from "@/components/issues/reactions/issue-vote-reactions";
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { usePublish } from "@/hooks/store";
type Props = {
issueId: string;
@@ -15,8 +15,7 @@ import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useIssueDetails, usePublish } from "@/hooks/store";
//
import { IIssue } from "@/types/issue";
import { IssueProperties } from "../properties/all-properties";
@@ -13,11 +13,7 @@ import {
TLoader,
} from "@plane/types";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useModule } from "@/hooks/store/use-module";
import { useStates } from "@/hooks/store/use-state";
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
//
import { getGroupByColumns } from "../utils";
// components
@@ -0,0 +1,2 @@
export * from "./block";
export * from "./blocks-list";
@@ -3,7 +3,7 @@
import { MutableRefObject, forwardRef, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
//types
import type {
import {
TGroupedIssues,
IIssueDisplayProperties,
TSubGroupedIssues,
@@ -14,8 +14,8 @@ import type {
import { cn } from "@plane/utils";
// hooks
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
// local imports
import { KanbanIssueBlocksList } from "./blocks-list";
//
import { KanbanIssueBlocksList } from ".";
interface IKanbanGroup {
groupId: string;
@@ -13,11 +13,7 @@ import {
TLoader,
} from "@plane/types";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useModule } from "@/hooks/store/use-module";
import { useStates } from "@/hooks/store/use-state";
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
//
import { getGroupByColumns } from "../utils";
import { KanBan } from "./default";
@@ -6,7 +6,7 @@ import { IIssueDisplayProperties, TGroupedIssues } from "@plane/types";
// components
import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC";
// hooks
import { useIssue } from "@/hooks/store/use-issue";
import { useIssue } from "@/hooks/store";
import { List } from "./default";
type Props = {
@@ -13,8 +13,7 @@ import { cn } from "@plane/utils";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useIssueDetails, usePublish } from "@/hooks/store";
//
import { IssueProperties } from "../properties/all-properties";
@@ -11,11 +11,7 @@ import {
TLoader,
} from "@plane/types";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useModule } from "@/hooks/store/use-module";
import { useStates } from "@/hooks/store/use-state";
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
//
import { getGroupByColumns } from "../utils";
import { ListGroup } from "./list-group";
@@ -0,0 +1,2 @@
export * from "./block";
export * from "./blocks-list";
@@ -2,23 +2,27 @@
import { observer } from "mobx-react";
import { Layers, Link, Paperclip } from "lucide-react";
// plane imports
import type { IIssueDisplayProperties } from "@plane/types";
// plane types
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// components
import {
IssueBlockDate,
IssueBlockLabels,
IssueBlockPriority,
IssueBlockState,
IssueBlockMembers,
IssueBlockModules,
IssueBlockCycle,
} from "@/components/issues";
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
// helpers
import { getDate } from "@/helpers/date-time.helper";
//// hooks
import { IIssue } from "@/types/issue";
import { IssueBlockCycle } from "./cycle";
import { IssueBlockDate } from "./due-date";
import { IssueBlockLabels } from "./labels";
import { IssueBlockMembers } from "./member";
import { IssueBlockModules } from "./modules";
import { IssueBlockPriority } from "./priority";
import { IssueBlockState } from "./state";
export interface IIssueProperties {
issue: IIssue;
@@ -8,7 +8,7 @@ import { cn } from "@plane/utils";
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
// hooks
import { useStates } from "@/hooks/store/use-state";
import { useStates } from "@/hooks/store";
type Props = {
due_date: string | undefined;
@@ -0,0 +1,8 @@
export * from "./due-date";
export * from "./labels";
export * from "./priority";
export * from "./state";
export * from "./cycle";
export * from "./member";
export * from "./modules";
export * from "./all-properties";
@@ -2,10 +2,8 @@
import { observer } from "mobx-react";
import { Tags } from "lucide-react";
// plane imports
import { Tooltip } from "@plane/ui";
// hooks
import { useLabel } from "@/hooks/store/use-label";
import { useLabel } from "@/hooks/store";
type Props = {
labelIds: string[];
@@ -6,7 +6,7 @@ import { StateGroupIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useStates } from "@/hooks/store/use-state";
import { useStates } from "@/hooks/store";
type Props = {
stateId: string | undefined;
@@ -4,18 +4,15 @@ import { FC, useEffect } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
// hooks
import { useIssue } from "@/hooks/store/use-issue";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssue, useIssueDetails, useIssueFilter } from "@/hooks/store";
// store
import type { PublishStore } from "@/store/publish/publish.store";
// local imports
import { PublishStore } from "@/store/publish/publish.store";
// assets
import { SomethingWentWrongError } from "./error";
import { IssueKanbanLayoutRoot } from "./kanban/base-kanban-root";
import { IssuesListLayoutRoot } from "./list/base-list-root";
type Props = {
peekId: string | undefined;
@@ -1,7 +1,6 @@
import { ReactNode } from "react";
import { observer } from "mobx-react";
// plane imports
import type { IIssueDisplayProperties } from "@plane/types";
import { IIssueDisplayProperties } from "@plane/types";
interface IWithDisplayPropertiesHOC {
displayProperties: IIssueDisplayProperties;
@@ -4,21 +4,17 @@ import { useEffect, FC } from "react";
import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
// components
import { IssuesLayoutSelection, NavbarTheme, UserAvatar } from "@/components/issues";
import { IssueFiltersDropdown } from "@/components/issues/filters";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssueFilter, useIssueDetails } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
// store
import type { PublishStore } from "@/store/publish/publish.store";
import { PublishStore } from "@/store/publish/publish.store";
// types
import type { TIssueLayout } from "@/types/issue";
// local imports
import { IssuesLayoutSelection } from "./layout-selection";
import { NavbarTheme } from "./theme";
import { UserAvatar } from "./user-avatar";
import { TIssueLayout } from "@/types/issue";
export type NavbarControlsProps = {
publishSettings: PublishStore;
@@ -1 +1,5 @@
export * from "./controls";
export * from "./layout-selection";
export * from "./root";
export * from "./theme";
export * from "./user-avatar";
@@ -11,7 +11,7 @@ import { Tooltip } from "@plane/ui";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
import { useIssueFilter } from "@/hooks/store";
// mobx
import { TIssueLayout } from "@/types/issue";
import { IssueLayoutIcon } from "./layout-icon";
@@ -1,14 +1,12 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { Briefcase } from "lucide-react";
// components
import { ProjectLogo } from "@/components/common/project-logo";
import { ProjectLogo } from "@/components/common";
import { NavbarControls } from "@/components/issues";
// store
import type { PublishStore } from "@/store/publish/publish.store";
// local imports
import { NavbarControls } from "./controls";
import { PublishStore } from "@/store/publish/publish.store";
type Props = {
publishSettings: PublishStore;
@@ -15,7 +15,7 @@ import { getFileURL } from "@plane/utils";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useUser } from "@/hooks/store/use-user";
import { useUser } from "@/hooks/store";
const authService = new AuthService();
@@ -11,9 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
// editor components
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useUser } from "@/hooks/store/use-user";
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
// services
const fileService = new SitesFileService();
@@ -8,14 +8,12 @@ import { EditorRefApi } from "@plane/editor";
import { TIssuePublicComment } from "@plane/types";
import { getFileURL } from "@plane/utils";
// components
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
import { CommentReactions } from "@/components/issues/peek-overview/comment/comment-reactions";
import { LiteTextEditor } from "@/components/editor";
import { CommentReactions } from "@/components/issues/peek-overview";
// helpers
import { timeAgo } from "@/helpers/date-time.helper";
// hooks
import { usePublish } from "@/hooks/store/publish";
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useUser } from "@/hooks/store/use-user";
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
type Props = {
@@ -12,8 +12,7 @@ import { ReactionSelector } from "@/components/ui";
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueDetails } from "@/hooks/store/use-issue-details";
import { useUser } from "@/hooks/store/use-user";
import { useIssueDetails, useUser } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
type Props = {
@@ -0,0 +1,3 @@
export * from "./add-comment";
export * from "./comment-detail-card";
export * from "./comment-reactions";

Some files were not shown because too many files have changed in this diff Show More